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.
- package/README.md +39 -4
- package/package.json +5 -2
- package/src/agents/aider-agent.js +1 -2
- package/src/agents/base-agent.js +63 -1
- package/src/agents/claude-agent.js +3 -4
- package/src/agents/codex-agent.js +1 -2
- package/src/agents/gemini-agent.js +1 -2
- package/src/agents/index.js +11 -2
- package/src/agents/opencode-agent.js +1 -2
- package/src/brain/decision-tracker.js +123 -0
- package/src/brain/decisor.js +243 -0
- package/src/brain/solomon-consult.js +171 -0
- package/src/budget/comparison.js +96 -0
- package/src/checks/binaries.js +123 -0
- package/src/checks/ci.js +105 -0
- package/src/checks/config-files.js +217 -0
- package/src/checks/dir-setup.js +63 -0
- package/src/checks/mcp-health.js +157 -0
- package/src/checks/node.js +61 -0
- package/src/checks/ports.js +110 -0
- package/src/checks/rtk.js +36 -0
- package/src/checks/runner.js +267 -0
- package/src/checks/skills.js +49 -0
- package/src/checks/sonar.js +67 -0
- package/src/checks/system.js +53 -0
- package/src/checks/tokens.js +96 -0
- package/src/checks/types.js +105 -0
- package/src/cli.js +29 -3
- package/src/commands/board.js +23 -0
- package/src/commands/doctor.js +130 -362
- package/src/commands/resume.js +1 -1
- package/src/commands/run.js +2 -1
- package/src/commands/skills.js +49 -0
- package/src/config/schema.js +222 -0
- package/src/config.js +32 -3
- package/src/guards/policy-resolver.js +8 -0
- package/src/hu/store.js +9 -0
- package/src/infrastructure/command-runner.js +44 -0
- package/src/infrastructure/environment.js +38 -0
- package/src/infrastructure/file-system-service.js +63 -0
- package/src/infrastructure/mocks.js +182 -0
- package/src/mcp/handlers/management-handlers.js +7 -2
- package/src/mcp/tools.js +10 -3
- package/src/orchestrator/config-init.js +16 -1
- package/src/orchestrator/flow-runner.js +2085 -0
- package/src/orchestrator/pipeline-context.js +17 -0
- package/src/orchestrator/preflight-checks.js +78 -4
- package/src/orchestrator/solomon-escalation.js +55 -0
- package/src/orchestrator/stages/stage-executor.js +155 -0
- package/src/orchestrator.js +19 -1923
- package/src/prompts/architect.js +2 -2
- package/src/prompts/coder.js +2 -2
- package/src/prompts/prompt-resolver.js +127 -0
- package/src/prompts/reviewer.js +2 -2
- package/src/roles/agent-role.js +9 -0
- package/src/roles/architect-role.js +3 -1
- package/src/roles/base-role.js +40 -2
- package/src/roles/coder-role.js +3 -1
- package/src/roles/security-role.js +12 -0
- package/src/roles/tester-role.js +11 -0
- package/src/session/journal/decisions-writer.js +200 -0
- package/src/session/journal/iteration-logger.js +194 -0
- package/src/session/journal/summary-writer.js +176 -0
- package/src/session/journal/tree-writer.js +164 -0
- package/src/skills/semantic-detector.js +147 -0
- package/src/skills/skill-detector.js +186 -8
- package/src/skills/skill-loader.js +131 -1
- package/src/skills/skills-cache.js +128 -0
- package/src/types/agent.js +55 -0
- package/src/types/config.js +114 -0
- package/src/types/hu.js +47 -0
- package/src/types/index.js +38 -0
- package/src/types/policy.js +43 -0
- package/src/types/session.js +74 -0
- package/src/types/solomon.js +87 -0
- package/src/types/stage.js +68 -0
- package/src/utils/display/event-handlers.js +389 -0
- package/src/utils/display/formatters.js +132 -0
- package/src/utils/display/header.js +48 -0
- package/src/utils/display/session-summary.js +108 -0
- package/src/utils/display/solomon.js +42 -0
- package/src/utils/port-check.js +45 -0
- package/src/utils/port-occupant.js +95 -0
- package/src/utils/provider-env.js +116 -0
- package/src/utils/with-timeout.js +42 -0
- package/templates/roles/architect/anthropic.md +65 -0
- package/templates/roles/architect/default.md +63 -0
- package/templates/roles/architect/google.md +52 -0
- package/templates/roles/architect/openai.md +51 -0
- package/templates/roles/architect/opencode.md +50 -0
- package/templates/roles/coder/anthropic.md +72 -0
- package/templates/roles/coder/default.md +69 -0
- package/templates/roles/coder/google.md +78 -0
- package/templates/roles/coder/openai.md +59 -0
- package/templates/roles/coder/opencode.md +49 -0
- package/templates/roles/planner/anthropic.md +50 -0
- package/templates/roles/planner/default.md +48 -0
- package/templates/roles/planner/google.md +66 -0
- package/templates/roles/planner/openai.md +46 -0
- package/templates/roles/planner/opencode.md +53 -0
- package/templates/roles/reviewer/anthropic.md +61 -0
- package/templates/roles/reviewer/default.md +62 -0
- package/templates/roles/reviewer/google.md +63 -0
- package/templates/roles/reviewer/openai.md +57 -0
- package/templates/roles/reviewer/opencode.md +42 -0
- package/templates/roles/solomon/anthropic.md +71 -0
- package/templates/roles/solomon/default.md +83 -0
- package/templates/roles/solomon/google.md +61 -0
- package/templates/roles/solomon/openai.md +53 -0
- package/templates/roles/solomon/opencode.md +55 -0
- package/src/utils/display.js +0 -669
package/src/checks/ci.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI infrastructure checks: required workflows, gh CLI, and GitHub secrets.
|
|
3
|
+
* Only applies when `config.ci.enabled === true`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { exists } from "../utils/fs.js";
|
|
8
|
+
import { runCommand } from "../utils/process.js";
|
|
9
|
+
import { checkBinary } from "../utils/agent-detect.js";
|
|
10
|
+
import { STRATEGY } from "./types.js";
|
|
11
|
+
|
|
12
|
+
const REQUIRED_WORKFLOWS = ["kj-ci-gateway.yml", "automerge.yml", "houston-override.yml"];
|
|
13
|
+
|
|
14
|
+
function ciEnabled(config) {
|
|
15
|
+
return config.ci?.enabled === true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createWorkflowCheck(workflow) {
|
|
19
|
+
return {
|
|
20
|
+
name: `ci:workflow:${workflow}`,
|
|
21
|
+
label: `CI workflow: ${workflow}`,
|
|
22
|
+
strategy: STRATEGY.MANUAL,
|
|
23
|
+
applies: ciEnabled,
|
|
24
|
+
async detect({ config }) {
|
|
25
|
+
const projectDir = config.projectDir || process.cwd();
|
|
26
|
+
const wfPath = path.join(projectDir, ".github", "workflows", workflow);
|
|
27
|
+
const wfExists = await exists(wfPath);
|
|
28
|
+
return {
|
|
29
|
+
ok: wfExists,
|
|
30
|
+
severity: "fail",
|
|
31
|
+
detail: wfExists ? "Found" : "Not found",
|
|
32
|
+
fix: wfExists
|
|
33
|
+
? undefined
|
|
34
|
+
: `Run 'kj init --scaffold-ci' or copy from karajan-code/templates/workflows/${workflow}`,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createGhCliCheck() {
|
|
41
|
+
return {
|
|
42
|
+
name: "ci:gh",
|
|
43
|
+
label: "CI: gh CLI",
|
|
44
|
+
strategy: STRATEGY.MANUAL,
|
|
45
|
+
applies: ciEnabled,
|
|
46
|
+
async detect() {
|
|
47
|
+
const gh = await checkBinary("gh");
|
|
48
|
+
return {
|
|
49
|
+
ok: gh.ok,
|
|
50
|
+
severity: "fail",
|
|
51
|
+
detail: gh.ok ? gh.version : "Not found",
|
|
52
|
+
fix: gh.ok ? undefined : "Install GitHub CLI: https://cli.github.com/",
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createGhSecretsCheck() {
|
|
59
|
+
return {
|
|
60
|
+
name: "ci:secrets",
|
|
61
|
+
label: "CI: GitHub secrets",
|
|
62
|
+
strategy: STRATEGY.MANUAL,
|
|
63
|
+
applies: ciEnabled,
|
|
64
|
+
async detect() {
|
|
65
|
+
try {
|
|
66
|
+
const { detectRepo } = await import("../ci/repo.js");
|
|
67
|
+
const repo = await detectRepo();
|
|
68
|
+
if (!repo) {
|
|
69
|
+
return { ok: true, severity: "info", detail: "No GitHub repo detected (skipped)" };
|
|
70
|
+
}
|
|
71
|
+
const res = await runCommand("gh", ["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
|
|
72
|
+
if (res.exitCode !== 0) {
|
|
73
|
+
return { ok: true, severity: "info", detail: "Cannot query secrets (gh unauthenticated?)" };
|
|
74
|
+
}
|
|
75
|
+
const names = new Set(res.stdout.split("\n").map((s) => s.trim()));
|
|
76
|
+
const hasAppId = names.has("KJ_CI_APP_ID");
|
|
77
|
+
const hasKey = names.has("KJ_CI_PRIVATE_KEY");
|
|
78
|
+
const secretsOk = hasAppId && hasKey;
|
|
79
|
+
const missing = [!hasAppId && "KJ_CI_APP_ID", !hasKey && "KJ_CI_PRIVATE_KEY"].filter(Boolean).join(" ");
|
|
80
|
+
return {
|
|
81
|
+
ok: secretsOk,
|
|
82
|
+
severity: "fail",
|
|
83
|
+
detail: secretsOk ? "KJ_CI_APP_ID + KJ_CI_PRIVATE_KEY found" : `Missing: ${missing}`,
|
|
84
|
+
fix: secretsOk ? undefined : "Add KJ_CI_APP_ID and KJ_CI_PRIVATE_KEY as GitHub repository secrets",
|
|
85
|
+
};
|
|
86
|
+
} catch {
|
|
87
|
+
return { ok: true, severity: "info", detail: "Cannot check secrets (unexpected error, skipped)" };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Aggregate of CI checks. Each check carries its own `applies` so the runner
|
|
95
|
+
* marks them SKIPPED when CI is disabled.
|
|
96
|
+
*/
|
|
97
|
+
export function getCiChecks() {
|
|
98
|
+
const checks = [];
|
|
99
|
+
for (const wf of REQUIRED_WORKFLOWS) {
|
|
100
|
+
checks.push(createWorkflowCheck(wf));
|
|
101
|
+
}
|
|
102
|
+
checks.push(createGhCliCheck());
|
|
103
|
+
checks.push(createGhSecretsCheck());
|
|
104
|
+
return checks;
|
|
105
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config and rule-file checks.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - kj.config.yml existence (strategy: prompt — offer to run kj init)
|
|
6
|
+
* - Review / coder rules discovery (strategy: none, informational)
|
|
7
|
+
* - Agent config files: ~/.claude.json, ~/.codex/config.toml,
|
|
8
|
+
* ~/.karajan/kj.config.yml (strategy: manual — structural fixes needed)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs/promises";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { exists } from "../utils/fs.js";
|
|
15
|
+
import { getConfigPath } from "../config.js";
|
|
16
|
+
import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
|
|
17
|
+
import { withDocLink } from "../utils/doc-links.js";
|
|
18
|
+
import { STRATEGY } from "./types.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* kj.config.yml presence check.
|
|
22
|
+
*/
|
|
23
|
+
export function createConfigFileCheck() {
|
|
24
|
+
return {
|
|
25
|
+
name: "config",
|
|
26
|
+
label: "Config file",
|
|
27
|
+
strategy: STRATEGY.PROMPT,
|
|
28
|
+
describe: "Run 'kj init' to scaffold a default kj.config.yml",
|
|
29
|
+
async detect() {
|
|
30
|
+
const configPath = getConfigPath();
|
|
31
|
+
const configExists = await exists(configPath);
|
|
32
|
+
return {
|
|
33
|
+
ok: configExists,
|
|
34
|
+
severity: "fail",
|
|
35
|
+
detail: configExists ? configPath : "Not found",
|
|
36
|
+
fix: configExists ? undefined : withDocLink("Run 'kj init' to create the config file.", "config_missing"),
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Review rules (.md) informational check.
|
|
44
|
+
*/
|
|
45
|
+
export function createReviewRulesCheck() {
|
|
46
|
+
return {
|
|
47
|
+
name: "review-rules",
|
|
48
|
+
label: "Reviewer rules (.md)",
|
|
49
|
+
strategy: STRATEGY.NONE,
|
|
50
|
+
async detect({ config }) {
|
|
51
|
+
const projectDir = config.projectDir || process.cwd();
|
|
52
|
+
const paths = resolveRoleMdPath("reviewer", projectDir);
|
|
53
|
+
const rules = await loadFirstExisting(paths);
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
severity: "info",
|
|
57
|
+
detail: rules ? "Found" : "Not found (will use defaults)",
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Coder rules (.md) informational check.
|
|
65
|
+
*/
|
|
66
|
+
export function createCoderRulesCheck() {
|
|
67
|
+
return {
|
|
68
|
+
name: "coder-rules",
|
|
69
|
+
label: "Coder rules (.md)",
|
|
70
|
+
strategy: STRATEGY.NONE,
|
|
71
|
+
async detect({ config }) {
|
|
72
|
+
const projectDir = config.projectDir || process.cwd();
|
|
73
|
+
const paths = resolveRoleMdPath("coder", projectDir);
|
|
74
|
+
const rules = await loadFirstExisting(paths);
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
severity: "info",
|
|
78
|
+
detail: rules ? "Found" : "Not found (will use defaults)",
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Detect duplicate TOML table headers (e.g. [mcp_servers."karajan-mcp"] twice).
|
|
86
|
+
*/
|
|
87
|
+
function findDuplicateTomlKeys(content) {
|
|
88
|
+
const tableHeaders = [];
|
|
89
|
+
const duplicates = [];
|
|
90
|
+
for (const line of content.split("\n")) {
|
|
91
|
+
const match = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
92
|
+
if (match) {
|
|
93
|
+
const key = match[1].trim();
|
|
94
|
+
if (tableHeaders.includes(key)) {
|
|
95
|
+
duplicates.push(key);
|
|
96
|
+
} else {
|
|
97
|
+
tableHeaders.push(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return duplicates;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Claude config (~/.claude.json) validity.
|
|
106
|
+
*/
|
|
107
|
+
export function createClaudeConfigCheck() {
|
|
108
|
+
return {
|
|
109
|
+
name: "agent-config:claude",
|
|
110
|
+
label: "Agent config: claude (~/.claude.json)",
|
|
111
|
+
strategy: STRATEGY.MANUAL,
|
|
112
|
+
async detect() {
|
|
113
|
+
const home = os.homedir();
|
|
114
|
+
const claudeJsonPath = path.join(home, ".claude.json");
|
|
115
|
+
try {
|
|
116
|
+
const raw = await fs.readFile(claudeJsonPath, "utf8");
|
|
117
|
+
JSON.parse(raw);
|
|
118
|
+
return { ok: true, severity: "info", detail: "Valid JSON" };
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (err.code === "ENOENT") {
|
|
121
|
+
// Not configured: Claude may not be in use. Informational skip.
|
|
122
|
+
return { ok: true, severity: "info", detail: "Not present (skipped)" };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
severity: "warn",
|
|
127
|
+
detail: `Invalid JSON: ${err.message.split("\n")[0]}`,
|
|
128
|
+
fix: "Fix the JSON syntax in ~/.claude.json. Common issues: trailing commas, missing quotes.",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Codex config (~/.codex/config.toml) validity.
|
|
137
|
+
*/
|
|
138
|
+
export function createCodexConfigCheck() {
|
|
139
|
+
return {
|
|
140
|
+
name: "agent-config:codex",
|
|
141
|
+
label: "Agent config: codex (~/.codex/config.toml)",
|
|
142
|
+
strategy: STRATEGY.MANUAL,
|
|
143
|
+
async detect() {
|
|
144
|
+
const home = os.homedir();
|
|
145
|
+
const codexTomlPath = path.join(home, ".codex", "config.toml");
|
|
146
|
+
try {
|
|
147
|
+
const raw = await fs.readFile(codexTomlPath, "utf8");
|
|
148
|
+
const duplicates = findDuplicateTomlKeys(raw);
|
|
149
|
+
if (duplicates.length > 0) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
severity: "warn",
|
|
153
|
+
detail: `Duplicate TOML keys: ${duplicates.join(", ")}`,
|
|
154
|
+
fix: `Remove duplicate entries in ~/.codex/config.toml: ${duplicates.join(", ")}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return { ok: true, severity: "info", detail: "Valid TOML (no duplicate keys)" };
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err.code === "ENOENT") {
|
|
160
|
+
return { ok: true, severity: "info", detail: "Not present (skipped)" };
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
severity: "warn",
|
|
165
|
+
detail: `Cannot read: ${err.message.split("\n")[0]}`,
|
|
166
|
+
fix: "Check file permissions on ~/.codex/config.toml",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* KJ config (~/.karajan/kj.config.yml) YAML validity (separate from existence).
|
|
175
|
+
*/
|
|
176
|
+
export function createKjConfigYamlCheck() {
|
|
177
|
+
return {
|
|
178
|
+
name: "agent-config:karajan",
|
|
179
|
+
label: "Agent config: karajan (kj.config.yml)",
|
|
180
|
+
strategy: STRATEGY.MANUAL,
|
|
181
|
+
async detect() {
|
|
182
|
+
const kjConfigPath = getConfigPath();
|
|
183
|
+
try {
|
|
184
|
+
const raw = await fs.readFile(kjConfigPath, "utf8");
|
|
185
|
+
const yaml = await import("js-yaml");
|
|
186
|
+
yaml.default.load(raw);
|
|
187
|
+
return { ok: true, severity: "info", detail: "Valid YAML" };
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err.code === "ENOENT") {
|
|
190
|
+
// Covered by createConfigFileCheck — don't duplicate the failure
|
|
191
|
+
return { ok: true, severity: "info", detail: "Not present (covered by config check)" };
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
severity: "fail",
|
|
196
|
+
detail: `Invalid YAML: ${err.message.split("\n")[0]}`,
|
|
197
|
+
fix: `Fix YAML syntax in ${kjConfigPath}. Run 'kj init' to regenerate if needed.`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Aggregate: all config-file-related checks.
|
|
206
|
+
* @returns {import("./types.js").Check[]}
|
|
207
|
+
*/
|
|
208
|
+
export function getConfigFileChecks() {
|
|
209
|
+
return [
|
|
210
|
+
createConfigFileCheck(),
|
|
211
|
+
createReviewRulesCheck(),
|
|
212
|
+
createCoderRulesCheck(),
|
|
213
|
+
createClaudeConfigCheck(),
|
|
214
|
+
createCodexConfigCheck(),
|
|
215
|
+
createKjConfigYamlCheck(),
|
|
216
|
+
];
|
|
217
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure ~/.karajan/ directory tree exists. Strategy: auto — creating
|
|
3
|
+
* directories under the user's home is non-invasive.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { STRATEGY } from "./types.js";
|
|
10
|
+
|
|
11
|
+
const REQUIRED_DIRS = [
|
|
12
|
+
"", // ~/.karajan/
|
|
13
|
+
"sessions",
|
|
14
|
+
"skills-cache",
|
|
15
|
+
"logs",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function createKarajanDirsCheck() {
|
|
19
|
+
return {
|
|
20
|
+
name: "karajan-dirs",
|
|
21
|
+
label: "~/.karajan directory tree",
|
|
22
|
+
strategy: STRATEGY.AUTO,
|
|
23
|
+
describe: "Create missing ~/.karajan subdirectories",
|
|
24
|
+
async detect() {
|
|
25
|
+
const home = os.homedir();
|
|
26
|
+
const root = path.join(home, ".karajan");
|
|
27
|
+
const missing = [];
|
|
28
|
+
for (const sub of REQUIRED_DIRS) {
|
|
29
|
+
const full = sub ? path.join(root, sub) : root;
|
|
30
|
+
try {
|
|
31
|
+
const stat = await fs.stat(full);
|
|
32
|
+
if (!stat.isDirectory()) missing.push(full);
|
|
33
|
+
} catch {
|
|
34
|
+
missing.push(full);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (missing.length === 0) {
|
|
38
|
+
return { ok: true, severity: "info", detail: `All ${REQUIRED_DIRS.length} directories present under ${root}` };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
severity: "fail",
|
|
43
|
+
detail: `Missing: ${missing.map((m) => path.relative(home, m)).join(", ")}`,
|
|
44
|
+
fix: `mkdir -p ${missing.join(" ")}`,
|
|
45
|
+
extra: { missing },
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
async remediate({ extra }) {
|
|
49
|
+
const missing = extra?.missing || [];
|
|
50
|
+
for (const dir of missing) {
|
|
51
|
+
await fs.mkdir(dir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
fixed: true,
|
|
55
|
+
detail: `Created ${missing.length} director${missing.length === 1 ? "y" : "ies"} under ~/.karajan`,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getDirSetupChecks() {
|
|
62
|
+
return [createKarajanDirsCheck()];
|
|
63
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server health checks.
|
|
3
|
+
*
|
|
4
|
+
* Karajan integrates with several MCP servers — karajan-mcp (stdio), Serena,
|
|
5
|
+
* Sonar MCP, Planning Game MCP, etc. This module verifies they respond to
|
|
6
|
+
* a minimal JSON-RPC `initialize` call within a short deadline.
|
|
7
|
+
*
|
|
8
|
+
* For stdio MCPs that Karajan spawns itself (karajan-mcp), a failed ping is
|
|
9
|
+
* auto-remediable by re-spawning. External MCPs (Serena) are warn-only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { STRATEGY } from "./types.js";
|
|
16
|
+
import { withTimeout, TimeoutError } from "../utils/with-timeout.js";
|
|
17
|
+
import { checkBinary } from "../utils/agent-detect.js";
|
|
18
|
+
|
|
19
|
+
const PING_TIMEOUT_MS = 3000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Send a JSON-RPC `initialize` to an MCP server over stdio and wait for
|
|
23
|
+
* any response. Kills the process immediately after.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} command
|
|
26
|
+
* @param {string[]} args
|
|
27
|
+
* @returns {Promise<{ok: boolean, detail: string}>}
|
|
28
|
+
*/
|
|
29
|
+
async function pingMcpStdio(command, args) {
|
|
30
|
+
const handshake = () =>
|
|
31
|
+
new Promise((resolve) => {
|
|
32
|
+
let child;
|
|
33
|
+
try {
|
|
34
|
+
child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
resolve({ ok: false, detail: `spawn failed: ${err.message}` });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let settled = false;
|
|
40
|
+
const finish = (ok, detail) => {
|
|
41
|
+
if (settled) return;
|
|
42
|
+
settled = true;
|
|
43
|
+
try {
|
|
44
|
+
child.kill();
|
|
45
|
+
} catch { /* already exited */ }
|
|
46
|
+
resolve({ ok, detail });
|
|
47
|
+
};
|
|
48
|
+
child.on("error", (err) => finish(false, `process error: ${err.message}`));
|
|
49
|
+
child.on("exit", (code) => {
|
|
50
|
+
if (!settled) finish(false, `exited with code ${code} before response`);
|
|
51
|
+
});
|
|
52
|
+
child.stdout.on("data", (buf) => {
|
|
53
|
+
const text = buf.toString();
|
|
54
|
+
// Any JSON-RPC response line counts as alive.
|
|
55
|
+
if (text.includes("\"jsonrpc\"") || text.includes("\"result\"") || text.includes("\"error\"")) {
|
|
56
|
+
finish(true, "Responded to initialize");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// Send initialize request
|
|
60
|
+
const req = JSON.stringify({
|
|
61
|
+
jsonrpc: "2.0",
|
|
62
|
+
id: 1,
|
|
63
|
+
method: "initialize",
|
|
64
|
+
params: {
|
|
65
|
+
protocolVersion: "2024-11-05",
|
|
66
|
+
clientInfo: { name: "kj-doctor", version: "1.0.0" },
|
|
67
|
+
capabilities: {},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
try {
|
|
71
|
+
child.stdin.write(req + "\n");
|
|
72
|
+
} catch (err) {
|
|
73
|
+
finish(false, `stdin write failed: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
return await withTimeout(handshake(), PING_TIMEOUT_MS, "mcp-ping");
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (err instanceof TimeoutError) {
|
|
80
|
+
return { ok: false, detail: `No response within ${PING_TIMEOUT_MS}ms` };
|
|
81
|
+
}
|
|
82
|
+
return { ok: false, detail: err.message };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* karajan-mcp health — stdio server bundled with this package.
|
|
88
|
+
* Strategy: auto (we can re-spawn it ourselves).
|
|
89
|
+
*/
|
|
90
|
+
export function createKarajanMcpCheck() {
|
|
91
|
+
return {
|
|
92
|
+
name: "mcp:karajan",
|
|
93
|
+
label: "MCP: karajan-mcp",
|
|
94
|
+
strategy: STRATEGY.AUTO,
|
|
95
|
+
describe: "Respawn karajan-mcp stdio process",
|
|
96
|
+
async detect() {
|
|
97
|
+
const binPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../bin/karajan-mcp.js");
|
|
98
|
+
const result = await pingMcpStdio(process.execPath, [binPath]);
|
|
99
|
+
return {
|
|
100
|
+
ok: result.ok,
|
|
101
|
+
severity: result.ok ? "info" : "warn",
|
|
102
|
+
detail: result.ok ? result.detail : `karajan-mcp did not respond: ${result.detail}`,
|
|
103
|
+
fix: result.ok ? undefined : "Re-run karajan-mcp via `node bin/karajan-mcp.js`",
|
|
104
|
+
extra: { binPath },
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
async remediate({ extra }) {
|
|
108
|
+
// Spawn once detached so the MCP client can pick it up for subsequent runs.
|
|
109
|
+
// This is idempotent: if one instance is already listening, another will
|
|
110
|
+
// coexist over a different pipe.
|
|
111
|
+
try {
|
|
112
|
+
spawn(process.execPath, [extra.binPath], { detached: true, stdio: "ignore" }).unref();
|
|
113
|
+
return { fixed: true, detail: "Re-spawned karajan-mcp in background" };
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return { fixed: false, detail: `Failed to respawn: ${err.message}` };
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Serena MCP — external tool the user installs themselves. Warn only when
|
|
123
|
+
* enabled in config. Strategy: manual (we don't manage its lifecycle).
|
|
124
|
+
*/
|
|
125
|
+
export function createSerenaMcpCheck() {
|
|
126
|
+
return {
|
|
127
|
+
name: "mcp:serena",
|
|
128
|
+
label: "MCP: serena",
|
|
129
|
+
strategy: STRATEGY.MANUAL,
|
|
130
|
+
applies: (config) => config?.serena?.enabled === true,
|
|
131
|
+
async detect() {
|
|
132
|
+
const bin = await checkBinary("serena");
|
|
133
|
+
if (!bin.ok) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
severity: "warn",
|
|
137
|
+
detail: "serena binary not on PATH",
|
|
138
|
+
fix: "Install Serena: uvx --from git+https://github.com/oraios/serena serena",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const result = await pingMcpStdio("serena", ["start", "--mcp"]);
|
|
142
|
+
return {
|
|
143
|
+
ok: result.ok,
|
|
144
|
+
severity: result.ok ? "info" : "warn",
|
|
145
|
+
detail: result.ok ? "Serena responded to initialize" : `No response: ${result.detail}`,
|
|
146
|
+
fix: result.ok ? undefined : "Restart Serena manually and verify connectivity.",
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Aggregate of MCP health checks. Only enabled servers run their ping.
|
|
154
|
+
*/
|
|
155
|
+
export function getMcpHealthChecks() {
|
|
156
|
+
return [createKarajanMcpCheck(), createSerenaMcpCheck()];
|
|
157
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js runtime version check. Requires Node >= 20 (minimum version that
|
|
3
|
+
* ships structuredClone, Array.prototype.findLast, AbortSignal.timeout, and
|
|
4
|
+
* stable fetch). Strategy: manual — we cannot auto-upgrade the runtime.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { STRATEGY } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export const MIN_NODE_MAJOR = 20;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Accepts a version string in the form "v20.12.1" or "20.12.1" and returns
|
|
13
|
+
* an object { major, minor, patch } or null when the shape is invalid.
|
|
14
|
+
*/
|
|
15
|
+
export function parseNodeVersion(versionString) {
|
|
16
|
+
const str = String(versionString || "").trim();
|
|
17
|
+
const m = str.match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
18
|
+
if (!m) return null;
|
|
19
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create the Node version check. Accepts an optional override of the
|
|
24
|
+
* reported version (useful in tests).
|
|
25
|
+
*
|
|
26
|
+
* @param {{ version?: string }} [opts]
|
|
27
|
+
* @returns {import("./types.js").Check}
|
|
28
|
+
*/
|
|
29
|
+
export function createNodeVersionCheck(opts = {}) {
|
|
30
|
+
return {
|
|
31
|
+
name: "node-version",
|
|
32
|
+
label: "Node.js runtime",
|
|
33
|
+
strategy: STRATEGY.MANUAL,
|
|
34
|
+
async detect() {
|
|
35
|
+
const raw = opts.version ?? process.env.KJ_FORCE_NODE_VERSION ?? process.version;
|
|
36
|
+
const parsed = parseNodeVersion(raw);
|
|
37
|
+
if (!parsed) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
severity: "fail",
|
|
41
|
+
detail: `Unrecognized Node version: ${raw}`,
|
|
42
|
+
fix: `Install Node ${MIN_NODE_MAJOR}+ from https://nodejs.org/ (current: ${raw}).`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (parsed.major < MIN_NODE_MAJOR) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
severity: "fail",
|
|
49
|
+
detail: `Node ${raw} detected, requires >=${MIN_NODE_MAJOR}`,
|
|
50
|
+
fix: `Upgrade Node to ${MIN_NODE_MAJOR}+ (nvm install ${MIN_NODE_MAJOR} && nvm use ${MIN_NODE_MAJOR}, or brew upgrade node).`,
|
|
51
|
+
extra: { detected: parsed, required: MIN_NODE_MAJOR },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, severity: "info", detail: `${raw} (>= ${MIN_NODE_MAJOR} required)` };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getNodeChecks() {
|
|
60
|
+
return [createNodeVersionCheck()];
|
|
61
|
+
}
|