karajan-code 1.48.0 → 1.50.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 (55) hide show
  1. package/package.json +1 -1
  2. package/src/agents/resolve-bin.js +1 -1
  3. package/src/audit/basal-cost.js +26 -23
  4. package/src/bootstrap.js +2 -2
  5. package/src/commands/agents.js +1 -1
  6. package/src/commands/board.js +3 -3
  7. package/src/commands/code.js +1 -1
  8. package/src/commands/doctor.js +4 -4
  9. package/src/commands/init.js +2 -2
  10. package/src/commands/report.js +1 -1
  11. package/src/commands/review.js +1 -1
  12. package/src/commands/roles.js +1 -1
  13. package/src/hu/store.js +9 -5
  14. package/src/mcp/handlers/direct-handlers.js +393 -0
  15. package/src/mcp/handlers/hu-handlers.js +93 -0
  16. package/src/mcp/handlers/management-handlers.js +182 -0
  17. package/src/mcp/handlers/run-handler.js +283 -0
  18. package/src/mcp/orphan-guard.js +1 -1
  19. package/src/mcp/server-handlers.js +41 -871
  20. package/src/mcp/suggest-handler.js +2 -2
  21. package/src/orchestrator/hu-sub-pipeline.js +1 -1
  22. package/src/orchestrator/iteration-stages.js +9 -890
  23. package/src/orchestrator/pre-loop-stages.js +8 -906
  24. package/src/orchestrator/preflight-checks.js +13 -19
  25. package/src/orchestrator/stages/architect-stage.js +176 -0
  26. package/src/orchestrator/stages/coder-stage.js +307 -0
  27. package/src/orchestrator/stages/hu-reviewer-stage.js +467 -0
  28. package/src/orchestrator/stages/planner-stage.js +90 -0
  29. package/src/orchestrator/stages/research-stage.js +74 -0
  30. package/src/orchestrator/stages/reviewer-stage.js +350 -0
  31. package/src/orchestrator/stages/sonar-stage.js +267 -0
  32. package/src/orchestrator/stages/triage-stage.js +137 -0
  33. package/src/plugins/loader.js +1 -1
  34. package/src/prompts/architect.js +1 -1
  35. package/src/review/parser.js +3 -3
  36. package/src/roles/architect-role.js +1 -1
  37. package/src/roles/audit-role.js +3 -3
  38. package/src/roles/base-role.js +1 -1
  39. package/src/roles/discover-role.js +1 -1
  40. package/src/roles/hu-reviewer-role.js +1 -1
  41. package/src/roles/researcher-role.js +1 -1
  42. package/src/roles/triage-role.js +1 -1
  43. package/src/session-cleanup.js +1 -1
  44. package/src/session-store.js +2 -2
  45. package/src/skills/openskills-client.js +1 -1
  46. package/src/skills/skill-detector.js +2 -2
  47. package/src/sonar/api.js +4 -3
  48. package/src/sonar/config-resolver.js +69 -0
  49. package/src/sonar/credentials.js +1 -1
  50. package/src/sonar/project-key.js +1 -1
  51. package/src/sonar/scanner.js +13 -21
  52. package/src/utils/fs.js +1 -1
  53. package/src/utils/json-extract.js +1 -1
  54. package/src/utils/project-detect.js +1 -1
  55. package/src/utils/rtk-detect.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.48.0",
3
+ "version": "1.50.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",
@@ -26,7 +26,7 @@ function getNvmDirs() {
26
26
  const versionsDir = path.join(nvmDir, "versions", "node");
27
27
  try {
28
28
  return readdirSync(versionsDir).map(v => path.join(versionsDir, v, "bin"));
29
- } catch {
29
+ } catch { /* nvm versions dir may not exist */
30
30
  return [];
31
31
  }
32
32
  }
@@ -1,8 +1,11 @@
1
- import fs from "node:fs";
1
+ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { execFile, execFileSync } from "node:child_process";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
4
5
  import { getKarajanHome } from "../utils/paths.js";
5
6
 
7
+ const execFileAsync = promisify(execFile);
8
+
6
9
  const SOURCE_EXTENSIONS = new Set([
7
10
  ".js", ".mjs", ".cjs", ".ts", ".mts", ".cts", ".tsx", ".jsx",
8
11
  ".py", ".rb", ".go", ".rs", ".java", ".kt", ".swift", ".c", ".cpp", ".h",
@@ -18,11 +21,11 @@ function slugify(projectDir) {
18
21
  return path.basename(projectDir).replace(/[^a-zA-Z0-9_-]/g, "_");
19
22
  }
20
23
 
21
- function walkSourceFiles(dir) {
24
+ async function walkSourceFiles(dir) {
22
25
  // Use git ls-files for speed (instant vs recursive walk)
23
26
  try {
24
- const output = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], { cwd: dir, encoding: "utf8", timeout: 5000 });
25
- return output.trim().split("\n")
27
+ const { stdout } = await execFileAsync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], { cwd: dir, encoding: "utf8", timeout: 5000 });
28
+ return stdout.trim().split("\n")
26
29
  .filter(f => f && SOURCE_EXTENSIONS.has(path.extname(f)) && !EXCLUDE_DIRS.has(f.split("/")[0]))
27
30
  .map(f => path.join(dir, f));
28
31
  } catch {
@@ -30,15 +33,15 @@ function walkSourceFiles(dir) {
30
33
  const files = [];
31
34
  let entries;
32
35
  try {
33
- entries = fs.readdirSync(dir, { withFileTypes: true });
34
- } catch {
36
+ entries = await fs.readdir(dir, { withFileTypes: true });
37
+ } catch { /* directory not readable */
35
38
  return files;
36
39
  }
37
40
  for (const entry of entries) {
38
41
  if (EXCLUDE_DIRS.has(entry.name)) continue;
39
42
  const full = path.join(dir, entry.name);
40
43
  if (entry.isDirectory()) {
41
- files.push(...walkSourceFiles(full));
44
+ files.push(...await walkSourceFiles(full));
42
45
  } else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
43
46
  files.push(full);
44
47
  }
@@ -47,19 +50,19 @@ function walkSourceFiles(dir) {
47
50
  }
48
51
  }
49
52
 
50
- function countLines(filePath) {
53
+ async function countLines(filePath) {
51
54
  try {
52
- const content = fs.readFileSync(filePath, "utf8");
55
+ const content = await fs.readFile(filePath, "utf8");
53
56
  return content.split("\n").length;
54
57
  } catch {
55
58
  return 0;
56
59
  }
57
60
  }
58
61
 
59
- function countDependencies(projectDir) {
62
+ async function countDependencies(projectDir) {
60
63
  const pkgPath = path.join(projectDir, "package.json");
61
64
  try {
62
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
65
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
63
66
  const deps = Object.keys(pkg.dependencies || {});
64
67
  const devDeps = Object.keys(pkg.devDependencies || {});
65
68
  return { dependencies: deps.length, devDependencies: devDeps.length, total: deps.length + devDeps.length };
@@ -89,7 +92,7 @@ function runDepcheck(projectDir) {
89
92
  });
90
93
  }
91
94
 
92
- function findDeadExports(sourceFiles) {
95
+ async function findDeadExports(sourceFiles) {
93
96
  const exportMap = new Map(); // file -> [exportName]
94
97
  const importedNames = new Set();
95
98
 
@@ -101,7 +104,7 @@ function findDeadExports(sourceFiles) {
101
104
  for (const file of sourceFiles) {
102
105
  let content;
103
106
  try {
104
- content = fs.readFileSync(file, "utf8");
107
+ content = await fs.readFile(file, "utf8");
105
108
  } catch {
106
109
  continue;
107
110
  }
@@ -139,15 +142,15 @@ function findDeadExports(sourceFiles) {
139
142
  }
140
143
 
141
144
  export async function measureBasalCost(projectDir) {
142
- const sourceFiles = walkSourceFiles(projectDir);
145
+ const sourceFiles = await walkSourceFiles(projectDir);
143
146
  let totalLines = 0;
144
147
  for (const f of sourceFiles) {
145
- totalLines += countLines(f);
148
+ totalLines += await countLines(f);
146
149
  }
147
150
 
148
- const depInfo = countDependencies(projectDir);
151
+ const depInfo = await countDependencies(projectDir);
149
152
  const depcheck = await runDepcheck(projectDir);
150
- const deadExports = findDeadExports(sourceFiles);
153
+ const deadExports = await findDeadExports(sourceFiles);
151
154
 
152
155
  return {
153
156
  totalLines,
@@ -162,20 +165,20 @@ function auditDir() {
162
165
  return path.join(getKarajanHome(), "audits");
163
166
  }
164
167
 
165
- export function loadPreviousAudit(projectDir) {
168
+ export async function loadPreviousAudit(projectDir) {
166
169
  const file = path.join(auditDir(), slugify(projectDir), "latest.json");
167
170
  try {
168
- return JSON.parse(fs.readFileSync(file, "utf8"));
171
+ return JSON.parse(await fs.readFile(file, "utf8"));
169
172
  } catch {
170
173
  return null;
171
174
  }
172
175
  }
173
176
 
174
- export function saveAuditSnapshot(projectDir, metrics) {
177
+ export async function saveAuditSnapshot(projectDir, metrics) {
175
178
  const dir = path.join(auditDir(), slugify(projectDir));
176
- fs.mkdirSync(dir, { recursive: true });
179
+ await fs.mkdir(dir, { recursive: true });
177
180
  const snapshot = { ...metrics, timestamp: new Date().toISOString() };
178
- fs.writeFileSync(path.join(dir, "latest.json"), JSON.stringify(snapshot, null, 2));
181
+ await fs.writeFile(path.join(dir, "latest.json"), JSON.stringify(snapshot, null, 2));
179
182
  return snapshot;
180
183
  }
181
184
 
package/src/bootstrap.js CHANGED
@@ -38,7 +38,7 @@ async function checkGitRepo() {
38
38
  let ok = false;
39
39
  try {
40
40
  ok = await ensureGitRepo();
41
- } catch {
41
+ } catch { /* git may not be installed */
42
42
  ok = false;
43
43
  }
44
44
  return {
@@ -156,7 +156,7 @@ async function readBootstrapFile(projectDir) {
156
156
  try {
157
157
  const raw = await fs.readFile(bootstrapPath(projectDir), "utf8");
158
158
  return JSON.parse(raw);
159
- } catch {
159
+ } catch { /* bootstrap file may not exist or contain invalid JSON */
160
160
  return null;
161
161
  }
162
162
  }
@@ -51,7 +51,7 @@ export async function setAgent(role, provider, { global: isGlobal = false } = {}
51
51
  const { setSessionOverride } = await import("../mcp/preflight.js");
52
52
  setSessionOverride(role, provider);
53
53
  return { role, provider, scope: "session" };
54
- } catch {
54
+ } catch { /* preflight module not available in CLI mode */
55
55
  // preflight module not available (CLI mode) — write to project config
56
56
  const projectConfigPath = getProjectConfigPath();
57
57
  const projectConfig = (await loadProjectConfig()) || {};
@@ -10,7 +10,7 @@ function readPid() {
10
10
  try {
11
11
  const pid = Number.parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
12
12
  return Number.isFinite(pid) ? pid : null;
13
- } catch {
13
+ } catch { /* PID file does not exist */
14
14
  return null;
15
15
  }
16
16
  }
@@ -19,7 +19,7 @@ function isProcessAlive(pid) {
19
19
  try {
20
20
  process.kill(pid, 0);
21
21
  return true;
22
- } catch {
22
+ } catch { /* process does not exist */
23
23
  return false;
24
24
  }
25
25
  }
@@ -55,7 +55,7 @@ export async function stopBoard() {
55
55
 
56
56
  try {
57
57
  process.kill(pid, "SIGTERM");
58
- } catch {
58
+ } catch { /* process already dead */
59
59
  // already dead
60
60
  }
61
61
  try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
@@ -13,7 +13,7 @@ export async function codeCommand({ task, config, logger }) {
13
13
  if (config.coder_rules) {
14
14
  try {
15
15
  coderRules = await fs.readFile(config.coder_rules, "utf8");
16
- } catch {
16
+ } catch { /* configured coder_rules path not found — try fallback */
17
17
  try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
18
18
  }
19
19
  }
@@ -44,7 +44,7 @@ async function checkGitRepo() {
44
44
  let gitOk = false;
45
45
  try {
46
46
  gitOk = await ensureGitRepo();
47
- } catch {
47
+ } catch { /* git check failed */
48
48
  gitOk = false;
49
49
  }
50
50
  return {
@@ -79,7 +79,7 @@ async function checkSonarQube(config) {
79
79
  if (config.sonarqube?.enabled !== false) {
80
80
  try {
81
81
  sonarOk = await isSonarReachable(sonarHost);
82
- } catch {
82
+ } catch { /* sonar check failed */
83
83
  sonarOk = false;
84
84
  }
85
85
  }
@@ -130,7 +130,7 @@ async function checkSerena() {
130
130
  try {
131
131
  const serenaCheck = await runCommand("serena", ["--version"]);
132
132
  serenaOk = serenaCheck.exitCode === 0;
133
- } catch {
133
+ } catch { /* serena not installed */
134
134
  serenaOk = false;
135
135
  }
136
136
  return {
@@ -181,7 +181,7 @@ async function checkBecariaSecrets() {
181
181
  detail: secretsOk ? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found" : `Missing: ${missing}`,
182
182
  fix: secretsOk ? null : "Add BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY as GitHub repository secrets"
183
183
  };
184
- } catch {
184
+ } catch { /* GitHub API error */
185
185
  return null;
186
186
  }
187
187
  }
@@ -163,7 +163,7 @@ async function ensureCoderRules(coderRulesPath, logger) {
163
163
  let content;
164
164
  try {
165
165
  content = await fs.readFile(templatePath, "utf8");
166
- } catch {
166
+ } catch { /* template not found, use inline default */
167
167
  content = [
168
168
  "# Coder Rules",
169
169
  "",
@@ -333,7 +333,7 @@ export async function initCommand({ logger, flags = {} }) {
333
333
  try {
334
334
  const rtkRes = await runCommand("rtk", ["--version"]);
335
335
  hasRtk = rtkRes.exitCode === 0;
336
- } catch {
336
+ } catch { /* rtk not installed */
337
337
  hasRtk = false;
338
338
  }
339
339
  if (!hasRtk) {
@@ -312,7 +312,7 @@ async function findSessionsByPgTask(dir, pgTask) {
312
312
  if (session.pg_task_id === pgTask) {
313
313
  matches.push(entry);
314
314
  }
315
- } catch {
315
+ } catch { /* skip malformed session files */
316
316
  // skip malformed sessions
317
317
  }
318
318
  }
@@ -53,7 +53,7 @@ export async function reviewCommand({ task, config, logger, baseRef }) {
53
53
  let review;
54
54
  try {
55
55
  review = JSON.parse(result.output);
56
- } catch {
56
+ } catch { /* output is not JSON */
57
57
  review = { approved: true, summary: result.output };
58
58
  }
59
59
 
@@ -60,7 +60,7 @@ export async function showRole(roleName, config) {
60
60
  try {
61
61
  await fs.readFile(candidates[1], "utf8");
62
62
  source = "user";
63
- } catch {
63
+ } catch { /* user role file not found */
64
64
  source = "built-in";
65
65
  }
66
66
  }
package/src/hu/store.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { execSync } from "node:child_process";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+
6
+ const execFileAsync = promisify(execFile);
4
7
  import { getKarajanHome } from "../utils/paths.js";
5
8
 
6
9
  // FUTURE: hu-storage adapter for PG/Trello/etc — currently local files only
@@ -237,13 +240,14 @@ export function addSplittingMetadata(batch, huId, metadata) {
237
240
  /**
238
241
  * Detect project info from the given directory.
239
242
  * @param {string} cwd - Directory to detect project from.
240
- * @returns {{ name: string, remoteUrl: string|null }}
243
+ * @returns {Promise<{ name: string, remoteUrl: string|null }>}
241
244
  */
242
- export function detectProject(cwd) {
245
+ export async function detectProject(cwd) {
243
246
  const name = path.basename(cwd);
244
247
  let remoteUrl = null;
245
248
  try {
246
- remoteUrl = execSync("git remote get-url origin", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
249
+ const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
250
+ remoteUrl = stdout.trim();
247
251
  } catch { /* not a git repo or no remote */ }
248
252
  return { name, remoteUrl };
249
253
  }
@@ -276,7 +280,7 @@ async function loadProjectBatch(projectDir) {
276
280
  try {
277
281
  const raw = await fs.readFile(filePath, "utf8");
278
282
  return JSON.parse(raw);
279
- } catch {
283
+ } catch { /* no saved batch for this project */
280
284
  return {
281
285
  project: projectSlug(projectDir),
282
286
  created_at: new Date().toISOString(),