karajan-code 1.59.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/scripts/install.js +18 -15
- package/src/agents/aider-agent.js +0 -7
- package/src/agents/claude-agent.js +1 -4
- package/src/agents/codex-agent.js +2 -6
- package/src/agents/gemini-agent.js +0 -4
- package/src/agents/opencode-agent.js +0 -4
- package/src/{becaria → ci}/dispatch.js +13 -13
- package/src/{becaria → ci}/pr-diff.js +1 -1
- package/src/cli.js +2 -2
- package/src/commands/agents.js +0 -1
- package/src/commands/board.js +8 -1
- package/src/commands/doctor.js +22 -40
- package/src/commands/init.js +8 -8
- package/src/commands/report.js +0 -13
- package/src/commands/review.js +17 -17
- package/src/commands/run.js +1 -2
- package/src/commands/status.js +1 -7
- package/src/config.js +17 -52
- package/src/git/automation.js +9 -9
- package/src/guards/output-guard.js +0 -17
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +1 -2
- package/src/mcp/sovereignty-guard.js +1 -1
- package/src/mcp/tools.js +1 -1
- package/src/orchestrator/brain-coordinator.js +165 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +435 -0
- package/src/orchestrator/direct-actions.js +186 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +152 -0
- package/src/orchestrator/flow-control.js +159 -0
- package/src/orchestrator/pipeline-context.js +3 -0
- package/src/orchestrator/post-loop-stages.js +20 -15
- package/src/orchestrator/preflight-checks.js +7 -3
- package/src/orchestrator/role-output-compressor.js +190 -0
- package/src/orchestrator/session-journal.js +344 -0
- package/src/orchestrator/solomon-escalation.js +38 -5
- package/src/orchestrator/solomon-rules.js +32 -33
- package/src/orchestrator/stages/coder-stage.js +49 -4
- package/src/orchestrator/stages/reviewer-stage.js +52 -9
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +481 -672
- package/src/planning-game/client.js +2 -2
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/coder.js +17 -3
- package/src/prompts/reviewer.js +2 -2
- package/src/prompts/rtk-snippet.js +3 -5
- package/src/review/diff-generator.js +66 -17
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/tdd-policy.js +4 -2
- package/src/roles/agent-role.js +135 -0
- package/src/roles/architect-role.js +61 -108
- package/src/roles/audit-role.js +25 -71
- package/src/roles/coder-role.js +32 -57
- package/src/roles/discover-role.js +30 -71
- package/src/roles/hu-reviewer-role.js +51 -99
- package/src/roles/impeccable-role.js +33 -88
- package/src/roles/index.js +2 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/planner-role.js +40 -75
- package/src/roles/refactorer-role.js +22 -64
- package/src/roles/researcher-role.js +51 -115
- package/src/roles/reviewer-role.js +68 -125
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +69 -11
- package/src/roles/tester-role.js +121 -95
- package/src/roles/triage-role.js +50 -111
- package/src/session-store.js +1 -1
- package/src/skills/skill-detector.js +11 -0
- package/src/sonar/config-resolver.js +20 -1
- package/src/sonar/credentials.js +17 -0
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +1 -1
- package/src/utils/display.js +33 -14
- package/src/utils/injection-guard.js +23 -8
- package/src/utils/logger.js +6 -1
- package/src/utils/process.js +3 -4
- package/src/utils/role-assigner.js +131 -0
- package/src/utils/update-check.js +5 -1
- package/src/webperf/devtools-detect.js +70 -23
- package/templates/roles/coder.md +7 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/solomon.md +67 -106
- package/templates/workflows/automerge.yml +4 -4
- package/templates/workflows/houston-override.yml +4 -4
- package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
- package/src/proxy/adapters/anthropic.js +0 -131
- package/src/proxy/adapters/gemini.js +0 -106
- package/src/proxy/adapters/index.js +0 -51
- package/src/proxy/adapters/openai.js +0 -71
- package/src/proxy/compression/ai-compressor.js +0 -209
- package/src/proxy/compression/dedup-cache.js +0 -100
- package/src/proxy/compression/deterministic/bash-build.js +0 -60
- package/src/proxy/compression/deterministic/bash-git.js +0 -101
- package/src/proxy/compression/deterministic/bash-infra.js +0 -64
- package/src/proxy/compression/deterministic/bash-misc.js +0 -53
- package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
- package/src/proxy/compression/deterministic/bash-test.js +0 -61
- package/src/proxy/compression/deterministic/glob.js +0 -26
- package/src/proxy/compression/deterministic/grep.js +0 -53
- package/src/proxy/compression/deterministic/index.js +0 -58
- package/src/proxy/compression/deterministic/read.js +0 -22
- package/src/proxy/compression/deterministic/utils.js +0 -33
- package/src/proxy/compression/pipeline.js +0 -270
- package/src/proxy/middleware/prompt-injector.js +0 -165
- package/src/proxy/middleware/response-interceptor.js +0 -223
- package/src/proxy/progress-monitor.js +0 -98
- package/src/proxy/proxy-lifecycle.js +0 -201
- package/src/proxy/proxy-server.js +0 -239
- package/src/proxy/security.js +0 -45
- /package/src/{becaria → ci}/index.js +0 -0
- /package/src/{becaria → ci}/repo.js +0 -0
package/src/sonar/scanner.js
CHANGED
|
@@ -150,7 +150,7 @@ async function resolveSonarTokenWithFallback(config, apiHost) {
|
|
|
150
150
|
return null;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
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);
|
package/src/utils/display.js
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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
|
-
|
|
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}${
|
|
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
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
444
|
-
console.log(` \u251c\u2500 ${icon} Budget: ${color}
|
|
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
|
-
"
|
|
591
|
+
"ci:pr-created": (event) => {
|
|
572
592
|
const url = event.detail?.prUrl || "";
|
|
573
|
-
console.log(` \u251c\u2500 ${ANSI.green}\ud83d\ude80
|
|
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
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 ---
|
package/src/utils/logger.js
CHANGED
|
@@ -19,7 +19,12 @@ const LEVEL_COLORS = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
function timestamp() {
|
|
22
|
-
|
|
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) {
|
package/src/utils/process.js
CHANGED
|
@@ -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
|
-
//
|
|
80
|
-
// Without this, a subprocess prompting for input
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
8
|
+
/** Skill names relevant for WebPerf analysis. Already installed globally via openskills. */
|
|
8
9
|
export const WEBPERF_SKILLS = [
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"optimize",
|
|
11
|
+
"audit",
|
|
12
|
+
"polish"
|
|
12
13
|
];
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Check whether Chrome DevTools MCP is configured and available.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
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
|
|
29
|
-
* @param {object} [logger]
|
|
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?.
|
|
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
|
-
//
|
|
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
|
}
|
package/templates/roles/coder.md
CHANGED
|
@@ -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.
|