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
@@ -1,393 +1,161 @@
1
- import { readFileSync } from "node:fs";
2
- import fs from "node:fs/promises";
3
- import { fileURLToPath } from "node:url";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import { runCommand } from "../utils/process.js";
7
- import { exists } from "../utils/fs.js";
8
- import { getConfigPath } from "../config.js";
9
- import { isSonarReachable } from "../sonar/manager.js";
10
- import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
11
- import { ensureGitRepo } from "../utils/git.js";
12
- import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
13
- import { getInstallCommand } from "../utils/os-detect.js";
14
- import { withDocLink } from "../utils/doc-links.js";
15
-
16
-
17
- function getPackageVersion() {
18
- const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
19
- return JSON.parse(readFileSync(pkgPath, "utf8")).version;
20
- }
21
-
22
- function checkKarajanVersion() {
23
- const version = getPackageVersion();
24
- return {
25
- name: "karajan",
26
- label: "Karajan Code",
27
- ok: true,
28
- detail: `v${version}`,
29
- fix: null
30
- };
31
- }
32
-
33
- async function checkConfigFile() {
34
- const configPath = getConfigPath();
35
- const configExists = await exists(configPath);
36
- return {
37
- name: "config",
38
- label: "Config file",
39
- ok: configExists,
40
- detail: configExists ? configPath : "Not found",
41
- fix: configExists ? null : withDocLink("Run 'kj init' to create the config file.", "config_missing")
42
- };
43
- }
44
-
45
- async function checkGitRepo() {
46
- let gitOk = false;
47
- try {
48
- gitOk = await ensureGitRepo();
49
- } catch { /* git check failed */
50
- gitOk = false;
51
- }
52
- return {
53
- name: "git",
54
- label: "Git repository",
55
- ok: gitOk,
56
- detail: gitOk ? "Inside a git repository" : "Not a git repository",
57
- fix: gitOk ? null : "Run 'git init' or navigate to a git-managed project."
58
- };
59
- }
1
+ /**
2
+ * `kj doctor` command. Runs the unified check runner and prints a
3
+ * human-readable report by default. Supports flags:
4
+ *
5
+ * --check-only : only detect, do not apply fixes
6
+ * --yes / -y : auto-confirm prompts for invasive remediations (CI mode)
7
+ * --json : emit machine-readable JSON to stdout
8
+ * --verbose : include fix hints and timing per check
9
+ *
10
+ * Exit code is 1 whenever any check ends with FAIL (including remediation
11
+ * failures). WARN, OK, FIXED, SKIPPED and TIMEOUT do not fail the command.
12
+ */
60
13
 
61
- async function checkDocker() {
62
- const docker = await checkBinary("docker", "--version");
63
- return {
64
- name: "docker",
65
- label: "Docker",
66
- ok: docker.ok,
67
- detail: docker.ok ? docker.version : "Not found",
68
- fix: docker.ok ? null : withDocLink("Install Docker: https://docs.docker.com/get-docker/", "sonar_docker")
69
- };
70
- }
14
+ import readline from "node:readline";
15
+ import { runChecks as runCheckPipeline, toLegacyShape } from "../checks/runner.js";
16
+ import { STATUS } from "../checks/types.js";
17
+ import { getSystemChecks } from "../checks/system.js";
18
+ import { getConfigFileChecks } from "../checks/config-files.js";
19
+ import { getBinaryChecks } from "../checks/binaries.js";
20
+ import { getSonarChecks } from "../checks/sonar.js";
21
+ import { getCiChecks } from "../checks/ci.js";
22
+ import { getRtkChecks } from "../checks/rtk.js";
23
+ import { getNodeChecks } from "../checks/node.js";
24
+ import { getPortChecks } from "../checks/ports.js";
25
+ import { getTokenChecks } from "../checks/tokens.js";
26
+ import { getMcpHealthChecks } from "../checks/mcp-health.js";
27
+ import { getSkillsChecks } from "../checks/skills.js";
28
+ import { getDirSetupChecks } from "../checks/dir-setup.js";
71
29
 
72
- function sonarDetail(config, sonarOk, sonarHost) {
73
- if (config.sonarqube?.enabled === false) return "Disabled in config";
74
- if (sonarOk) return `Reachable at ${sonarHost}`;
75
- return `Not reachable at ${sonarHost}`;
30
+ /**
31
+ * Build the list of Check objects applicable to the current config.
32
+ * @param {Object} config
33
+ * @returns {import("../checks/types.js").Check[]}
34
+ */
35
+ function buildChecks(config) {
36
+ return [
37
+ ...getSystemChecks(),
38
+ ...getNodeChecks(),
39
+ ...getDirSetupChecks(),
40
+ ...getConfigFileChecks(),
41
+ ...getBinaryChecks(),
42
+ ...getSonarChecks(),
43
+ ...getPortChecks(),
44
+ ...getTokenChecks(config),
45
+ ...getMcpHealthChecks(),
46
+ ...getSkillsChecks(),
47
+ ...getCiChecks(),
48
+ ...getRtkChecks(),
49
+ ];
76
50
  }
77
51
 
78
- async function checkSonarQube(config) {
79
- const sonarHost = config.sonarqube?.host || "http://localhost:9000";
80
- let sonarOk = false;
81
- if (config.sonarqube?.enabled !== false) {
82
- try {
83
- sonarOk = await isSonarReachable(sonarHost);
84
- } catch { /* sonar check failed */
85
- sonarOk = false;
52
+ /**
53
+ * Interactive terminal confirmation. Returns false when stdin is not a TTY
54
+ * so non-interactive contexts don't hang forever.
55
+ *
56
+ * @param {string} describe
57
+ * @returns {Promise<boolean>}
58
+ */
59
+ function confirmViaTty(describe) {
60
+ return new Promise((resolve) => {
61
+ if (!process.stdin.isTTY) {
62
+ resolve(false);
63
+ return;
86
64
  }
87
- }
88
- const isOkOrDisabled = sonarOk || config.sonarqube?.enabled === false;
89
- return {
90
- name: "sonarqube",
91
- label: "SonarQube",
92
- ok: isOkOrDisabled,
93
- detail: sonarDetail(config, sonarOk, sonarHost),
94
- fix: isOkOrDisabled
95
- ? null
96
- : withDocLink("Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip.", "sonar_docker")
97
- };
98
- }
99
-
100
- async function checkAgentCLIs() {
101
- const checks = [];
102
- for (const agent of KNOWN_AGENTS) {
103
- const result = await checkBinary(agent.name);
104
- checks.push({
105
- name: `agent:${agent.name}`,
106
- label: `Agent: ${agent.name}`,
107
- ok: result.ok,
108
- detail: result.ok ? `${result.version} (${result.path})` : "Not found",
109
- fix: result.ok ? null : withDocLink(`Install: ${agent.install}`, "agent_not_found")
110
- });
111
- }
112
- return checks;
113
- }
114
-
115
- async function checkCoreBinaries() {
116
- const checks = [];
117
- for (const bin of ["node", "npm", "git"]) {
118
- const result = await checkBinary(bin);
119
- checks.push({
120
- name: bin,
121
- label: bin,
122
- ok: result.ok,
123
- detail: result.ok ? result.version : "Not found",
124
- fix: result.ok ? null : `Install ${bin} from its official website.`
125
- });
126
- }
127
- return checks;
128
- }
129
-
130
- async function checkSerena() {
131
- let serenaOk = false;
132
- try {
133
- const serenaCheck = await runCommand("serena", ["--version"]);
134
- serenaOk = serenaCheck.exitCode === 0;
135
- } catch { /* serena not installed */
136
- serenaOk = false;
137
- }
138
- return {
139
- name: "serena",
140
- label: "Serena MCP",
141
- ok: serenaOk,
142
- detail: serenaOk ? "Available" : "Not found (prompts will still include Serena instructions)",
143
- fix: serenaOk ? null : "Install Serena: uvx --from git+https://github.com/oraios/serena serena --help"
144
- };
145
- }
146
-
147
- async function checkCiWorkflows(projectDir) {
148
- const checks = [];
149
- const workflowDir = path.join(projectDir, ".github", "workflows");
150
- const requiredWorkflows = ["kj-ci-gateway.yml", "automerge.yml", "houston-override.yml"];
151
- for (const wf of requiredWorkflows) {
152
- const wfPath = path.join(workflowDir, wf);
153
- const wfExists = await exists(wfPath);
154
- checks.push({
155
- name: `ci:workflow:${wf}`,
156
- label: `CI workflow: ${wf}`,
157
- ok: wfExists,
158
- detail: wfExists ? "Found" : "Not found",
159
- fix: wfExists ? null : `Run 'kj init --scaffold-ci' or copy from karajan-code/templates/workflows/${wf}`
65
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
66
+ rl.question(`? ${describe} [y/N] `, (answer) => {
67
+ rl.close();
68
+ resolve(/^y(es)?$/i.test(answer.trim()));
160
69
  });
161
- }
162
- return checks;
163
- }
164
-
165
- async function checkCiSecrets() {
166
- try {
167
- const { detectRepo } = await import("../ci/repo.js");
168
- const repo = await detectRepo();
169
- if (!repo) return null;
170
-
171
- const secretsRes = await runCommand("gh", ["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
172
- if (secretsRes.exitCode !== 0) return null;
173
-
174
- const names = new Set(secretsRes.stdout.split("\n").map((s) => s.trim()));
175
- const hasAppId = names.has("KJ_CI_APP_ID");
176
- const hasKey = names.has("KJ_CI_PRIVATE_KEY");
177
- const secretsOk = hasAppId && hasKey;
178
- const missing = [!hasAppId && "KJ_CI_APP_ID", !hasKey && "KJ_CI_PRIVATE_KEY"].filter(Boolean).join(" ");
179
- return {
180
- name: "ci:secrets",
181
- label: "CI: GitHub secrets",
182
- ok: secretsOk,
183
- detail: secretsOk ? "KJ_CI_APP_ID + KJ_CI_PRIVATE_KEY found" : `Missing: ${missing}`,
184
- fix: secretsOk ? null : "Add KJ_CI_APP_ID and KJ_CI_PRIVATE_KEY as GitHub repository secrets"
185
- };
186
- } catch { /* GitHub API error */
187
- return null;
188
- }
189
- }
190
-
191
- async function checkCiInfra(config) {
192
- const checks = [];
193
- const projectDir = config.projectDir || process.cwd();
194
-
195
- checks.push(...await checkCiWorkflows(projectDir));
196
-
197
- const ghCheck = await checkBinary("gh");
198
- checks.push({
199
- name: "ci:gh",
200
- label: "CI: gh CLI",
201
- ok: ghCheck.ok,
202
- detail: ghCheck.ok ? ghCheck.version : "Not found",
203
- fix: ghCheck.ok ? null : "Install GitHub CLI: https://cli.github.com/"
204
70
  });
205
-
206
- const secretsCheck = await checkCiSecrets();
207
- if (secretsCheck) checks.push(secretsCheck);
208
-
209
- return checks;
210
71
  }
211
72
 
212
- async function checkRtk() {
213
- const installCmd = getInstallCommand("rtk");
214
- try {
215
- const res = await runCommand("rtk", ["--version"]);
216
- if (res.exitCode === 0) {
217
- return { name: "rtk", label: "RTK (Rust Token Killer)", ok: true, detail: `${res.stdout.trim()} — token savings active`, fix: null };
218
- }
219
- } catch { /* not installed */ }
220
- return {
221
- name: "rtk",
222
- label: "RTK (Rust Token Killer)",
223
- ok: false,
224
- detail: "Not found — 60-90% token savings available",
225
- fix: withDocLink(`Install: ${installCmd}`, "rtk_install")
226
- };
227
- }
228
-
229
- async function checkRuleFiles(config) {
230
- const projectDir = config.projectDir || process.cwd();
231
- const reviewRules = await loadFirstExisting(resolveRoleMdPath("reviewer", projectDir));
232
- const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
233
- return [
234
- {
235
- name: "review-rules",
236
- label: "Reviewer rules (.md)",
237
- ok: Boolean(reviewRules),
238
- detail: reviewRules ? "Found" : "Not found (will use defaults)",
239
- fix: null
240
- },
241
- {
242
- name: "coder-rules",
243
- label: "Coder rules (.md)",
244
- ok: Boolean(coderRules),
245
- detail: coderRules ? "Found" : "Not found (will use defaults)",
246
- fix: null
247
- }
248
- ];
73
+ /**
74
+ * Run the full doctor pipeline without CLI formatting. Consumers that need
75
+ * the raw report (preflight, status command) should use this.
76
+ *
77
+ * @param {{ config: Object, checkOnly?: boolean, yes?: boolean, onConfirm?: Function, logger?: Object }} args
78
+ * @returns {Promise<import("../checks/types.js").RunReport>}
79
+ */
80
+ export async function runDoctor({ config, checkOnly = false, yes = false, onConfirm, logger }) {
81
+ const checks = buildChecks(config);
82
+ return runCheckPipeline(checks, { config }, {
83
+ mode: checkOnly ? "check-only" : "fix",
84
+ yes,
85
+ onConfirm: onConfirm ?? confirmViaTty,
86
+ logger,
87
+ });
249
88
  }
250
89
 
251
90
  /**
252
- * Detect duplicate TOML table headers (e.g. [mcp_servers."karajan-mcp"] appearing twice).
253
- * Full TOML parsing would require a dependency — this catches the most common config error.
91
+ * Legacy shape (backwards-compat for callers that still expect
92
+ * {name,label,ok,detail,fix}). Kept public because tests and the CI gate
93
+ * still rely on it.
94
+ *
95
+ * @param {{ config: Object }} args
254
96
  */
255
- function findDuplicateTomlKeys(content) {
256
- const tableHeaders = [];
257
- const duplicates = [];
258
- for (const line of content.split("\n")) {
259
- const match = line.match(/^\s*\[([^\]]+)\]\s*$/);
260
- if (match) {
261
- const key = match[1].trim();
262
- if (tableHeaders.includes(key)) {
263
- duplicates.push(key);
264
- } else {
265
- tableHeaders.push(key);
266
- }
267
- }
268
- }
269
- return duplicates;
97
+ export async function runChecks({ config }) {
98
+ const report = await runDoctor({ config });
99
+ return toLegacyShape(report);
270
100
  }
271
101
 
272
- async function checkAgentConfigs() {
273
- const checks = [];
274
- const home = os.homedir();
275
-
276
- // Claude: ~/.claude.json
277
- const claudeJsonPath = path.join(home, ".claude.json");
278
- try {
279
- const raw = await fs.readFile(claudeJsonPath, "utf8");
280
- JSON.parse(raw);
281
- checks.push({ name: "agent-config:claude", label: "Agent config: claude (~/.claude.json)", ok: true, detail: "Valid JSON", fix: null });
282
- } catch (err) {
283
- if (err.code === "ENOENT") {
284
- // File doesn't exist — not an error, Claude may not be configured
285
- } else {
286
- checks.push({
287
- name: "agent-config:claude",
288
- label: "Agent config: claude (~/.claude.json)",
289
- ok: false,
290
- detail: `Invalid JSON: ${err.message.split("\n")[0]}`,
291
- fix: "Fix the JSON syntax in ~/.claude.json. Common issues: trailing commas, missing quotes."
292
- });
293
- }
294
- }
295
-
296
- // Codex: ~/.codex/config.toml
297
- const codexTomlPath = path.join(home, ".codex", "config.toml");
298
- try {
299
- const raw = await fs.readFile(codexTomlPath, "utf8");
300
- const duplicates = findDuplicateTomlKeys(raw);
301
- if (duplicates.length > 0) {
302
- checks.push({
303
- name: "agent-config:codex",
304
- label: "Agent config: codex (~/.codex/config.toml)",
305
- ok: false,
306
- detail: `Duplicate TOML keys: ${duplicates.join(", ")}`,
307
- fix: `Remove duplicate entries in ~/.codex/config.toml: ${duplicates.join(", ")}`
308
- });
309
- } else {
310
- checks.push({ name: "agent-config:codex", label: "Agent config: codex (~/.codex/config.toml)", ok: true, detail: "Valid TOML (no duplicate keys)", fix: null });
311
- }
312
- } catch (err) {
313
- if (err.code !== "ENOENT") {
314
- checks.push({
315
- name: "agent-config:codex",
316
- label: "Agent config: codex (~/.codex/config.toml)",
317
- ok: false,
318
- detail: `Cannot read: ${err.message.split("\n")[0]}`,
319
- fix: "Check file permissions on ~/.codex/config.toml"
320
- });
321
- }
322
- }
102
+ /**
103
+ * `kj doctor` CLI entrypoint. Returns the exit code (0 OK, 1 when FAIL).
104
+ *
105
+ * @param {{ config: Object, checkOnly?: boolean, yes?: boolean, json?: boolean, verbose?: boolean }} args
106
+ * @returns {Promise<number>}
107
+ */
108
+ export async function doctorCommand({ config, checkOnly = false, yes = false, json = false, verbose = false }) {
109
+ const report = await runDoctor({ config, checkOnly, yes });
323
110
 
324
- // KJ config: ~/.karajan/kj.config.yml (validate YAML)
325
- const kjConfigPath = getConfigPath();
326
- try {
327
- const raw = await fs.readFile(kjConfigPath, "utf8");
328
- const yaml = await import("js-yaml");
329
- yaml.default.load(raw);
330
- checks.push({ name: "agent-config:karajan", label: "Agent config: karajan (kj.config.yml)", ok: true, detail: "Valid YAML", fix: null });
331
- } catch (err) {
332
- if (err.code !== "ENOENT") {
333
- checks.push({
334
- name: "agent-config:karajan",
335
- label: "Agent config: karajan (kj.config.yml)",
336
- ok: false,
337
- detail: `Invalid YAML: ${err.message.split("\n")[0]}`,
338
- fix: `Fix YAML syntax in ${kjConfigPath}. Run 'kj init' to regenerate if needed.`
339
- });
340
- }
111
+ if (json) {
112
+ console.log(JSON.stringify(report, null, 2));
113
+ } else {
114
+ printHuman(report, { verbose });
341
115
  }
342
116
 
343
- return checks;
117
+ return hasBlockingFailure(report) ? 1 : 0;
344
118
  }
345
119
 
346
- export async function runChecks({ config }) {
347
- const checks = [];
348
-
349
- checks.push(
350
- checkKarajanVersion(),
351
- await checkConfigFile(),
352
- await checkGitRepo(),
353
- await checkDocker(),
354
- await checkSonarQube(config),
355
- ...await checkAgentCLIs(),
356
- ...await checkCoreBinaries()
357
- );
358
-
359
- if (config.serena?.enabled) {
360
- checks.push(await checkSerena());
361
- }
120
+ function hasBlockingFailure(report) {
121
+ return report.checks.some((c) => c.status === STATUS.FAIL || c.status === STATUS.TIMEOUT);
122
+ }
362
123
 
363
- if (config.ci?.enabled) {
364
- checks.push(...await checkCiInfra(config));
124
+ function statusIcon(status) {
125
+ switch (status) {
126
+ case STATUS.OK: return "OK ";
127
+ case STATUS.FIXED: return "FIXED";
128
+ case STATUS.WARN: return "WARN ";
129
+ case STATUS.FAIL: return "FAIL ";
130
+ case STATUS.SKIPPED: return "SKIP ";
131
+ case STATUS.TIMEOUT: return "TIME ";
132
+ default: return "? ";
365
133
  }
366
-
367
- checks.push(...await checkAgentConfigs());
368
- checks.push(...await checkRuleFiles(config));
369
- checks.push(await checkRtk());
370
-
371
- return checks;
372
134
  }
373
135
 
374
- export async function doctorCommand({ config }) {
375
- const checks = await runChecks({ config });
376
-
377
- for (const check of checks) {
378
- const icon = check.ok ? "OK " : "MISS";
379
- console.log(`${icon} ${check.label}: ${check.detail}`);
380
- if (check.fix) {
136
+ function printHuman(report, { verbose }) {
137
+ for (const check of report.checks) {
138
+ console.log(`${statusIcon(check.status)} ${check.label}: ${check.detail}`);
139
+ if (check.fix && (check.status === STATUS.FAIL || check.status === STATUS.WARN || verbose)) {
381
140
  console.log(` -> ${check.fix}`);
382
141
  }
142
+ if (verbose) {
143
+ console.log(` (${check.runMs}ms, strategy=${check.strategy})`);
144
+ }
383
145
  }
384
146
 
385
- const failures = checks.filter((c) => !c.ok && c.fix);
386
- if (failures.length === 0) {
387
- console.log("\nAll checks passed.");
147
+ const s = report.summary;
148
+ const total = s.ok + s.fixed + s.warn + s.fail + s.skipped + s.timeout;
149
+ console.log();
150
+ if (s.fail === 0 && s.timeout === 0) {
151
+ const label = s.fixed > 0 ? `All checks passed (${s.fixed} auto-fixed)` : "All checks passed.";
152
+ console.log(label);
388
153
  } else {
389
- console.log(`\n${failures.length} issue(s) found.`);
154
+ console.log(
155
+ `${s.fail + s.timeout} issue(s) found, ${s.fixed} auto-fixed, ${s.warn} warnings, ${s.skipped} skipped out of ${total}.`
156
+ );
157
+ }
158
+ if (Object.keys(report.overrides).length > 0) {
159
+ console.log(`Runtime overrides applied: ${JSON.stringify(report.overrides)}`);
390
160
  }
391
-
392
- return checks;
393
161
  }
@@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
2
2
  import readline from "node:readline";
3
3
  import { resumeFlow } from "../orchestrator.js";
4
4
  import { createActivityLog } from "../activity-log.js";
5
- import { printEvent } from "../utils/display.js";
5
+ import { printEvent } from "../utils/display/event-handlers.js";
6
6
 
7
7
  function createCliAskQuestion() {
8
8
  return async (question, context) => {
@@ -3,7 +3,8 @@ import readline from "node:readline";
3
3
  import { runFlow } from "../orchestrator.js";
4
4
  import { assertAgentsAvailable } from "../agents/availability.js";
5
5
  import { createActivityLog } from "../activity-log.js";
6
- import { printHeader, printEvent } from "../utils/display.js";
6
+ import { printHeader } from "../utils/display/header.js";
7
+ import { printEvent } from "../utils/display/event-handlers.js";
7
8
  import { resolveRole } from "../config.js";
8
9
  import { parseCardId } from "../planning-game/adapter.js";
9
10
 
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `kj skills` — manage the local OpenSkills cache.
3
+ *
4
+ * Subcommands:
5
+ * - list : print cached skills with their age and freshness.
6
+ * - clear-cache : delete all cached metadata (forces re-install on next session).
7
+ */
8
+
9
+ import { listCached, clearCache, getCacheRoot, DEFAULT_TTL_MS } from "../skills/skills-cache.js";
10
+
11
+ function humanAge(cachedAt) {
12
+ const ms = Date.now() - Date.parse(cachedAt);
13
+ if (!Number.isFinite(ms) || ms < 0) return "unknown";
14
+ const days = ms / (24 * 60 * 60 * 1000);
15
+ if (days < 1) return `${Math.max(1, Math.round(ms / (60 * 60 * 1000)))}h ago`;
16
+ return `${days.toFixed(1)}d ago`;
17
+ }
18
+
19
+ export async function skillsCommand({ action = "list" } = {}) {
20
+ switch (action) {
21
+ case "list":
22
+ return listAction();
23
+ case "clear-cache":
24
+ return clearAction();
25
+ default:
26
+ console.error(`Unknown action: ${action}. Use: list | clear-cache`);
27
+ return 1;
28
+ }
29
+ }
30
+
31
+ async function listAction() {
32
+ const cache = await listCached();
33
+ if (cache.length === 0) {
34
+ console.log(`No cached skills at ${getCacheRoot()}`);
35
+ return 0;
36
+ }
37
+ console.log(`Cached skills (TTL ${Math.round(DEFAULT_TTL_MS / 86400000)}d) at ${getCacheRoot()}:\n`);
38
+ for (const entry of cache) {
39
+ const flag = entry.fresh ? "FRESH" : "STALE";
40
+ console.log(` ${flag} ${entry.name} (${humanAge(entry.cachedAt)})`);
41
+ }
42
+ return 0;
43
+ }
44
+
45
+ async function clearAction() {
46
+ const { removed } = await clearCache();
47
+ console.log(`Cleared ${removed} cached skill${removed === 1 ? "" : "s"} from ${getCacheRoot()}`);
48
+ return 0;
49
+ }