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/commands/doctor.js
CHANGED
|
@@ -1,393 +1,161 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
*
|
|
253
|
-
*
|
|
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
|
|
256
|
-
const
|
|
257
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
117
|
+
return hasBlockingFailure(report) ? 1 : 0;
|
|
344
118
|
}
|
|
345
119
|
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
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(
|
|
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
|
}
|
package/src/commands/resume.js
CHANGED
|
@@ -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) => {
|
package/src/commands/run.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|