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.
Files changed (69) hide show
  1. package/package.json +1 -1
  2. package/src/activity-log.js +13 -13
  3. package/src/agents/availability.js +2 -3
  4. package/src/agents/claude-agent.js +42 -21
  5. package/src/agents/model-registry.js +1 -1
  6. package/src/becaria/dispatch.js +1 -1
  7. package/src/becaria/repo.js +3 -3
  8. package/src/cli.js +6 -2
  9. package/src/commands/doctor.js +154 -108
  10. package/src/commands/init.js +101 -90
  11. package/src/commands/plan.js +1 -1
  12. package/src/commands/report.js +77 -71
  13. package/src/commands/roles.js +0 -1
  14. package/src/commands/run.js +2 -3
  15. package/src/config.js +157 -89
  16. package/src/git/automation.js +3 -4
  17. package/src/guards/policy-resolver.js +3 -3
  18. package/src/mcp/orphan-guard.js +1 -2
  19. package/src/mcp/progress.js +4 -3
  20. package/src/mcp/run-kj.js +2 -0
  21. package/src/mcp/server-handlers.js +294 -241
  22. package/src/mcp/server.js +4 -3
  23. package/src/mcp/tools.js +19 -0
  24. package/src/orchestrator/agent-fallback.js +1 -3
  25. package/src/orchestrator/iteration-stages.js +206 -170
  26. package/src/orchestrator/pre-loop-stages.js +266 -34
  27. package/src/orchestrator/solomon-rules.js +2 -2
  28. package/src/orchestrator.js +820 -739
  29. package/src/planning-game/adapter.js +23 -20
  30. package/src/planning-game/architect-adrs.js +45 -0
  31. package/src/planning-game/client.js +15 -1
  32. package/src/planning-game/decomposition.js +7 -5
  33. package/src/prompts/architect.js +88 -0
  34. package/src/prompts/discover.js +228 -0
  35. package/src/prompts/planner.js +53 -33
  36. package/src/prompts/triage.js +8 -16
  37. package/src/review/parser.js +18 -19
  38. package/src/review/profiles.js +2 -2
  39. package/src/review/schema.js +3 -3
  40. package/src/review/scope-filter.js +3 -4
  41. package/src/roles/architect-role.js +122 -0
  42. package/src/roles/commiter-role.js +2 -2
  43. package/src/roles/discover-role.js +122 -0
  44. package/src/roles/index.js +2 -0
  45. package/src/roles/planner-role.js +54 -38
  46. package/src/roles/refactorer-role.js +8 -7
  47. package/src/roles/researcher-role.js +6 -7
  48. package/src/roles/reviewer-role.js +4 -5
  49. package/src/roles/security-role.js +3 -4
  50. package/src/roles/solomon-role.js +6 -18
  51. package/src/roles/sonar-role.js +5 -1
  52. package/src/roles/tester-role.js +8 -5
  53. package/src/roles/triage-role.js +2 -2
  54. package/src/session-cleanup.js +29 -24
  55. package/src/session-store.js +1 -1
  56. package/src/sonar/api.js +1 -1
  57. package/src/sonar/manager.js +1 -1
  58. package/src/sonar/project-key.js +5 -5
  59. package/src/sonar/scanner.js +34 -65
  60. package/src/utils/display.js +312 -272
  61. package/src/utils/git.js +3 -3
  62. package/src/utils/logger.js +6 -1
  63. package/src/utils/model-selector.js +5 -5
  64. package/src/utils/process.js +80 -102
  65. package/src/utils/rate-limit-detector.js +13 -13
  66. package/src/utils/run-log.js +55 -52
  67. package/templates/roles/architect.md +62 -0
  68. package/templates/roles/discover.md +167 -0
  69. package/templates/roles/planner.md +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -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 || !meta.bin) continue;
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
- try {
15
- const obj = JSON.parse(lines[i]);
16
- if (obj.type === "result" && obj.result) {
17
- return typeof obj.result === "string" ? obj.result : JSON.stringify(obj.result);
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
- try {
29
- const obj = JSON.parse(line);
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 !== 0 ? raw : "", exitCode: 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 !== 0 ? raw : "", exitCode: 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 !== 0 ? raw : "", exitCode: 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 || !entry.deprecated) return false;
24
+ if (!entry?.deprecated) return false;
25
25
  return new Date(entry.deprecated) <= new Date();
26
26
  }
27
27
 
@@ -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 !== false ? `[${agent}] ` : "";
69
+ const prefix = becariaConfig?.comment_prefix === false ? "" : `[${agent}] `;
70
70
  const eventType = becariaConfig?.comment_event || "becaria-comment";
71
71
 
72
72
  await sendDispatch(repo, {
@@ -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 = url.match(/github\.com[^:]*:([^/]+\/[^/]+?)(?:\.git)?$/);
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 = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
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
- program.parseAsync().catch((error) => {
247
+ try {
248
+ await program.parseAsync();
249
+ } catch (error) {
246
250
  console.error(error.message);
247
251
  process.exit(1);
248
- });
252
+ }
@@ -14,56 +14,63 @@ function getPackageVersion() {
14
14
  return JSON.parse(readFileSync(pkgPath, "utf8")).version;
15
15
  }
16
16
 
17
- export async function runChecks({ config }) {
18
- const checks = [];
19
-
20
- // 0. Karajan version
17
+ function checkKarajanVersion() {
21
18
  const version = getPackageVersion();
22
- checks.push({
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
- // 1. Config file
28
+ async function checkConfigFile() {
31
29
  const configPath = getConfigPath();
32
30
  const configExists = await exists(configPath);
33
- checks.push({
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
- // 2. Git repository
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
- checks.push({
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
- // 3. Docker
56
+ async function checkDocker() {
57
57
  const docker = await checkBinary("docker", "--version");
58
- checks.push({
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
- // 4. SonarQube reachability
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
- checks.push({
83
+ const isOkOrDisabled = sonarOk || config.sonarqube?.enabled === false;
84
+ return {
77
85
  name: "sonarqube",
78
86
  label: "SonarQube",
79
- ok: sonarOk || config.sonarqube?.enabled === false,
80
- detail: config.sonarqube?.enabled === false
81
- ? "Disabled in config"
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
- // 5. Agent CLIs
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
- // 6. Core binaries
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
- // 7. Serena MCP
115
- if (config.serena?.enabled) {
116
- let serenaOk = false;
117
- try {
118
- const serenaCheck = await runCommand("serena", ["--version"]);
119
- serenaOk = serenaCheck.exitCode === 0;
120
- } catch {
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
- // 8. BecarIA Gateway infrastructure
133
- if (config.becaria?.enabled) {
134
- const projectDir = config.projectDir || process.cwd();
135
-
136
- // Workflow files
137
- const workflowDir = path.join(projectDir, ".github", "workflows");
138
- const requiredWorkflows = ["becaria-gateway.yml", "automerge.yml", "houston-override.yml"];
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: "becaria:gh",
155
- label: "BecarIA: gh CLI",
156
- ok: ghCheck.ok,
157
- detail: ghCheck.ok ? ghCheck.version : "Not found",
158
- fix: ghCheck.ok ? null : "Install GitHub CLI: https://cli.github.com/"
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
- // Secrets check via gh api (best effort — only works if user has admin access)
162
- let secretsOk = false;
163
- try {
164
- const { detectRepo } = await import("../becaria/repo.js");
165
- const repo = await detectRepo();
166
- if (repo) {
167
- const secretsRes = await runCommand("gh", ["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
168
- if (secretsRes.exitCode === 0) {
169
- const names = secretsRes.stdout.split("\n").map((s) => s.trim());
170
- const hasAppId = names.includes("BECARIA_APP_ID");
171
- const hasKey = names.includes("BECARIA_APP_PRIVATE_KEY");
172
- secretsOk = hasAppId && hasKey;
173
- checks.push({
174
- name: "becaria:secrets",
175
- label: "BecarIA: GitHub secrets",
176
- ok: secretsOk,
177
- detail: secretsOk ? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found" : `Missing: ${!hasAppId ? "BECARIA_APP_ID " : ""}${!hasKey ? "BECARIA_APP_PRIVATE_KEY" : ""}`.trim(),
178
- fix: secretsOk ? null : "Add BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY as GitHub repository secrets"
179
- });
180
- }
181
- }
182
- } catch {
183
- // Skip secrets check if we can't access the API
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
- // 9. Review rules / Coder rules
186
+ async function checkBecariaInfra(config) {
187
+ const checks = [];
188
188
  const projectDir = config.projectDir || process.cwd();
189
- const reviewRules = await loadFirstExisting(resolveRoleMdPath("reviewer", projectDir));
190
- const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
191
- checks.push({
192
- name: "review-rules",
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: "coder-rules",
200
- label: "Coder rules (.md)",
201
- ok: Boolean(coderRules),
202
- detail: coderRules ? "Found" : "Not found (will use defaults)",
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