karajan-code 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/activity-log.js +13 -13
- package/src/agents/availability.js +2 -3
- package/src/agents/claude-agent.js +42 -21
- package/src/agents/model-registry.js +1 -1
- package/src/becaria/dispatch.js +1 -1
- package/src/becaria/repo.js +3 -3
- package/src/cli.js +6 -2
- package/src/commands/doctor.js +154 -108
- package/src/commands/init.js +101 -90
- package/src/commands/plan.js +1 -1
- package/src/commands/report.js +77 -71
- package/src/commands/roles.js +0 -1
- package/src/commands/run.js +2 -3
- package/src/config.js +157 -89
- package/src/git/automation.js +3 -4
- package/src/guards/policy-resolver.js +3 -3
- package/src/mcp/orphan-guard.js +1 -2
- package/src/mcp/progress.js +4 -3
- package/src/mcp/run-kj.js +2 -0
- package/src/mcp/server-handlers.js +294 -241
- package/src/mcp/server.js +4 -3
- package/src/mcp/tools.js +19 -0
- package/src/orchestrator/agent-fallback.js +1 -3
- package/src/orchestrator/iteration-stages.js +206 -170
- package/src/orchestrator/pre-loop-stages.js +266 -34
- package/src/orchestrator/solomon-rules.js +2 -2
- package/src/orchestrator.js +820 -739
- package/src/planning-game/adapter.js +23 -20
- package/src/planning-game/architect-adrs.js +45 -0
- package/src/planning-game/client.js +15 -1
- package/src/planning-game/decomposition.js +7 -5
- package/src/prompts/architect.js +88 -0
- package/src/prompts/discover.js +228 -0
- package/src/prompts/planner.js +53 -33
- package/src/prompts/triage.js +8 -16
- package/src/review/parser.js +18 -19
- package/src/review/profiles.js +2 -2
- package/src/review/schema.js +3 -3
- package/src/review/scope-filter.js +3 -4
- package/src/roles/architect-role.js +122 -0
- package/src/roles/commiter-role.js +2 -2
- package/src/roles/discover-role.js +122 -0
- package/src/roles/index.js +2 -0
- package/src/roles/planner-role.js +54 -38
- package/src/roles/refactorer-role.js +8 -7
- package/src/roles/researcher-role.js +6 -7
- package/src/roles/reviewer-role.js +4 -5
- package/src/roles/security-role.js +3 -4
- package/src/roles/solomon-role.js +6 -18
- package/src/roles/sonar-role.js +5 -1
- package/src/roles/tester-role.js +8 -5
- package/src/roles/triage-role.js +2 -2
- package/src/session-cleanup.js +29 -24
- package/src/session-store.js +1 -1
- package/src/sonar/api.js +1 -1
- package/src/sonar/manager.js +1 -1
- package/src/sonar/project-key.js +5 -5
- package/src/sonar/scanner.js +34 -65
- package/src/utils/display.js +312 -272
- package/src/utils/git.js +3 -3
- package/src/utils/logger.js +6 -1
- package/src/utils/model-selector.js +5 -5
- package/src/utils/process.js +80 -102
- package/src/utils/rate-limit-detector.js +13 -13
- package/src/utils/run-log.js +55 -52
- package/templates/roles/architect.md +62 -0
- package/templates/roles/discover.md +167 -0
- package/templates/roles/planner.md +1 -0
package/package.json
CHANGED
package/src/activity-log.js
CHANGED
|
@@ -3,24 +3,24 @@ import path from "node:path";
|
|
|
3
3
|
import { getSessionRoot } from "./utils/paths.js";
|
|
4
4
|
import { ensureDir } from "./utils/fs.js";
|
|
5
5
|
|
|
6
|
+
function formatLine(entry) {
|
|
7
|
+
const ts = entry.timestamp || new Date().toISOString();
|
|
8
|
+
const lvl = (entry.level || "info").toUpperCase().padEnd(5);
|
|
9
|
+
const ctx = entry.context
|
|
10
|
+
? Object.entries(entry.context)
|
|
11
|
+
.filter(([, v]) => v !== undefined)
|
|
12
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
13
|
+
.join(" ")
|
|
14
|
+
: "";
|
|
15
|
+
const ctxStr = ctx ? ` ${ctx}` : "";
|
|
16
|
+
return `${ts} [${lvl}]${ctxStr} ${entry.message || ""}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
export function createActivityLog(sessionId) {
|
|
7
20
|
const logPath = path.join(getSessionRoot(), sessionId, "activity.log");
|
|
8
21
|
let buffer = [];
|
|
9
22
|
let flushing = false;
|
|
10
23
|
|
|
11
|
-
function formatLine(entry) {
|
|
12
|
-
const ts = entry.timestamp || new Date().toISOString();
|
|
13
|
-
const lvl = (entry.level || "info").toUpperCase().padEnd(5);
|
|
14
|
-
const ctx = entry.context
|
|
15
|
-
? Object.entries(entry.context)
|
|
16
|
-
.filter(([, v]) => v !== undefined)
|
|
17
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
18
|
-
.join(" ")
|
|
19
|
-
: "";
|
|
20
|
-
const ctxStr = ctx ? ` ${ctx}` : "";
|
|
21
|
-
return `${ts} [${lvl}]${ctxStr} ${entry.message || ""}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
24
|
async function flush() {
|
|
25
25
|
if (flushing || buffer.length === 0) return;
|
|
26
26
|
flushing = true;
|
|
@@ -8,7 +8,7 @@ export async function assertAgentsAvailable(agentNames = []) {
|
|
|
8
8
|
|
|
9
9
|
for (const name of unique) {
|
|
10
10
|
const meta = getAgentMeta(name);
|
|
11
|
-
if (!meta
|
|
11
|
+
if (!meta?.bin) continue;
|
|
12
12
|
const res = await runCommand(resolveBin(meta.bin), ["--version"]);
|
|
13
13
|
if (res.exitCode !== 0) {
|
|
14
14
|
missing.push({ name, ...meta });
|
|
@@ -19,8 +19,7 @@ export async function assertAgentsAvailable(agentNames = []) {
|
|
|
19
19
|
|
|
20
20
|
const lines = ["Missing required AI CLIs for this command:"];
|
|
21
21
|
for (const m of missing) {
|
|
22
|
-
lines.push(`- ${m.name}: command '${m.bin}' not found`);
|
|
23
|
-
lines.push(` Install: ${m.installUrl}`);
|
|
22
|
+
lines.push(`- ${m.name}: command '${m.bin}' not found`, ` Install: ${m.installUrl}`);
|
|
24
23
|
}
|
|
25
24
|
throw new Error(lines.join("\n"));
|
|
26
25
|
}
|
|
@@ -2,6 +2,39 @@ import { BaseAgent } from "./base-agent.js";
|
|
|
2
2
|
import { runCommand } from "../utils/process.js";
|
|
3
3
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Safely parse a JSON line, returning null on failure.
|
|
7
|
+
*/
|
|
8
|
+
function tryParseJson(line) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(line);
|
|
11
|
+
} catch { return null; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Try to extract a result string from a parsed JSON object.
|
|
16
|
+
* Returns the result string or null if the object is not a result message.
|
|
17
|
+
*/
|
|
18
|
+
function extractResultText(obj) {
|
|
19
|
+
if (obj.type === "result" && obj.result) {
|
|
20
|
+
return typeof obj.result === "string" ? obj.result : JSON.stringify(obj.result);
|
|
21
|
+
}
|
|
22
|
+
if (obj.result && typeof obj.result === "string") {
|
|
23
|
+
return obj.result;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Collect text parts from an assistant message's content blocks.
|
|
30
|
+
*/
|
|
31
|
+
function collectAssistantText(obj) {
|
|
32
|
+
if (obj.type !== "assistant" || !obj.message?.content) return [];
|
|
33
|
+
return obj.message.content
|
|
34
|
+
.filter(block => block.type === "text" && block.text)
|
|
35
|
+
.map(block => block.text);
|
|
36
|
+
}
|
|
37
|
+
|
|
5
38
|
/**
|
|
6
39
|
* Extract the final text result from stream-json NDJSON output.
|
|
7
40
|
* Each line is a JSON object. We collect assistant text content from
|
|
@@ -11,28 +44,16 @@ function extractTextFromStreamJson(raw) {
|
|
|
11
44
|
const lines = (raw || "").split("\n").filter(Boolean);
|
|
12
45
|
// Try to find a "result" message with the final text
|
|
13
46
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
// Claude Code stream-json final message
|
|
20
|
-
if (obj.result && typeof obj.result === "string") {
|
|
21
|
-
return obj.result;
|
|
22
|
-
}
|
|
23
|
-
} catch { /* skip unparseable lines */ }
|
|
47
|
+
const obj = tryParseJson(lines[i]);
|
|
48
|
+
if (!obj) continue;
|
|
49
|
+
const result = extractResultText(obj);
|
|
50
|
+
if (result) return result;
|
|
24
51
|
}
|
|
25
52
|
// Fallback: accumulate all assistant text deltas
|
|
26
53
|
const parts = [];
|
|
27
54
|
for (const line of lines) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (obj.type === "assistant" && obj.message?.content) {
|
|
31
|
-
for (const block of obj.message.content) {
|
|
32
|
-
if (block.type === "text" && block.text) parts.push(block.text);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} catch { /* skip */ }
|
|
55
|
+
const obj = tryParseJson(line);
|
|
56
|
+
if (obj) parts.push(...collectAssistantText(obj));
|
|
36
57
|
}
|
|
37
58
|
return parts.join("") || raw;
|
|
38
59
|
}
|
|
@@ -129,7 +150,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
129
150
|
}));
|
|
130
151
|
const raw = pickOutput(res);
|
|
131
152
|
const output = extractTextFromStreamJson(raw);
|
|
132
|
-
return { ok: res.exitCode === 0, output, error: res.exitCode
|
|
153
|
+
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode };
|
|
133
154
|
}
|
|
134
155
|
|
|
135
156
|
// Without streaming, use json output to get structured response via stderr
|
|
@@ -137,7 +158,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
137
158
|
const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts());
|
|
138
159
|
const raw = pickOutput(res);
|
|
139
160
|
const output = extractTextFromStreamJson(raw);
|
|
140
|
-
return { ok: res.exitCode === 0, output, error: res.exitCode
|
|
161
|
+
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode };
|
|
141
162
|
}
|
|
142
163
|
|
|
143
164
|
async reviewTask(task) {
|
|
@@ -150,6 +171,6 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
150
171
|
timeout: task.timeoutMs
|
|
151
172
|
}));
|
|
152
173
|
const raw = pickOutput(res);
|
|
153
|
-
return { ok: res.exitCode === 0, output: raw, error: res.exitCode
|
|
174
|
+
return { ok: res.exitCode === 0, output: raw, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode };
|
|
154
175
|
}
|
|
155
176
|
}
|
|
@@ -21,7 +21,7 @@ export function getModelPricing(name) {
|
|
|
21
21
|
|
|
22
22
|
export function isModelDeprecated(name) {
|
|
23
23
|
const entry = modelRegistry.get(name);
|
|
24
|
-
if (!entry
|
|
24
|
+
if (!entry?.deprecated) return false;
|
|
25
25
|
return new Date(entry.deprecated) <= new Date();
|
|
26
26
|
}
|
|
27
27
|
|
package/src/becaria/dispatch.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function dispatchComment({ repo, prNumber, agent, body, becariaConf
|
|
|
66
66
|
validateAgent(agent);
|
|
67
67
|
if (!body) throw new Error("body is required (comment text)");
|
|
68
68
|
|
|
69
|
-
const prefix = becariaConfig?.comment_prefix
|
|
69
|
+
const prefix = becariaConfig?.comment_prefix === false ? "" : `[${agent}] `;
|
|
70
70
|
const eventType = becariaConfig?.comment_event || "becaria-comment";
|
|
71
71
|
|
|
72
72
|
await sendDispatch(repo, {
|
package/src/becaria/repo.js
CHANGED
|
@@ -19,11 +19,11 @@ export async function detectRepo() {
|
|
|
19
19
|
|
|
20
20
|
const url = res.stdout.trim();
|
|
21
21
|
// SSH: git@github.com:owner/repo.git or git@github.com-alias:owner/repo.git
|
|
22
|
-
const sshMatch =
|
|
22
|
+
const sshMatch = /github\.com[^:]*:([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
|
|
23
23
|
if (sshMatch) return sshMatch[1];
|
|
24
24
|
|
|
25
25
|
// HTTPS: https://github.com/owner/repo.git
|
|
26
|
-
const httpsMatch =
|
|
26
|
+
const httpsMatch = /github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
|
|
27
27
|
if (httpsMatch) return httpsMatch[1];
|
|
28
28
|
|
|
29
29
|
return null;
|
|
@@ -40,6 +40,6 @@ export async function detectPrNumber(branch) {
|
|
|
40
40
|
const res = await runCommand("gh", args);
|
|
41
41
|
if (res.exitCode !== 0) return null;
|
|
42
42
|
|
|
43
|
-
const num = parseInt(res.stdout.trim(), 10);
|
|
43
|
+
const num = Number.parseInt(res.stdout.trim(), 10);
|
|
44
44
|
return Number.isFinite(num) ? num : null;
|
|
45
45
|
}
|
package/src/cli.js
CHANGED
|
@@ -72,6 +72,8 @@ program
|
|
|
72
72
|
.option("--enable-tester")
|
|
73
73
|
.option("--enable-security")
|
|
74
74
|
.option("--enable-triage")
|
|
75
|
+
.option("--enable-discover")
|
|
76
|
+
.option("--enable-architect")
|
|
75
77
|
.option("--enable-serena")
|
|
76
78
|
.option("--mode <name>")
|
|
77
79
|
.option("--max-iterations <n>")
|
|
@@ -242,7 +244,9 @@ sonar
|
|
|
242
244
|
console.log(`Opened ${result.url}`);
|
|
243
245
|
});
|
|
244
246
|
|
|
245
|
-
|
|
247
|
+
try {
|
|
248
|
+
await program.parseAsync();
|
|
249
|
+
} catch (error) {
|
|
246
250
|
console.error(error.message);
|
|
247
251
|
process.exit(1);
|
|
248
|
-
}
|
|
252
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -14,56 +14,63 @@ function getPackageVersion() {
|
|
|
14
14
|
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
const checks = [];
|
|
19
|
-
|
|
20
|
-
// 0. Karajan version
|
|
17
|
+
function checkKarajanVersion() {
|
|
21
18
|
const version = getPackageVersion();
|
|
22
|
-
|
|
19
|
+
return {
|
|
23
20
|
name: "karajan",
|
|
24
21
|
label: "Karajan Code",
|
|
25
22
|
ok: true,
|
|
26
23
|
detail: `v${version}`,
|
|
27
24
|
fix: null
|
|
28
|
-
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
async function checkConfigFile() {
|
|
31
29
|
const configPath = getConfigPath();
|
|
32
30
|
const configExists = await exists(configPath);
|
|
33
|
-
|
|
31
|
+
return {
|
|
34
32
|
name: "config",
|
|
35
33
|
label: "Config file",
|
|
36
34
|
ok: configExists,
|
|
37
35
|
detail: configExists ? configPath : "Not found",
|
|
38
36
|
fix: configExists ? null : "Run 'kj init' to create the config file."
|
|
39
|
-
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
async function checkGitRepo() {
|
|
42
41
|
let gitOk = false;
|
|
43
42
|
try {
|
|
44
43
|
gitOk = await ensureGitRepo();
|
|
45
44
|
} catch {
|
|
46
45
|
gitOk = false;
|
|
47
46
|
}
|
|
48
|
-
|
|
47
|
+
return {
|
|
49
48
|
name: "git",
|
|
50
49
|
label: "Git repository",
|
|
51
50
|
ok: gitOk,
|
|
52
51
|
detail: gitOk ? "Inside a git repository" : "Not a git repository",
|
|
53
52
|
fix: gitOk ? null : "Run 'git init' or navigate to a git-managed project."
|
|
54
|
-
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
async function checkDocker() {
|
|
57
57
|
const docker = await checkBinary("docker", "--version");
|
|
58
|
-
|
|
58
|
+
return {
|
|
59
59
|
name: "docker",
|
|
60
60
|
label: "Docker",
|
|
61
61
|
ok: docker.ok,
|
|
62
62
|
detail: docker.ok ? docker.version : "Not found",
|
|
63
63
|
fix: docker.ok ? null : "Install Docker: https://docs.docker.com/get-docker/"
|
|
64
|
-
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
function sonarDetail(config, sonarOk, sonarHost) {
|
|
68
|
+
if (config.sonarqube?.enabled === false) return "Disabled in config";
|
|
69
|
+
if (sonarOk) return `Reachable at ${sonarHost}`;
|
|
70
|
+
return `Not reachable at ${sonarHost}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function checkSonarQube(config) {
|
|
67
74
|
const sonarHost = config.sonarqube?.host || "http://localhost:9000";
|
|
68
75
|
let sonarOk = false;
|
|
69
76
|
if (config.sonarqube?.enabled !== false) {
|
|
@@ -73,21 +80,20 @@ export async function runChecks({ config }) {
|
|
|
73
80
|
sonarOk = false;
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
|
-
|
|
83
|
+
const isOkOrDisabled = sonarOk || config.sonarqube?.enabled === false;
|
|
84
|
+
return {
|
|
77
85
|
name: "sonarqube",
|
|
78
86
|
label: "SonarQube",
|
|
79
|
-
ok:
|
|
80
|
-
detail: config
|
|
81
|
-
|
|
82
|
-
: sonarOk
|
|
83
|
-
? `Reachable at ${sonarHost}`
|
|
84
|
-
: `Not reachable at ${sonarHost}`,
|
|
85
|
-
fix: sonarOk || config.sonarqube?.enabled === false
|
|
87
|
+
ok: isOkOrDisabled,
|
|
88
|
+
detail: sonarDetail(config, sonarOk, sonarHost),
|
|
89
|
+
fix: isOkOrDisabled
|
|
86
90
|
? null
|
|
87
91
|
: "Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip."
|
|
88
|
-
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
89
94
|
|
|
90
|
-
|
|
95
|
+
async function checkAgentCLIs() {
|
|
96
|
+
const checks = [];
|
|
91
97
|
for (const agent of KNOWN_AGENTS) {
|
|
92
98
|
const result = await checkBinary(agent.name);
|
|
93
99
|
checks.push({
|
|
@@ -98,8 +104,11 @@ export async function runChecks({ config }) {
|
|
|
98
104
|
fix: result.ok ? null : `Install: ${agent.install}`
|
|
99
105
|
});
|
|
100
106
|
}
|
|
107
|
+
return checks;
|
|
108
|
+
}
|
|
101
109
|
|
|
102
|
-
|
|
110
|
+
async function checkCoreBinaries() {
|
|
111
|
+
const checks = [];
|
|
103
112
|
for (const bin of ["node", "npm", "git"]) {
|
|
104
113
|
const result = await checkBinary(bin);
|
|
105
114
|
checks.push({
|
|
@@ -110,99 +119,136 @@ export async function runChecks({ config }) {
|
|
|
110
119
|
fix: result.ok ? null : `Install ${bin} from its official website.`
|
|
111
120
|
});
|
|
112
121
|
}
|
|
122
|
+
return checks;
|
|
123
|
+
}
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
serenaOk = false;
|
|
122
|
-
}
|
|
123
|
-
checks.push({
|
|
124
|
-
name: "serena",
|
|
125
|
-
label: "Serena MCP",
|
|
126
|
-
ok: serenaOk,
|
|
127
|
-
detail: serenaOk ? "Available" : "Not found (prompts will still include Serena instructions)",
|
|
128
|
-
fix: serenaOk ? null : "Install Serena: uvx --from git+https://github.com/oraios/serena serena --help"
|
|
129
|
-
});
|
|
125
|
+
async function checkSerena() {
|
|
126
|
+
let serenaOk = false;
|
|
127
|
+
try {
|
|
128
|
+
const serenaCheck = await runCommand("serena", ["--version"]);
|
|
129
|
+
serenaOk = serenaCheck.exitCode === 0;
|
|
130
|
+
} catch {
|
|
131
|
+
serenaOk = false;
|
|
130
132
|
}
|
|
133
|
+
return {
|
|
134
|
+
name: "serena",
|
|
135
|
+
label: "Serena MCP",
|
|
136
|
+
ok: serenaOk,
|
|
137
|
+
detail: serenaOk ? "Available" : "Not found (prompts will still include Serena instructions)",
|
|
138
|
+
fix: serenaOk ? null : "Install Serena: uvx --from git+https://github.com/oraios/serena serena --help"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
for (const wf of requiredWorkflows) {
|
|
140
|
-
const wfPath = path.join(workflowDir, wf);
|
|
141
|
-
const wfExists = await exists(wfPath);
|
|
142
|
-
checks.push({
|
|
143
|
-
name: `becaria:workflow:${wf}`,
|
|
144
|
-
label: `BecarIA workflow: ${wf}`,
|
|
145
|
-
ok: wfExists,
|
|
146
|
-
detail: wfExists ? "Found" : "Not found",
|
|
147
|
-
fix: wfExists ? null : `Run 'kj init --scaffold-becaria' or copy from karajan-code/templates/workflows/${wf}`
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// gh CLI
|
|
152
|
-
const ghCheck = await checkBinary("gh");
|
|
142
|
+
async function checkBecariaWorkflows(projectDir) {
|
|
143
|
+
const checks = [];
|
|
144
|
+
const workflowDir = path.join(projectDir, ".github", "workflows");
|
|
145
|
+
const requiredWorkflows = ["becaria-gateway.yml", "automerge.yml", "houston-override.yml"];
|
|
146
|
+
for (const wf of requiredWorkflows) {
|
|
147
|
+
const wfPath = path.join(workflowDir, wf);
|
|
148
|
+
const wfExists = await exists(wfPath);
|
|
153
149
|
checks.push({
|
|
154
|
-
name:
|
|
155
|
-
label:
|
|
156
|
-
ok:
|
|
157
|
-
detail:
|
|
158
|
-
fix:
|
|
150
|
+
name: `becaria:workflow:${wf}`,
|
|
151
|
+
label: `BecarIA workflow: ${wf}`,
|
|
152
|
+
ok: wfExists,
|
|
153
|
+
detail: wfExists ? "Found" : "Not found",
|
|
154
|
+
fix: wfExists ? null : `Run 'kj init --scaffold-becaria' or copy from karajan-code/templates/workflows/${wf}`
|
|
159
155
|
});
|
|
156
|
+
}
|
|
157
|
+
return checks;
|
|
158
|
+
}
|
|
160
159
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
160
|
+
async function checkBecariaSecrets() {
|
|
161
|
+
try {
|
|
162
|
+
const { detectRepo } = await import("../becaria/repo.js");
|
|
163
|
+
const repo = await detectRepo();
|
|
164
|
+
if (!repo) return null;
|
|
165
|
+
const secretsRes = await runCommand("gh", ["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
|
|
166
|
+
if (secretsRes.exitCode !== 0) return null;
|
|
167
|
+
const names = new Set(secretsRes.stdout.split("\n").map((s) => s.trim()));
|
|
168
|
+
const hasAppId = names.has("BECARIA_APP_ID");
|
|
169
|
+
const hasKey = names.has("BECARIA_APP_PRIVATE_KEY");
|
|
170
|
+
const secretsOk = hasAppId && hasKey;
|
|
171
|
+
return {
|
|
172
|
+
name: "becaria:secrets",
|
|
173
|
+
label: "BecarIA: GitHub secrets",
|
|
174
|
+
ok: secretsOk,
|
|
175
|
+
detail: secretsOk
|
|
176
|
+
? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found"
|
|
177
|
+
: `Missing: ${[!hasAppId && "BECARIA_APP_ID", !hasKey && "BECARIA_APP_PRIVATE_KEY"].filter(Boolean).join(" ")}`,
|
|
178
|
+
fix: secretsOk ? null : "Add BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY as GitHub repository secrets"
|
|
179
|
+
};
|
|
180
|
+
} catch {
|
|
181
|
+
// Skip secrets check if we can't access the API
|
|
182
|
+
return null;
|
|
185
183
|
}
|
|
184
|
+
}
|
|
186
185
|
|
|
187
|
-
|
|
186
|
+
async function checkBecariaInfra(config) {
|
|
187
|
+
const checks = [];
|
|
188
188
|
const projectDir = config.projectDir || process.cwd();
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
label: "Reviewer rules (.md)",
|
|
194
|
-
ok: Boolean(reviewRules),
|
|
195
|
-
detail: reviewRules ? "Found" : "Not found (will use defaults)",
|
|
196
|
-
fix: null
|
|
197
|
-
});
|
|
189
|
+
|
|
190
|
+
checks.push(...await checkBecariaWorkflows(projectDir));
|
|
191
|
+
|
|
192
|
+
const ghCheck = await checkBinary("gh");
|
|
198
193
|
checks.push({
|
|
199
|
-
name: "
|
|
200
|
-
label: "
|
|
201
|
-
ok:
|
|
202
|
-
detail:
|
|
203
|
-
fix: null
|
|
194
|
+
name: "becaria:gh",
|
|
195
|
+
label: "BecarIA: gh CLI",
|
|
196
|
+
ok: ghCheck.ok,
|
|
197
|
+
detail: ghCheck.ok ? ghCheck.version : "Not found",
|
|
198
|
+
fix: ghCheck.ok ? null : "Install GitHub CLI: https://cli.github.com/"
|
|
204
199
|
});
|
|
205
200
|
|
|
201
|
+
const secretsCheck = await checkBecariaSecrets();
|
|
202
|
+
if (secretsCheck) checks.push(secretsCheck);
|
|
203
|
+
|
|
204
|
+
return checks;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function checkRuleFiles(config) {
|
|
208
|
+
const projectDir = config.projectDir || process.cwd();
|
|
209
|
+
const reviewRules = await loadFirstExisting(resolveRoleMdPath("reviewer", projectDir));
|
|
210
|
+
const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
name: "review-rules",
|
|
214
|
+
label: "Reviewer rules (.md)",
|
|
215
|
+
ok: Boolean(reviewRules),
|
|
216
|
+
detail: reviewRules ? "Found" : "Not found (will use defaults)",
|
|
217
|
+
fix: null
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "coder-rules",
|
|
221
|
+
label: "Coder rules (.md)",
|
|
222
|
+
ok: Boolean(coderRules),
|
|
223
|
+
detail: coderRules ? "Found" : "Not found (will use defaults)",
|
|
224
|
+
fix: null
|
|
225
|
+
}
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function runChecks({ config }) {
|
|
230
|
+
const checks = [];
|
|
231
|
+
|
|
232
|
+
checks.push(
|
|
233
|
+
checkKarajanVersion(),
|
|
234
|
+
await checkConfigFile(),
|
|
235
|
+
await checkGitRepo(),
|
|
236
|
+
await checkDocker(),
|
|
237
|
+
await checkSonarQube(config),
|
|
238
|
+
...await checkAgentCLIs(),
|
|
239
|
+
...await checkCoreBinaries()
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (config.serena?.enabled) {
|
|
243
|
+
checks.push(await checkSerena());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (config.becaria?.enabled) {
|
|
247
|
+
checks.push(...await checkBecariaInfra(config));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
checks.push(...await checkRuleFiles(config));
|
|
251
|
+
|
|
206
252
|
return checks;
|
|
207
253
|
}
|
|
208
254
|
|