karajan-code 1.59.1 → 2.0.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 (112) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/scripts/install.js +18 -15
  4. package/src/agents/aider-agent.js +0 -7
  5. package/src/agents/claude-agent.js +1 -4
  6. package/src/agents/codex-agent.js +2 -6
  7. package/src/agents/gemini-agent.js +0 -4
  8. package/src/agents/opencode-agent.js +0 -4
  9. package/src/{becaria → ci}/dispatch.js +13 -13
  10. package/src/{becaria → ci}/pr-diff.js +1 -1
  11. package/src/cli.js +2 -2
  12. package/src/commands/agents.js +0 -1
  13. package/src/commands/doctor.js +22 -40
  14. package/src/commands/init.js +8 -8
  15. package/src/commands/report.js +0 -13
  16. package/src/commands/review.js +17 -17
  17. package/src/commands/run.js +1 -2
  18. package/src/commands/status.js +1 -7
  19. package/src/config.js +10 -52
  20. package/src/git/automation.js +9 -9
  21. package/src/guards/output-guard.js +0 -17
  22. package/src/mcp/handlers/direct-handlers.js +1 -1
  23. package/src/mcp/handlers/run-handler.js +1 -2
  24. package/src/mcp/sovereignty-guard.js +1 -1
  25. package/src/mcp/tools.js +1 -1
  26. package/src/orchestrator/brain-coordinator.js +165 -0
  27. package/src/orchestrator/ci-integration.js +139 -0
  28. package/src/orchestrator/config-init.js +435 -0
  29. package/src/orchestrator/direct-actions.js +186 -0
  30. package/src/orchestrator/feedback-enrichment.js +140 -0
  31. package/src/orchestrator/feedback-queue.js +152 -0
  32. package/src/orchestrator/flow-control.js +159 -0
  33. package/src/orchestrator/post-loop-stages.js +20 -15
  34. package/src/orchestrator/preflight-checks.js +7 -3
  35. package/src/orchestrator/role-output-compressor.js +190 -0
  36. package/src/orchestrator/session-journal.js +344 -0
  37. package/src/orchestrator/solomon-escalation.js +38 -5
  38. package/src/orchestrator/solomon-rules.js +32 -33
  39. package/src/orchestrator/stages/reviewer-stage.js +16 -5
  40. package/src/orchestrator/stages/sonar-stage.js +24 -1
  41. package/src/orchestrator/verification-gate.js +146 -0
  42. package/src/orchestrator.js +428 -664
  43. package/src/planning-game/client.js +2 -2
  44. package/src/planning-game/pipeline-adapter.js +1 -1
  45. package/src/prompts/coder.js +17 -3
  46. package/src/prompts/reviewer.js +2 -2
  47. package/src/prompts/rtk-snippet.js +3 -5
  48. package/src/review/diff-generator.js +66 -17
  49. package/src/review/snapshot-diff.js +122 -0
  50. package/src/review/tdd-policy.js +4 -2
  51. package/src/roles/agent-role.js +135 -0
  52. package/src/roles/architect-role.js +61 -108
  53. package/src/roles/audit-role.js +25 -71
  54. package/src/roles/coder-role.js +32 -57
  55. package/src/roles/discover-role.js +30 -71
  56. package/src/roles/hu-reviewer-role.js +51 -99
  57. package/src/roles/impeccable-role.js +33 -88
  58. package/src/roles/index.js +2 -0
  59. package/src/roles/karajan-brain-role.js +134 -0
  60. package/src/roles/planner-role.js +40 -75
  61. package/src/roles/refactorer-role.js +22 -64
  62. package/src/roles/researcher-role.js +51 -115
  63. package/src/roles/reviewer-role.js +68 -125
  64. package/src/roles/security-role.js +38 -102
  65. package/src/roles/solomon-role.js +69 -11
  66. package/src/roles/tester-role.js +121 -95
  67. package/src/roles/triage-role.js +50 -111
  68. package/src/session-store.js +1 -1
  69. package/src/skills/skill-detector.js +11 -0
  70. package/src/sonar/config-resolver.js +20 -1
  71. package/src/sonar/credentials.js +17 -0
  72. package/src/sonar/project-key.js +1 -1
  73. package/src/sonar/scanner.js +1 -1
  74. package/src/utils/display.js +33 -14
  75. package/src/utils/injection-guard.js +23 -8
  76. package/src/utils/logger.js +6 -1
  77. package/src/utils/process.js +3 -4
  78. package/src/utils/role-assigner.js +131 -0
  79. package/src/utils/update-check.js +5 -1
  80. package/src/webperf/devtools-detect.js +70 -23
  81. package/templates/roles/coder.md +7 -0
  82. package/templates/roles/karajan-brain.md +95 -0
  83. package/templates/roles/solomon.md +67 -106
  84. package/templates/workflows/automerge.yml +4 -4
  85. package/templates/workflows/houston-override.yml +4 -4
  86. package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
  87. package/src/proxy/adapters/anthropic.js +0 -131
  88. package/src/proxy/adapters/gemini.js +0 -106
  89. package/src/proxy/adapters/index.js +0 -51
  90. package/src/proxy/adapters/openai.js +0 -71
  91. package/src/proxy/compression/ai-compressor.js +0 -209
  92. package/src/proxy/compression/dedup-cache.js +0 -100
  93. package/src/proxy/compression/deterministic/bash-build.js +0 -60
  94. package/src/proxy/compression/deterministic/bash-git.js +0 -101
  95. package/src/proxy/compression/deterministic/bash-infra.js +0 -64
  96. package/src/proxy/compression/deterministic/bash-misc.js +0 -53
  97. package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
  98. package/src/proxy/compression/deterministic/bash-test.js +0 -61
  99. package/src/proxy/compression/deterministic/glob.js +0 -26
  100. package/src/proxy/compression/deterministic/grep.js +0 -53
  101. package/src/proxy/compression/deterministic/index.js +0 -58
  102. package/src/proxy/compression/deterministic/read.js +0 -22
  103. package/src/proxy/compression/deterministic/utils.js +0 -33
  104. package/src/proxy/compression/pipeline.js +0 -270
  105. package/src/proxy/middleware/prompt-injector.js +0 -165
  106. package/src/proxy/middleware/response-interceptor.js +0 -223
  107. package/src/proxy/progress-monitor.js +0 -98
  108. package/src/proxy/proxy-lifecycle.js +0 -201
  109. package/src/proxy/proxy-server.js +0 -239
  110. package/src/proxy/security.js +0 -45
  111. /package/src/{becaria → ci}/index.js +0 -0
  112. /package/src/{becaria → ci}/repo.js +0 -0
@@ -150,7 +150,7 @@ async function resolveSonarTokenWithFallback(config, apiHost) {
150
150
  return null;
151
151
  }
152
152
 
153
- export async function ensureSonarProjectProperties(cwd = process.cwd()) {
153
+ async function ensureSonarProjectProperties(cwd = process.cwd()) {
154
154
  const propsPath = path.join(cwd, "sonar-project.properties");
155
155
  try {
156
156
  await fsPromises.access(propsPath);
@@ -99,7 +99,10 @@ export function printHeader({ task, config }) {
99
99
  if (pipeline.researcher?.enabled) activeRoles.push(`Researcher (${config.roles?.researcher?.provider || "?"})`);
100
100
  if (pipeline.tester?.enabled) activeRoles.push("Tester");
101
101
  if (pipeline.security?.enabled) activeRoles.push("Security");
102
- if (pipeline.solomon?.enabled) activeRoles.push(`Solomon (${config.roles?.solomon?.provider || "?"})`);
102
+ if (pipeline.solomon?.enabled) {
103
+ const solomonProvider = config.roles?.solomon?.provider;
104
+ activeRoles.push(solomonProvider ? `Solomon (${solomonProvider})` : "Solomon");
105
+ }
103
106
  if (activeRoles.length > 0) {
104
107
  const separator = ` ${ANSI.dim}|${ANSI.reset} `;
105
108
  console.log(`${ANSI.bold}Pipeline:${ANSI.reset} ${activeRoles.join(separator)}`);
@@ -256,7 +259,14 @@ function printSessionRtkSavings(rtkSavings) {
256
259
  const tokens = rtkSavings.estimatedTokensSaved ?? 0;
257
260
  const ratio = rtkSavings.savedPct ?? 0;
258
261
  const commands = rtkSavings.callCount ?? 0;
259
- console.log(` ${ANSI.dim}\u26a1 RTK: saved ~${tokens} tokens (${ratio}% compression, ${commands} commands)${ANSI.reset}`);
262
+ const original = rtkSavings.originalBytes ?? 0;
263
+ const compressed = rtkSavings.rtkBytes ?? 0;
264
+
265
+ if (tokens === 0 || ratio === 0) {
266
+ console.log(` ${ANSI.dim}⚡ RTK: ${commands} commands wrapped, 0% savings — output was already minimal (${original} bytes in, ${compressed} bytes out)${ANSI.reset}`);
267
+ } else {
268
+ console.log(` ${ANSI.dim}⚡ RTK: saved ~${tokens.toLocaleString()} tokens (${ratio}% compression, ${commands} commands, ${original.toLocaleString()} → ${compressed.toLocaleString()} bytes)${ANSI.reset}`);
269
+ }
260
270
  }
261
271
 
262
272
  function printSessionBudget(budget) {
@@ -267,11 +277,15 @@ function printSessionBudget(budget) {
267
277
  }
268
278
  const estPrefix = budget.includes_estimates ? "~" : "";
269
279
  const estNote = budget.includes_estimates ? " (includes estimates)" : "";
270
- console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${estPrefix}${budget.total_tokens ?? 0}${estNote}${ANSI.reset}`);
280
+ const fmtTokens = (n) => Number(n || 0).toLocaleString("en-US");
281
+ console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${estPrefix}${fmtTokens(budget.total_tokens)}${estNote}${ANSI.reset}`);
271
282
  console.log(` ${ANSI.dim}\ud83d\udcb0 Total cost: ${estPrefix}$${Number(budget.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`);
272
283
  for (const [role, metrics] of Object.entries(budget.breakdown_by_role || {})) {
284
+ const tokens = Number(metrics.total_tokens || 0);
285
+ const cost = Number(metrics.total_cost_usd || 0);
286
+ if (tokens === 0 && cost === 0) continue; // skip roles with no usage
273
287
  console.log(
274
- ` ${ANSI.dim} - ${role}: ${estPrefix}${metrics.total_tokens ?? 0} tokens, ${estPrefix}$${Number(metrics.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`
288
+ ` ${ANSI.dim} - ${role}: ${estPrefix}${fmtTokens(tokens)} tokens, ${estPrefix}$${cost.toFixed(2)}${ANSI.reset}`
275
289
  );
276
290
  }
277
291
  }
@@ -327,6 +341,10 @@ const EVENT_HANDLERS = {
327
341
 
328
342
  "tdd:result": (event, icon) => {
329
343
  const tdd = event.detail || {};
344
+ if (tdd.reason === "no_diff_available") {
345
+ console.log(` \u251c\u2500 ${icon} TDD policy: ${ANSI.yellow}SKIP${ANSI.reset} ${ANSI.dim}(no diff available)${ANSI.reset}`);
346
+ return;
347
+ }
330
348
  const label = tdd.ok ? `${ANSI.green}PASS${ANSI.reset}` : `${ANSI.red}FAIL${ANSI.reset}`;
331
349
  const files = tdd.sourceFiles === undefined ? "" : ` (${tdd.sourceFiles} src, ${tdd.testFiles} test)`;
332
350
  const executor = formatExecutor(event.detail);
@@ -434,14 +452,16 @@ const EVENT_HANDLERS = {
434
452
  const max = Number(d.max_budget_usd);
435
453
  const pct = Number(d.pct_used ?? 0);
436
454
  const warn = Number(d.warn_threshold_pct ?? 80);
437
- const hasEntries = (d.entries?.length ?? 0) > 0 || Object.keys(d.breakdown_by_role || {}).length > 0;
438
- if (hasEntries && totalTokens === 0 && total === 0) {
439
- console.log(` \u251c\u2500 ${icon} Budget: ${ANSI.dim}N/A (provider does not report usage)${ANSI.reset}`);
440
- return;
441
- }
455
+ // Don't show N/A — just skip budget display when there's nothing to report
456
+ if (total === 0 && totalTokens === 0) return;
457
+ const fmtTokens = (n) => n.toLocaleString("en-US");
458
+ const tokenStr = totalTokens > 0 ? ` / ${fmtTokens(totalTokens)} tokens` : "";
459
+ const costStr = `$${total.toFixed(2)}`;
442
460
  const color = budgetColor(max, pct, warn);
443
- if (Number.isFinite(max) && max >= 0) {
444
- console.log(` \u251c\u2500 ${icon} Budget: ${color}$${total.toFixed(2)} / $${max.toFixed(2)} (${pct.toFixed(1)}%)${ANSI.reset}`);
461
+ if (Number.isFinite(max) && max > 0) {
462
+ console.log(` \u251c\u2500 ${icon} Budget: ${color}${costStr}${tokenStr} / $${max.toFixed(2)} (${pct.toFixed(1)}%)${ANSI.reset}`);
463
+ } else {
464
+ console.log(` \u251c\u2500 ${icon} Budget: ${color}${costStr}${tokenStr}${ANSI.reset}`);
445
465
  }
446
466
  },
447
467
 
@@ -568,9 +588,9 @@ const EVENT_HANDLERS = {
568
588
  }
569
589
  },
570
590
 
571
- "becaria:pr-created": (event) => {
591
+ "ci:pr-created": (event) => {
572
592
  const url = event.detail?.prUrl || "";
573
- console.log(` \u251c\u2500 ${ANSI.green}\ud83d\ude80 BecarIA PR created: ${url}${ANSI.reset}`);
593
+ console.log(` \u251c\u2500 ${ANSI.green}\ud83d\ude80 CI PR created: ${url}${ANSI.reset}`);
574
594
  },
575
595
 
576
596
  "solomon:alert": (event) => {
@@ -596,7 +616,6 @@ const QUIET_SUPPRESSED = new Set([
596
616
  "skills:auto-install",
597
617
  "context:loaded",
598
618
  "rtk:detected",
599
- "proxy:started",
600
619
  "board:started",
601
620
  "plan:loaded",
602
621
  "tdd:auto-detect",
@@ -148,14 +148,29 @@ export function scanForInjection(text, opts = {}) {
148
148
  export function scanDiff(diff, opts = {}) {
149
149
  if (!diff) return { clean: true, findings: [], summary: "Empty diff" };
150
150
 
151
- // Extract only added lines (skip diff headers like +++ b/file)
152
- const addedLines = diff
153
- .split("\n")
154
- .filter((l) => l.startsWith("+") && !l.startsWith("+++"))
155
- .map((l) => l.slice(1))
156
- .join("\n");
157
-
158
- return scanForInjection(addedLines, opts);
151
+ // Split diff into per-file chunks to avoid false positives from
152
+ // concatenated added-lines across unrelated files.
153
+ const files = diff.split(/^diff --git /m).filter(Boolean);
154
+
155
+ const allFindings = [];
156
+ for (const fileChunk of files) {
157
+ const addedLines = fileChunk
158
+ .split("\n")
159
+ .filter((l) => l.startsWith("+") && !l.startsWith("+++"))
160
+ .map((l) => l.slice(1))
161
+ .join("\n");
162
+
163
+ if (!addedLines) continue;
164
+ const result = scanForInjection(addedLines, opts);
165
+ if (!result.clean) allFindings.push(...result.findings);
166
+ }
167
+
168
+ const clean = allFindings.length === 0;
169
+ return {
170
+ clean,
171
+ findings: allFindings,
172
+ summary: clean ? "No injection patterns detected" : `${allFindings.length} injection pattern(s) detected across diff`
173
+ };
159
174
  }
160
175
 
161
176
  // --- Helpers ---
@@ -19,7 +19,12 @@ const LEVEL_COLORS = {
19
19
  };
20
20
 
21
21
  function timestamp() {
22
- return new Date().toISOString().slice(11, 23);
22
+ const now = new Date();
23
+ const h = String(now.getHours()).padStart(2, "0");
24
+ const m = String(now.getMinutes()).padStart(2, "0");
25
+ const s = String(now.getSeconds()).padStart(2, "0");
26
+ const ms = String(now.getMilliseconds()).padStart(3, "0");
27
+ return `${h}:${m}:${s}.${ms}`;
23
28
  }
24
29
 
25
30
  function formatContext(ctx) {
@@ -76,10 +76,9 @@ async function awaitWithTimeout(subprocess, timeout, getStdout) {
76
76
 
77
77
  export async function runCommand(command, args = [], options = {}) {
78
78
  const { timeout, onOutput, silenceTimeoutMs, partialOutputFlushMs, ...rest } = options;
79
- // Always detach stdin: KJ subprocesses never need interactive input.
80
- // Without this, a subprocess prompting for input (sonar credentials,
81
- // agent approval, npm prompts) hangs forever since there is no TTY.
82
- if (!rest.stdin) rest.stdin = "ignore";
79
+ // Detach stdin unless the caller provides input (e.g. Codex uses input: prompt).
80
+ // Without this, a subprocess prompting for input hangs forever since there is no TTY.
81
+ if (!rest.stdin && !rest.input) rest.stdin = "ignore";
83
82
  const subprocess = execa(command, args, { reject: false, ...rest });
84
83
 
85
84
  let stdoutAccum = "";
@@ -0,0 +1,131 @@
1
+ // Smart role assignment based on available AI agents.
2
+ // Used by both kj init (interactive) and auto-init (non-interactive).
3
+
4
+ import { detectAvailableAgents } from "./agent-detect.js";
5
+
6
+ // Agent capability tiers (higher = more capable for complex reasoning)
7
+ const CAPABILITY_TIERS = {
8
+ claude: 5, // Best for reasoning, orchestration, complex tasks
9
+ codex: 4, // Strong coder, good reviewer
10
+ gemini: 3, // Good general purpose
11
+ aider: 2, // Good coder, limited reasoning
12
+ opencode: 2 // Similar to aider
13
+ };
14
+
15
+ // Role requirements: what capability level is ideal for each role
16
+ const ROLE_PREFERENCES = {
17
+ brain: { minTier: 4, prefer: "claude", description: "Karajan Brain (orchestrator)" },
18
+ solomon: { minTier: 3, prefer: "gemini", description: "Solomon (judge/arbiter)" },
19
+ coder: { minTier: 2, prefer: "claude", description: "Coder" },
20
+ reviewer: { minTier: 3, prefer: "codex", description: "Reviewer" },
21
+ planner: { minTier: 4, prefer: "claude", description: "Planner" },
22
+ researcher: { minTier: 3, prefer: "claude", description: "Researcher" },
23
+ architect: { minTier: 4, prefer: "claude", description: "Architect" },
24
+ tester: { minTier: 3, prefer: "claude", description: "Tester" },
25
+ security: { minTier: 3, prefer: "claude", description: "Security" },
26
+ triage: { minTier: 3, prefer: "claude", description: "Triage" }
27
+ };
28
+
29
+ /**
30
+ * Assign the best available agent to a role.
31
+ * Prefers the role's preferred agent if available, then falls back by capability tier.
32
+ */
33
+ function assignRole(roleName, availableAgents) {
34
+ const pref = ROLE_PREFERENCES[roleName];
35
+ if (!pref) return availableAgents[0]?.name || "claude";
36
+
37
+ // Preferred agent available?
38
+ if (availableAgents.find(a => a.name === pref.prefer)) {
39
+ return pref.prefer;
40
+ }
41
+
42
+ // Sort by capability tier (descending) and pick the best available
43
+ const sorted = [...availableAgents].sort(
44
+ (a, b) => (CAPABILITY_TIERS[b.name] || 1) - (CAPABILITY_TIERS[a.name] || 1)
45
+ );
46
+
47
+ return sorted[0]?.name || "claude";
48
+ }
49
+
50
+ /**
51
+ * Try to diversify: if possible, use a different agent for reviewer than coder.
52
+ */
53
+ function diversifyReviewer(coderAgent, availableAgents) {
54
+ const alternatives = availableAgents.filter(a => a.name !== coderAgent);
55
+ if (alternatives.length === 0) return coderAgent;
56
+
57
+ const sorted = [...alternatives].sort(
58
+ (a, b) => (CAPABILITY_TIERS[b.name] || 1) - (CAPABILITY_TIERS[a.name] || 1)
59
+ );
60
+ return sorted[0].name;
61
+ }
62
+
63
+ /**
64
+ * Auto-assign all roles based on detected agents. Non-interactive.
65
+ * Returns a config-ready object with role assignments.
66
+ */
67
+ export async function autoAssignRoles(logger) {
68
+ const agents = await detectAvailableAgents();
69
+ const available = agents.filter(a => a.available);
70
+
71
+ if (available.length === 0) {
72
+ logger?.warn?.("No AI agents detected. Using claude as default for all roles.");
73
+ return { assignments: getDefaultAssignments("claude"), agents, available: [] };
74
+ }
75
+
76
+ logger?.info?.(`Detected ${available.length} AI agent(s): ${available.map(a => `${a.name} (${a.version})`).join(", ")}`);
77
+
78
+ const coder = assignRole("coder", available);
79
+ const reviewer = available.length > 1 ? diversifyReviewer(coder, available) : coder;
80
+ const brain = assignRole("brain", available);
81
+ const solomon = available.length > 1 ? diversifyReviewer(brain, available) : brain;
82
+
83
+ const assignments = {
84
+ brain,
85
+ solomon,
86
+ coder,
87
+ reviewer,
88
+ planner: assignRole("planner", available),
89
+ researcher: assignRole("researcher", available),
90
+ architect: assignRole("architect", available),
91
+ tester: assignRole("tester", available),
92
+ security: assignRole("security", available),
93
+ triage: assignRole("triage", available)
94
+ };
95
+
96
+ logger?.info?.(`Role assignments: Brain=${brain}, Solomon=${solomon}, Coder=${coder}, Reviewer=${reviewer}`);
97
+
98
+ return { assignments, agents, available };
99
+ }
100
+
101
+ function getDefaultAssignments(agent) {
102
+ return {
103
+ brain: agent, solomon: agent, coder: agent, reviewer: agent,
104
+ planner: agent, researcher: agent, architect: agent,
105
+ tester: agent, security: agent, triage: agent
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Apply role assignments to a Karajan config object.
111
+ */
112
+ export function applyRoleAssignments(config, assignments) {
113
+ config.coder = assignments.coder;
114
+ config.reviewer = assignments.reviewer;
115
+
116
+ const roleNames = ["coder", "reviewer", "planner", "researcher", "architect",
117
+ "tester", "security", "triage", "solomon"];
118
+
119
+ for (const role of roleNames) {
120
+ config.roles = config.roles || {};
121
+ config.roles[role] = config.roles[role] || {};
122
+ config.roles[role].provider = assignments[role] || assignments.coder;
123
+ }
124
+
125
+ // Brain is a new top-level config (not in roles yet — will be when KarajanBrainRole is implemented)
126
+ config.brain = { provider: assignments.brain };
127
+
128
+ return config;
129
+ }
130
+
131
+ export { CAPABILITY_TIERS, ROLE_PREFERENCES };
@@ -20,7 +20,11 @@ export async function checkForUpdate(currentVersion) {
20
20
  const cache = JSON.parse(raw);
21
21
  if (cache.checkedAt && Date.now() - cache.checkedAt < CACHE_TTL_MS) {
22
22
  if (!cache.latest || cache.latest === currentVersion) return null;
23
- return { updateAvailable: true, latest: cache.latest, current: currentVersion };
23
+ // Only show update if latest is actually NEWER than current
24
+ if (compareVersions(cache.latest, currentVersion) > 0) {
25
+ return { updateAvailable: true, latest: cache.latest, current: currentVersion };
26
+ }
27
+ return null;
24
28
  }
25
29
  } catch { /* no cache or expired */ }
26
30
 
@@ -1,32 +1,81 @@
1
- /**
2
- * Chrome DevTools MCP detection and WebPerf skill auto-installation.
3
- */
1
+ // Chrome DevTools MCP detection and WebPerf skill auto-installation.
4
2
 
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import os from "node:os";
5
6
  import { isOpenSkillsAvailable, installSkill, listSkills } from "../skills/openskills-client.js";
6
7
 
7
- /** Skill names to auto-install when WebPerf is enabled. */
8
+ /** Skill names relevant for WebPerf analysis. Already installed globally via openskills. */
8
9
  export const WEBPERF_SKILLS = [
9
- "webperf",
10
- "webperf-core-web-vitals",
11
- "webperf-loading"
10
+ "optimize",
11
+ "audit",
12
+ "polish"
12
13
  ];
13
14
 
14
15
  /**
15
16
  * Check whether Chrome DevTools MCP is configured and available.
16
- * Since Karajan runs as an MCP server itself, it cannot directly call other MCPs.
17
- * Instead, we check the config flag `webperf.devtools_mcp`.
18
- * @param {object} config — merged Karajan config
17
+ * Checks: (1) config flag, (2) Claude MCP config (~/.claude.json), (3) VS Code MCP settings.
18
+ * @param {object} config - merged Karajan config
19
19
  * @returns {boolean}
20
20
  */
21
21
  export function isDevToolsMcpAvailable(config) {
22
- return Boolean(config?.webperf?.devtools_mcp);
22
+ // Explicit config flag takes priority
23
+ if (config?.webperf?.devtools_mcp === true) return true;
24
+ if (config?.webperf?.devtools_mcp === false) return false;
25
+
26
+ // Check cached detection result
27
+ if (_devToolsDetected !== null) return _devToolsDetected;
28
+
29
+ // Not yet detected - return false (async detection runs at init)
30
+ return false;
31
+ }
32
+
33
+ let _devToolsDetected = null;
34
+
35
+ /**
36
+ * Auto-detect Chrome DevTools MCP from system configuration.
37
+ * Checks ~/.claude.json and VS Code MCP settings.
38
+ * Call once at pipeline init.
39
+ */
40
+ export async function detectDevToolsMcp(logger) {
41
+ // Check Claude MCP config
42
+ const claudeConfigPath = path.join(os.homedir(), ".claude.json");
43
+ try {
44
+ const raw = await fs.readFile(claudeConfigPath, "utf8");
45
+ const parsed = JSON.parse(raw);
46
+ const mcpServers = parsed?.mcpServers || parsed?.mcp_servers || {};
47
+ if (mcpServers["chrome-devtools"] || mcpServers["chrome_devtools"]) {
48
+ _devToolsDetected = true;
49
+ logger?.info?.("Chrome DevTools MCP detected in ~/.claude.json");
50
+ return true;
51
+ }
52
+ } catch { /* file not found or parse error */ }
53
+
54
+ // Check VS Code MCP settings
55
+ const vscodePaths = [
56
+ path.join(os.homedir(), ".vscode", "settings.json"),
57
+ path.join(process.cwd(), ".vscode", "settings.json")
58
+ ];
59
+ for (const vscPath of vscodePaths) {
60
+ try {
61
+ const raw = await fs.readFile(vscPath, "utf8");
62
+ if (raw.includes("chrome-devtools") || raw.includes("chrome_devtools")) {
63
+ _devToolsDetected = true;
64
+ logger?.info?.(`Chrome DevTools MCP detected in ${vscPath}`);
65
+ return true;
66
+ }
67
+ } catch { /* not found */ }
68
+ }
69
+
70
+ _devToolsDetected = false;
71
+ return false;
23
72
  }
24
73
 
25
74
  /**
26
75
  * Ensure WebPerf skills are installed via OpenSkills.
27
76
  * Installs any missing skills from WEBPERF_SKILLS.
28
- * @param {string} projectDir — absolute path to the project root
29
- * @param {object} [logger] — optional logger ({ info, warn })
77
+ * @param {string} projectDir - absolute path to the project root
78
+ * @param {object} [logger] - optional logger ({ info, warn })
30
79
  * @returns {Promise<{ installed: string[], alreadyInstalled: string[], skipped: string[] }>}
31
80
  */
32
81
  export async function ensureWebPerfSkills(projectDir, logger) {
@@ -34,12 +83,12 @@ export async function ensureWebPerfSkills(projectDir, logger) {
34
83
 
35
84
  const available = await isOpenSkillsAvailable();
36
85
  if (!available) {
37
- logger?.warn?.("OpenSkills CLI not available — skipping WebPerf skill installation");
86
+ logger?.debug?.("OpenSkills CLI not available - WebPerf skills unavailable");
38
87
  result.skipped = [...WEBPERF_SKILLS];
39
88
  return result;
40
89
  }
41
90
 
42
- // Get currently installed skills
91
+ // List skills (includes global installs)
43
92
  const listResult = await listSkills({ projectDir });
44
93
  const installedNames = new Set(
45
94
  (listResult.ok ? listResult.skills : []).map(s => s.name)
@@ -48,18 +97,16 @@ export async function ensureWebPerfSkills(projectDir, logger) {
48
97
  for (const skill of WEBPERF_SKILLS) {
49
98
  if (installedNames.has(skill)) {
50
99
  result.alreadyInstalled.push(skill);
51
- continue;
52
- }
53
-
54
- const installResult = await installSkill(skill, { projectDir });
55
- if (installResult.ok) {
56
- result.installed.push(skill);
57
- logger?.info?.(`Installed WebPerf skill: ${skill}`);
58
100
  } else {
101
+ // Skills should be installed globally via `openskills install owner/repo`
102
+ // Don't attempt auto-install with bare names — just note they're missing
59
103
  result.skipped.push(skill);
60
- logger?.warn?.(`Failed to install WebPerf skill "${skill}": ${installResult.error}`);
61
104
  }
62
105
  }
63
106
 
107
+ if (result.alreadyInstalled.length > 0) {
108
+ logger?.info?.(`WebPerf skills available: ${result.alreadyInstalled.join(", ")}`);
109
+ }
110
+
64
111
  return result;
65
112
  }
@@ -45,6 +45,13 @@ Before reporting done, verify that ALL parts of the task are addressed:
45
45
  - If unintended changes are detected, revert immediately with `git checkout -- <file>`.
46
46
  - Pay special attention to CSS, HTML, and config files where full rewrites destroy prior work.
47
47
 
48
+ ## Code Quality Rules
49
+
50
+ - Follow SOLID principles. Write small, focused functions (< 30 lines).
51
+ - Make atomic commits: 1 logical change = 1 commit. Keep PRs small and reviewable.
52
+ - Security: use httpOnly cookies for auth tokens, validate all input, parameterize queries, never expose secrets.
53
+ - No console.log in production code -- use a structured logger. No 'any' types -- use JSDoc annotations.
54
+
48
55
  ## Output format
49
56
 
50
57
  Return a JSON object:
@@ -0,0 +1,95 @@
1
+ # Karajan Brain — Central Pipeline Orchestrator
2
+
3
+ You are the central intelligence of the Karajan pipeline. Every role output passes through you. You decide what happens next.
4
+
5
+ ## Your responsibilities
6
+
7
+ 1. **Route** — decide which role runs next based on current state
8
+ 2. **Enrich** — transform vague feedback into concrete, actionable instructions
9
+ 3. **Verify** — check that each role actually did its job before moving on
10
+ 4. **Act directly** — execute commands when a full role invocation isn't needed
11
+ 5. **Consult Solomon** — only when facing a genuine dilemma
12
+
13
+ ## Skills
14
+
15
+ ### 1. route-decision
16
+ Decide the next role based on pipeline state.
17
+ - If coder just finished successfully → reviewer
18
+ - If reviewer approved → next quality gate (tester/security/impeccable) or done
19
+ - If reviewer rejected with issues → coder (with enriched feedback)
20
+ - If tester/security failed → coder (with specific fix instructions)
21
+ - If max iterations reached and still failing → consult Solomon
22
+ - If approved and all gates pass → done
23
+
24
+ ### 2. prompt-enrichment
25
+ When feedback is vague, make it actionable.
26
+ - Identify concrete file paths from the project structure
27
+ - Add line numbers when possible
28
+ - Suggest specific code changes
29
+ - Break complex feedback into numbered steps
30
+ - Include test commands to run
31
+
32
+ Example transformation:
33
+ - Vague: "auth test suite missing refresh token coverage"
34
+ - Enriched: "In packages/server/tests/auth/, create refresh-token.test.js. Test cases: (1) POST /auth/refresh with valid token returns new pair, (2) expired token returns 401, (3) invalid token returns 401. Reference packages/server/src/auth/jwt.js for the refresh logic."
35
+
36
+ ### 3. output-verification
37
+ Check that coder produced real changes before advancing.
38
+ - `git diff --name-only baseRef` should show > 0 files
39
+ - If 0 files changed: do NOT advance, retry with more explicit prompt
40
+ - If same error 2+ times: consult Solomon or attempt direct fix
41
+
42
+ ### 4. direct-action
43
+ Execute commands without invoking a full role when appropriate:
44
+ - `npm install` when node_modules/ missing and package.json exists
45
+ - `.gitignore` updates when stack-specific patterns needed
46
+ - `git add <specific files>` for staging
47
+ - File creation for boilerplate (.env.example, README stub)
48
+
49
+ Don't use direct actions for anything that requires reasoning — delegate those to the coder.
50
+
51
+ ### 5. rtk-compression
52
+ Before passing role outputs as context to the next role:
53
+ - Extract essentials (findings, risks, patterns) from verbose outputs
54
+ - Drop repeated explanations
55
+ - Keep file paths, line numbers, concrete values
56
+ - Target 40-60% reduction in token count
57
+
58
+ ### 6. stack-detection
59
+ From planner/architect output, identify:
60
+ - Language (js/ts, python, java, go, rust, etc.)
61
+ - Framework (express, django, spring, gin, etc.)
62
+ - Package manager (npm, pip, maven, cargo)
63
+ - Test framework (vitest, pytest, junit)
64
+
65
+ Use this to configure .gitignore, install commands, test commands.
66
+
67
+ ### 7. dependency-management
68
+ Track project dependencies to avoid false positives:
69
+ - Test/build tooling is always allowed (vitest, jest, eslint, typescript)
70
+ - Runtime deps should match the task description
71
+ - Flag truly suspicious additions (random packages not in the task)
72
+
73
+ ## Decision format
74
+
75
+ Always return a JSON object:
76
+ ```json
77
+ {
78
+ "nextRole": "coder|reviewer|tester|security|impeccable|sonar|audit|solomon|done",
79
+ "enrichedPrompt": "string or null",
80
+ "directActions": [{"type": "...", "params": {}}],
81
+ "reasoning": "why this decision",
82
+ "consultSolomon": false,
83
+ "dilemma": null
84
+ }
85
+ ```
86
+
87
+ ## When to consult Solomon
88
+
89
+ Set `consultSolomon: true` and describe the `dilemma` ONLY for:
90
+ - Security vs deadline tradeoffs
91
+ - Two quality gates giving contradictory feedback
92
+ - Stalled loops where you've tried 3+ approaches with no progress
93
+ - Unclear risk evaluation (is this blocking or cosmetic?)
94
+
95
+ For everything else, decide yourself. Solomon is the lawyer you consult on tough calls — not the CEO.