karajan-code 1.59.0 → 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.
- 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 +34 -6
- 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 +3 -2
- package/src/commands/agents.js +0 -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 +10 -52
- package/src/domains/domain-loader.js +85 -4
- 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 +2 -2
- package/src/mcp/tools.js +3 -2
- 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/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/reviewer-stage.js +16 -5
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +435 -661
- 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 +34 -91
- 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 +52 -118
- package/src/roles/reviewer-role.js +69 -126
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +71 -15
- 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/banner.js +66 -0
- package/src/utils/display.js +35 -17
- 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/utils/welcome.js +3 -8
- 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 -198
- 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/README.md
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
+
> **v2.0 released** — Karajan Brain architecture, smart init, session journal, and more. See [MIGRATION-v2.md](./MIGRATION-v2.md) for breaking changes (proxy removed, becaria→ci rename).
|
|
27
|
+
|
|
26
28
|
You describe what you want to build. Karajan orchestrates multiple AI agents to plan it, implement it, test it, review it with SonarQube, and iterate. No babysitting required.
|
|
27
29
|
|
|
28
30
|
## What is Karajan?
|
package/package.json
CHANGED
package/scripts/install.js
CHANGED
|
@@ -641,7 +641,7 @@ async function collectSettings(rootDir, parsedArgs, instanceContext, nonInteract
|
|
|
641
641
|
sonarToken: "",
|
|
642
642
|
coder: instanceContext.existing?.coder || "codex",
|
|
643
643
|
reviewer: instanceContext.existing?.reviewer || "claude",
|
|
644
|
-
reviewerFallback: instanceContext.existing?.reviewerFallback ||
|
|
644
|
+
reviewerFallback: instanceContext.existing?.reviewerFallback || null,
|
|
645
645
|
setupMcpClaude: true,
|
|
646
646
|
setupMcpCodex: true,
|
|
647
647
|
runDoctor: true,
|
|
@@ -649,7 +649,7 @@ async function collectSettings(rootDir, parsedArgs, instanceContext, nonInteract
|
|
|
649
649
|
};
|
|
650
650
|
|
|
651
651
|
if (nonInteractive) {
|
|
652
|
-
|
|
652
|
+
const settings = {
|
|
653
653
|
linkGlobal: parseBool(pickSetting({ cli: parsedArgs, cliKey: "linkGlobal", envKey: "KJ_INSTALL_LINK_GLOBAL", fallback: defaults.linkGlobal }), defaults.linkGlobal),
|
|
654
654
|
kjHome: pickSetting({ cli: parsedArgs, cliKey: "kjHome", envKey: "KJ_INSTALL_KJ_HOME", fallback: defaults.kjHome }),
|
|
655
655
|
sonarHost: pickSetting({ cli: parsedArgs, cliKey: "sonarHost", envKey: "KJ_INSTALL_SONAR_HOST", fallback: defaults.sonarHost }),
|
|
@@ -660,13 +660,14 @@ async function collectSettings(rootDir, parsedArgs, instanceContext, nonInteract
|
|
|
660
660
|
sonarToken: pickSetting({ cli: parsedArgs, cliKey: "sonarToken", envKey: "KJ_SONAR_TOKEN", fallback: defaults.sonarToken }),
|
|
661
661
|
coder: pickSetting({ cli: parsedArgs, cliKey: "coder", envKey: "KJ_INSTALL_CODER", fallback: defaults.coder }),
|
|
662
662
|
reviewer: pickSetting({ cli: parsedArgs, cliKey: "reviewer", envKey: "KJ_INSTALL_REVIEWER", fallback: defaults.reviewer }),
|
|
663
|
-
reviewerFallback: pickSetting({ cli: parsedArgs, cliKey: "reviewerFallback", envKey: "KJ_INSTALL_REVIEWER_FALLBACK", fallback: defaults.reviewerFallback }),
|
|
664
|
-
setupMcpClaude: parseBool(pickSetting({ cli: parsedArgs, cliKey: "setupMcpClaude", envKey: "KJ_INSTALL_SETUP_MCP_CLAUDE", fallback: defaults.setupMcpClaude }), defaults.setupMcpClaude),
|
|
665
|
-
setupMcpCodex: parseBool(pickSetting({ cli: parsedArgs, cliKey: "setupMcpCodex", envKey: "KJ_INSTALL_SETUP_MCP_CODEX", fallback: defaults.setupMcpCodex }), defaults.setupMcpCodex),
|
|
666
|
-
setupChromeDevtools: parseBool(pickSetting({ cli: parsedArgs, cliKey: "setupChromeDevtools", envKey: "KJ_INSTALL_SETUP_CHROME_DEVTOOLS", fallback: true }), true),
|
|
667
|
-
runDoctor: parseBool(pickSetting({ cli: parsedArgs, cliKey: "runDoctor", envKey: "KJ_INSTALL_RUN_DOCTOR", fallback: defaults.runDoctor }), defaults.runDoctor),
|
|
668
|
-
runTests: parseBool(pickSetting({ cli: parsedArgs, cliKey: "runTests", envKey: "KJ_INSTALL_RUN_TESTS", fallback: defaults.runTests }), defaults.runTests)
|
|
669
663
|
};
|
|
664
|
+
settings.reviewerFallback = pickSetting({ cli: parsedArgs, cliKey: "reviewerFallback", envKey: "KJ_INSTALL_REVIEWER_FALLBACK", fallback: settings.reviewer });
|
|
665
|
+
settings.setupMcpClaude = parseBool(pickSetting({ cli: parsedArgs, cliKey: "setupMcpClaude", envKey: "KJ_INSTALL_SETUP_MCP_CLAUDE", fallback: defaults.setupMcpClaude }), defaults.setupMcpClaude);
|
|
666
|
+
settings.setupMcpCodex = parseBool(pickSetting({ cli: parsedArgs, cliKey: "setupMcpCodex", envKey: "KJ_INSTALL_SETUP_MCP_CODEX", fallback: defaults.setupMcpCodex }), defaults.setupMcpCodex);
|
|
667
|
+
settings.setupChromeDevtools = parseBool(pickSetting({ cli: parsedArgs, cliKey: "setupChromeDevtools", envKey: "KJ_INSTALL_SETUP_CHROME_DEVTOOLS", fallback: true }), true);
|
|
668
|
+
settings.runDoctor = parseBool(pickSetting({ cli: parsedArgs, cliKey: "runDoctor", envKey: "KJ_INSTALL_RUN_DOCTOR", fallback: defaults.runDoctor }), defaults.runDoctor);
|
|
669
|
+
settings.runTests = parseBool(pickSetting({ cli: parsedArgs, cliKey: "runTests", envKey: "KJ_INSTALL_RUN_TESTS", fallback: defaults.runTests }), defaults.runTests);
|
|
670
|
+
return settings;
|
|
670
671
|
}
|
|
671
672
|
|
|
672
673
|
const linkGlobal = await askBool("Link karajan binaries globally with npm link", defaults.linkGlobal);
|
|
@@ -706,7 +707,7 @@ async function collectSettings(rootDir, parsedArgs, instanceContext, nonInteract
|
|
|
706
707
|
}
|
|
707
708
|
}
|
|
708
709
|
|
|
709
|
-
|
|
710
|
+
const settings = {
|
|
710
711
|
linkGlobal,
|
|
711
712
|
kjHome,
|
|
712
713
|
sonarHost,
|
|
@@ -717,13 +718,15 @@ async function collectSettings(rootDir, parsedArgs, instanceContext, nonInteract
|
|
|
717
718
|
sonarToken,
|
|
718
719
|
coder: await ask("Default coder", defaults.coder),
|
|
719
720
|
reviewer: await ask("Default reviewer", defaults.reviewer),
|
|
720
|
-
reviewerFallback: await ask("Reviewer fallback", defaults.reviewerFallback),
|
|
721
|
-
setupMcpClaude: await askBool("Configure Claude MCP automatically", defaults.setupMcpClaude),
|
|
722
|
-
setupMcpCodex: await askBool("Configure Codex MCP automatically", defaults.setupMcpCodex),
|
|
723
|
-
setupChromeDevtools: await askBool("Configure Chrome DevTools MCP", true),
|
|
724
|
-
runDoctor: await askBool("Run kj doctor now", defaults.runDoctor),
|
|
725
|
-
runTests: await askBool("Run tests now", defaults.runTests)
|
|
726
721
|
};
|
|
722
|
+
settings.reviewerFallback = await ask("Reviewer fallback", settings.reviewer);
|
|
723
|
+
settings.setupMcpClaude = await askBool("Configure Claude MCP automatically", defaults.setupMcpClaude);
|
|
724
|
+
settings.setupMcpCodex = await askBool("Configure Codex MCP automatically", defaults.setupMcpCodex);
|
|
725
|
+
settings.setupChromeDevtools = await askBool("Configure Chrome DevTools MCP", true);
|
|
726
|
+
settings.runDoctor = await askBool("Run kj doctor now", defaults.runDoctor);
|
|
727
|
+
settings.runTests = await askBool("Run tests now", defaults.runTests);
|
|
728
|
+
|
|
729
|
+
return settings;
|
|
727
730
|
}
|
|
728
731
|
|
|
729
732
|
async function main() {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
2
|
import { runCommand } from "../utils/process.js";
|
|
3
3
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
|
-
import { getProxyEnv } from "../proxy/proxy-lifecycle.js";
|
|
5
4
|
|
|
6
5
|
export class AiderAgent extends BaseAgent {
|
|
7
6
|
async runTask(task) {
|
|
@@ -29,13 +28,7 @@ export class AiderAgent extends BaseAgent {
|
|
|
29
28
|
async _exec(task, model) {
|
|
30
29
|
const args = ["--yes", "--message", task.prompt];
|
|
31
30
|
if (model) args.push("--model", model);
|
|
32
|
-
const proxyEnv = getProxyEnv();
|
|
33
|
-
if (proxyEnv) {
|
|
34
|
-
args.push("--api-base", proxyEnv.OPENAI_BASE_URL);
|
|
35
|
-
}
|
|
36
|
-
const env = proxyEnv ? { ...process.env, ...proxyEnv } : undefined;
|
|
37
31
|
const res = await runCommand(resolveBin("aider"), args, {
|
|
38
|
-
...(env && { env }),
|
|
39
32
|
onOutput: task.onOutput,
|
|
40
33
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
41
34
|
timeout: task.timeoutMs
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
2
|
import { runCommand } from "../utils/process.js";
|
|
3
3
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
|
-
import { getProxyEnv } from "../proxy/proxy-lifecycle.js";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Safely parse a JSON line, returning null on failure.
|
|
@@ -12,6 +11,37 @@ function tryParseJson(line) {
|
|
|
12
11
|
} catch { return null; }
|
|
13
12
|
}
|
|
14
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Extract a human-readable error message from Claude's raw NDJSON stderr output.
|
|
16
|
+
* Claude emits system init, api_retry, assistant error, and result lines.
|
|
17
|
+
* We extract just the meaningful error text, discarding the JSON noise.
|
|
18
|
+
*/
|
|
19
|
+
export function sanitizeClaudeError(raw) {
|
|
20
|
+
if (!raw) return "Unknown error";
|
|
21
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
22
|
+
|
|
23
|
+
// Look for a result line with the actual error
|
|
24
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
25
|
+
const obj = tryParseJson(lines[i]);
|
|
26
|
+
if (!obj) continue;
|
|
27
|
+
if (obj.type === "result" && obj.result) return String(obj.result);
|
|
28
|
+
if (obj.type === "assistant" && obj.message?.content) {
|
|
29
|
+
const text = obj.message.content
|
|
30
|
+
.filter(b => b.type === "text" && b.text)
|
|
31
|
+
.map(b => b.text)
|
|
32
|
+
.join(" ");
|
|
33
|
+
if (text) return text;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If no JSON parsed, return first non-JSON line or truncated raw
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (!line.startsWith("{")) return line.slice(0, 200);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return raw.slice(0, 200);
|
|
43
|
+
}
|
|
44
|
+
|
|
15
45
|
/**
|
|
16
46
|
* Try to extract a result string from a parsed JSON object.
|
|
17
47
|
* Returns the result string or null if the object is not a result message.
|
|
@@ -42,7 +72,7 @@ function collectAssistantText(obj) {
|
|
|
42
72
|
* usage.input_tokens/output_tokens, and modelUsage.
|
|
43
73
|
* Returns an object with tokens_in, tokens_out, cost_usd, model or null if not found.
|
|
44
74
|
*/
|
|
45
|
-
|
|
75
|
+
function extractUsageFromStreamJson(raw) {
|
|
46
76
|
const lines = (raw || "").split("\n").filter(Boolean);
|
|
47
77
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
48
78
|
const obj = tryParseJson(lines[i]);
|
|
@@ -135,8 +165,6 @@ function createStreamJsonFilter(onOutput) {
|
|
|
135
165
|
*/
|
|
136
166
|
function cleanExecaOpts(extra = {}) {
|
|
137
167
|
const { CLAUDECODE, ...env } = process.env;
|
|
138
|
-
const proxyEnv = getProxyEnv();
|
|
139
|
-
if (proxyEnv) Object.assign(env, proxyEnv);
|
|
140
168
|
return { env, stdin: "ignore", ...extra };
|
|
141
169
|
}
|
|
142
170
|
|
|
@@ -197,7 +225,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
197
225
|
const raw = pickOutput(res);
|
|
198
226
|
const output = extractTextFromStreamJson(raw);
|
|
199
227
|
const usage = extractUsageFromStreamJson(raw);
|
|
200
|
-
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode, ...usage };
|
|
228
|
+
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : sanitizeClaudeError(raw), exitCode: res.exitCode, ...usage };
|
|
201
229
|
}
|
|
202
230
|
|
|
203
231
|
// Without streaming, use json output to get structured response via stderr
|
|
@@ -206,7 +234,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
206
234
|
const raw = pickOutput(res);
|
|
207
235
|
const output = extractTextFromStreamJson(raw);
|
|
208
236
|
const usage = extractUsageFromStreamJson(raw);
|
|
209
|
-
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode, ...usage };
|
|
237
|
+
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : sanitizeClaudeError(raw), exitCode: res.exitCode, ...usage };
|
|
210
238
|
}
|
|
211
239
|
|
|
212
240
|
async _reviewTaskExec(task, model) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
2
|
import { runCommand } from "../utils/process.js";
|
|
3
3
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
|
-
import { getProxyEnv } from "../proxy/proxy-lifecycle.js";
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Extract token usage from Codex CLI stdout.
|
|
@@ -10,7 +9,7 @@ import { getProxyEnv } from "../proxy/proxy-lifecycle.js";
|
|
|
10
9
|
* Since Codex doesn't split input/output, we assign the total to tokens_out
|
|
11
10
|
* as a conservative estimate for cost calculation.
|
|
12
11
|
*/
|
|
13
|
-
|
|
12
|
+
function extractCodexTokens(stdout) {
|
|
14
13
|
const match = (stdout || "").match(/tokens?\s+used\s*\n\s*([\d,]+)/i);
|
|
15
14
|
if (!match) return null;
|
|
16
15
|
const total = Number(match[1].replace(/,/g, ""));
|
|
@@ -42,14 +41,11 @@ export class CodexAgent extends BaseAgent {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
async _exec(task, model, role) {
|
|
45
|
-
const args = ["exec"];
|
|
44
|
+
const args = ["exec", "--skip-git-repo-check"];
|
|
46
45
|
if (model) args.push("--model", model);
|
|
47
46
|
if (role !== "reviewer" && this.isAutoApproveEnabled(role)) args.push("--full-auto");
|
|
48
47
|
args.push("-");
|
|
49
|
-
const proxyEnv = getProxyEnv();
|
|
50
|
-
const env = proxyEnv ? { ...process.env, ...proxyEnv } : undefined;
|
|
51
48
|
const res = await runCommand(resolveBin("codex"), args, {
|
|
52
|
-
...(env && { env }),
|
|
53
49
|
onOutput: task.onOutput,
|
|
54
50
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
55
51
|
timeout: task.timeoutMs,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
2
|
import { runCommand } from "../utils/process.js";
|
|
3
3
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
|
-
import { getProxyEnv } from "../proxy/proxy-lifecycle.js";
|
|
5
4
|
|
|
6
5
|
export class GeminiAgent extends BaseAgent {
|
|
7
6
|
async runTask(task) {
|
|
@@ -30,10 +29,7 @@ export class GeminiAgent extends BaseAgent {
|
|
|
30
29
|
const args = ["-p", task.prompt];
|
|
31
30
|
if (mode === "review") args.push("--output-format", "json");
|
|
32
31
|
if (model) args.push("--model", model);
|
|
33
|
-
const proxyEnv = getProxyEnv();
|
|
34
|
-
const env = proxyEnv ? { ...process.env, ...proxyEnv } : undefined;
|
|
35
32
|
const res = await runCommand(resolveBin("gemini"), args, {
|
|
36
|
-
...(env && { env }),
|
|
37
33
|
onOutput: task.onOutput,
|
|
38
34
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
39
35
|
timeout: task.timeoutMs
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
2
|
import { runCommand } from "../utils/process.js";
|
|
3
3
|
import { resolveBin } from "./resolve-bin.js";
|
|
4
|
-
import { getProxyEnv } from "../proxy/proxy-lifecycle.js";
|
|
5
4
|
|
|
6
5
|
export class OpenCodeAgent extends BaseAgent {
|
|
7
6
|
async runTask(task) {
|
|
@@ -31,10 +30,7 @@ export class OpenCodeAgent extends BaseAgent {
|
|
|
31
30
|
if (jsonFormat) args.push("--format", "json");
|
|
32
31
|
if (model) args.push("--model", model);
|
|
33
32
|
args.push(task.prompt);
|
|
34
|
-
const proxyEnv = getProxyEnv();
|
|
35
|
-
const env = proxyEnv ? { ...process.env, ...proxyEnv } : undefined;
|
|
36
33
|
const res = await runCommand(resolveBin("opencode"), args, {
|
|
37
|
-
...(env && { env }),
|
|
38
34
|
onOutput: task.onOutput,
|
|
39
35
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
40
36
|
timeout: task.timeoutMs
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* so the
|
|
2
|
+
* Karajan CI dispatch client — sends repository_dispatch events via gh CLI
|
|
3
|
+
* so the Karajan CI Gateway can publish comments and reviews on PRs.
|
|
4
4
|
*
|
|
5
|
-
* Event types are configurable via
|
|
6
|
-
* - comment_event (default: "
|
|
7
|
-
* - review_event (default: "
|
|
5
|
+
* Event types are configurable via ci config:
|
|
6
|
+
* - comment_event (default: "kj-comment")
|
|
7
|
+
* - review_event (default: "kj-review")
|
|
8
8
|
*
|
|
9
|
-
* Only active when
|
|
9
|
+
* Only active when ci.enabled: true.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { runCommand } from "../utils/process.js";
|
|
@@ -59,15 +59,15 @@ async function sendDispatch(repo, payload) {
|
|
|
59
59
|
/**
|
|
60
60
|
* Send a comment event so the gateway posts a PR comment.
|
|
61
61
|
* @param {object} opts
|
|
62
|
-
* @param {object} [opts.
|
|
62
|
+
* @param {object} [opts.ciConfig] - ci config section (optional)
|
|
63
63
|
*/
|
|
64
|
-
export async function dispatchComment({ repo, prNumber, agent, body,
|
|
64
|
+
export async function dispatchComment({ repo, prNumber, agent, body, ciConfig }) {
|
|
65
65
|
validateCommon({ repo, prNumber });
|
|
66
66
|
validateAgent(agent);
|
|
67
67
|
if (!body) throw new Error("body is required (comment text)");
|
|
68
68
|
|
|
69
|
-
const prefix =
|
|
70
|
-
const eventType =
|
|
69
|
+
const prefix = ciConfig?.comment_prefix === false ? "" : `[${agent}] `;
|
|
70
|
+
const eventType = ciConfig?.comment_event || "kj-comment";
|
|
71
71
|
|
|
72
72
|
await sendDispatch(repo, {
|
|
73
73
|
event_type: eventType,
|
|
@@ -78,9 +78,9 @@ export async function dispatchComment({ repo, prNumber, agent, body, becariaConf
|
|
|
78
78
|
/**
|
|
79
79
|
* Send a review event so the gateway submits a formal PR review.
|
|
80
80
|
* @param {object} opts
|
|
81
|
-
* @param {object} [opts.
|
|
81
|
+
* @param {object} [opts.ciConfig] - ci config section (optional)
|
|
82
82
|
*/
|
|
83
|
-
export async function dispatchReview({ repo, prNumber, event, body, agent,
|
|
83
|
+
export async function dispatchReview({ repo, prNumber, event, body, agent, ciConfig }) {
|
|
84
84
|
validateCommon({ repo, prNumber });
|
|
85
85
|
validateAgent(agent);
|
|
86
86
|
if (!VALID_REVIEW_EVENTS.includes(event)) {
|
|
@@ -90,7 +90,7 @@ export async function dispatchReview({ repo, prNumber, event, body, agent, becar
|
|
|
90
90
|
}
|
|
91
91
|
if (!body) throw new Error("body is required (review text)");
|
|
92
92
|
|
|
93
|
-
const eventType =
|
|
93
|
+
const eventType = ciConfig?.review_event || "kj-review";
|
|
94
94
|
|
|
95
95
|
await sendDispatch(repo, {
|
|
96
96
|
event_type: eventType,
|
package/src/cli.js
CHANGED
|
@@ -57,7 +57,7 @@ program
|
|
|
57
57
|
.command("init")
|
|
58
58
|
.description("Initialize config, review rules and SonarQube")
|
|
59
59
|
.option("--no-interactive", "Skip wizard, use defaults (for CI/scripts)")
|
|
60
|
-
.option("--scaffold-
|
|
60
|
+
.option("--scaffold-ci", "Scaffold Karajan CI Gateway workflow files")
|
|
61
61
|
.action(async (flags) => {
|
|
62
62
|
await withConfig("init", flags, async ({ config, logger }) => {
|
|
63
63
|
await initCommand({ logger, flags });
|
|
@@ -111,7 +111,7 @@ program
|
|
|
111
111
|
.option("--auto-commit")
|
|
112
112
|
.option("--auto-push")
|
|
113
113
|
.option("--auto-pr")
|
|
114
|
-
.option("--enable-
|
|
114
|
+
.option("--enable-ci", "Enable Karajan CI (early PR + dispatch comments/reviews)")
|
|
115
115
|
.option("--branch-prefix <prefix>")
|
|
116
116
|
.option("--task-type <type>", "Explicit task type: sw, infra, doc, add-tests, refactor")
|
|
117
117
|
.option("--methodology <name>")
|
|
@@ -127,6 +127,7 @@ program
|
|
|
127
127
|
.option("--smart-models", "Enable smart model selection based on triage complexity")
|
|
128
128
|
.option("--no-smart-models", "Disable smart model selection")
|
|
129
129
|
.option("--design", "Activate design refactoring mode (impeccable role applies design changes)")
|
|
130
|
+
.option("--domain <text-or-path>", "Domain knowledge: inline text or path to .md file")
|
|
130
131
|
.option("--dry-run", "Show what would be executed without running anything")
|
|
131
132
|
.option("--json", "Output JSON only (no styled display)")
|
|
132
133
|
.option("-q, --quiet", "Show only stage status lines, suppress raw agent output (default)")
|
package/src/commands/agents.js
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -12,7 +12,7 @@ import { ensureGitRepo } from "../utils/git.js";
|
|
|
12
12
|
import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
|
|
13
13
|
import { getInstallCommand } from "../utils/os-detect.js";
|
|
14
14
|
import { withDocLink } from "../utils/doc-links.js";
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
|
|
17
17
|
function getPackageVersion() {
|
|
18
18
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
@@ -144,27 +144,27 @@ async function checkSerena() {
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
async function
|
|
147
|
+
async function checkCiWorkflows(projectDir) {
|
|
148
148
|
const checks = [];
|
|
149
149
|
const workflowDir = path.join(projectDir, ".github", "workflows");
|
|
150
|
-
const requiredWorkflows = ["
|
|
150
|
+
const requiredWorkflows = ["kj-ci-gateway.yml", "automerge.yml", "houston-override.yml"];
|
|
151
151
|
for (const wf of requiredWorkflows) {
|
|
152
152
|
const wfPath = path.join(workflowDir, wf);
|
|
153
153
|
const wfExists = await exists(wfPath);
|
|
154
154
|
checks.push({
|
|
155
|
-
name: `
|
|
156
|
-
label: `
|
|
155
|
+
name: `ci:workflow:${wf}`,
|
|
156
|
+
label: `CI workflow: ${wf}`,
|
|
157
157
|
ok: wfExists,
|
|
158
158
|
detail: wfExists ? "Found" : "Not found",
|
|
159
|
-
fix: wfExists ? null : `Run 'kj init --scaffold-
|
|
159
|
+
fix: wfExists ? null : `Run 'kj init --scaffold-ci' or copy from karajan-code/templates/workflows/${wf}`
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
return checks;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
async function
|
|
165
|
+
async function checkCiSecrets() {
|
|
166
166
|
try {
|
|
167
|
-
const { detectRepo } = await import("../
|
|
167
|
+
const { detectRepo } = await import("../ci/repo.js");
|
|
168
168
|
const repo = await detectRepo();
|
|
169
169
|
if (!repo) return null;
|
|
170
170
|
|
|
@@ -172,38 +172,38 @@ async function checkBecariaSecrets() {
|
|
|
172
172
|
if (secretsRes.exitCode !== 0) return null;
|
|
173
173
|
|
|
174
174
|
const names = new Set(secretsRes.stdout.split("\n").map((s) => s.trim()));
|
|
175
|
-
const hasAppId = names.has("
|
|
176
|
-
const hasKey = names.has("
|
|
175
|
+
const hasAppId = names.has("KJ_CI_APP_ID");
|
|
176
|
+
const hasKey = names.has("KJ_CI_PRIVATE_KEY");
|
|
177
177
|
const secretsOk = hasAppId && hasKey;
|
|
178
|
-
const missing = [!hasAppId && "
|
|
178
|
+
const missing = [!hasAppId && "KJ_CI_APP_ID", !hasKey && "KJ_CI_PRIVATE_KEY"].filter(Boolean).join(" ");
|
|
179
179
|
return {
|
|
180
|
-
name: "
|
|
181
|
-
label: "
|
|
180
|
+
name: "ci:secrets",
|
|
181
|
+
label: "CI: GitHub secrets",
|
|
182
182
|
ok: secretsOk,
|
|
183
|
-
detail: secretsOk ? "
|
|
184
|
-
fix: secretsOk ? null : "Add
|
|
183
|
+
detail: secretsOk ? "KJ_CI_APP_ID + KJ_CI_PRIVATE_KEY found" : `Missing: ${missing}`,
|
|
184
|
+
fix: secretsOk ? null : "Add KJ_CI_APP_ID and KJ_CI_PRIVATE_KEY as GitHub repository secrets"
|
|
185
185
|
};
|
|
186
186
|
} catch { /* GitHub API error */
|
|
187
187
|
return null;
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
async function
|
|
191
|
+
async function checkCiInfra(config) {
|
|
192
192
|
const checks = [];
|
|
193
193
|
const projectDir = config.projectDir || process.cwd();
|
|
194
194
|
|
|
195
|
-
checks.push(...await
|
|
195
|
+
checks.push(...await checkCiWorkflows(projectDir));
|
|
196
196
|
|
|
197
197
|
const ghCheck = await checkBinary("gh");
|
|
198
198
|
checks.push({
|
|
199
|
-
name: "
|
|
200
|
-
label: "
|
|
199
|
+
name: "ci:gh",
|
|
200
|
+
label: "CI: gh CLI",
|
|
201
201
|
ok: ghCheck.ok,
|
|
202
202
|
detail: ghCheck.ok ? ghCheck.version : "Not found",
|
|
203
203
|
fix: ghCheck.ok ? null : "Install GitHub CLI: https://cli.github.com/"
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
-
const secretsCheck = await
|
|
206
|
+
const secretsCheck = await checkCiSecrets();
|
|
207
207
|
if (secretsCheck) checks.push(secretsCheck);
|
|
208
208
|
|
|
209
209
|
return checks;
|
|
@@ -343,23 +343,6 @@ async function checkAgentConfigs() {
|
|
|
343
343
|
return checks;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
async function checkProxy(config) {
|
|
347
|
-
if (config.proxy?.enabled === false) {
|
|
348
|
-
return { name: "proxy", label: "Proxy", ok: true, detail: "Disabled in config", fix: null };
|
|
349
|
-
}
|
|
350
|
-
let running = false;
|
|
351
|
-
try {
|
|
352
|
-
running = await isProxyRunning();
|
|
353
|
-
} catch { /* proxy check failed */ }
|
|
354
|
-
return {
|
|
355
|
-
name: "proxy",
|
|
356
|
-
label: "Proxy",
|
|
357
|
-
ok: true,
|
|
358
|
-
detail: running ? "Running" : "Available (starts on kj run)",
|
|
359
|
-
fix: null
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
346
|
export async function runChecks({ config }) {
|
|
364
347
|
const checks = [];
|
|
365
348
|
|
|
@@ -377,14 +360,13 @@ export async function runChecks({ config }) {
|
|
|
377
360
|
checks.push(await checkSerena());
|
|
378
361
|
}
|
|
379
362
|
|
|
380
|
-
if (config.
|
|
381
|
-
checks.push(...await
|
|
363
|
+
if (config.ci?.enabled) {
|
|
364
|
+
checks.push(...await checkCiInfra(config));
|
|
382
365
|
}
|
|
383
366
|
|
|
384
367
|
checks.push(...await checkAgentConfigs());
|
|
385
368
|
checks.push(...await checkRuleFiles(config));
|
|
386
369
|
checks.push(await checkRtk());
|
|
387
|
-
checks.push(await checkProxy(config));
|
|
388
370
|
|
|
389
371
|
return checks;
|
|
390
372
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -222,14 +222,14 @@ async function setupSonarQube(config, logger) {
|
|
|
222
222
|
logger.info(' or export KJ_SONAR_TOKEN="<your-token>"');
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
async function
|
|
226
|
-
if (!config.
|
|
225
|
+
async function scaffoldCiGateway(config, flags, logger) {
|
|
226
|
+
if (!config.ci?.enabled && !flags.scaffoldCi) return;
|
|
227
227
|
const projectDir = process.cwd();
|
|
228
228
|
const workflowDir = path.join(projectDir, ".github", "workflows");
|
|
229
229
|
await ensureDir(workflowDir);
|
|
230
230
|
|
|
231
231
|
const templatesDir = path.resolve(import.meta.dirname, "../../templates/workflows");
|
|
232
|
-
const workflows = ["
|
|
232
|
+
const workflows = ["kj-ci-gateway.yml", "automerge.yml", "houston-override.yml"];
|
|
233
233
|
|
|
234
234
|
for (const wf of workflows) {
|
|
235
235
|
const destPath = path.join(workflowDir, wf);
|
|
@@ -248,11 +248,11 @@ async function scaffoldBecariaGateway(config, flags, logger) {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
logger.info("");
|
|
251
|
-
logger.info("
|
|
252
|
-
logger.info(" 1. Create a GitHub App named '
|
|
251
|
+
logger.info("Karajan CI Gateway scaffolded. Next steps:");
|
|
252
|
+
logger.info(" 1. Create a GitHub App named 'kj-reviewer' with pull_request write permissions");
|
|
253
253
|
logger.info(" 2. Install the App on your repository");
|
|
254
|
-
logger.info(" 3. Add secrets:
|
|
255
|
-
logger.info(" 4. Push the workflow files and enable 'kj run --enable-
|
|
254
|
+
logger.info(" 3. Add secrets: KJ_CI_APP_ID and KJ_CI_PRIVATE_KEY");
|
|
255
|
+
logger.info(" 4. Push the workflow files and enable 'kj run --enable-ci'");
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
async function installSkills(logger, interactive) {
|
|
@@ -403,7 +403,7 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
await setupSonarQube(config, logger);
|
|
406
|
-
await
|
|
406
|
+
await scaffoldCiGateway(config, flags, logger);
|
|
407
407
|
|
|
408
408
|
// Telemetry: anonymous install event (non-blocking)
|
|
409
409
|
const { sendTelemetryEvent } = await import("../utils/telemetry.js");
|
package/src/commands/report.js
CHANGED
|
@@ -153,7 +153,6 @@ async function buildReport(dir, sessionId) {
|
|
|
153
153
|
if (session.pg_task_id) report.pg_task_id = session.pg_task_id;
|
|
154
154
|
if (session.pg_project_id) report.pg_project_id = session.pg_project_id;
|
|
155
155
|
if (session.rtk_savings) report.rtk_savings = session.rtk_savings;
|
|
156
|
-
if (session.proxy_stats) report.proxy_stats = session.proxy_stats;
|
|
157
156
|
return report;
|
|
158
157
|
}
|
|
159
158
|
|
|
@@ -200,7 +199,6 @@ function printTextReport(report) {
|
|
|
200
199
|
console.log(formatBudgetText(report.budget_consumed));
|
|
201
200
|
console.log("Commits Generated:");
|
|
202
201
|
console.log(formatCommitsText(report.commits_generated));
|
|
203
|
-
printProxyStats(report.proxy_stats);
|
|
204
202
|
}
|
|
205
203
|
|
|
206
204
|
function formatDuration(ms) {
|
|
@@ -329,16 +327,6 @@ async function resolveTraceOptions(currency) {
|
|
|
329
327
|
return { cur, rate };
|
|
330
328
|
}
|
|
331
329
|
|
|
332
|
-
function printProxyStats(proxyStats) {
|
|
333
|
-
if (!proxyStats) return;
|
|
334
|
-
const orig = proxyStats.originalTokens ?? 0;
|
|
335
|
-
const comp = proxyStats.compressedTokens ?? 0;
|
|
336
|
-
const pct = orig > 0 ? ((1 - comp / orig) * 100).toFixed(1) : "0.0";
|
|
337
|
-
const hits = proxyStats.cacheHits ?? 0;
|
|
338
|
-
console.log("");
|
|
339
|
-
console.log(`Proxy: ${orig} original tokens, ${comp} compressed tokens, ${pct}% savings, ${hits} cache hits`);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
330
|
function printRtkSavings(rtkSavings) {
|
|
343
331
|
if (!rtkSavings || !rtkSavings.callCount) return;
|
|
344
332
|
const tokens = rtkSavings.estimatedTokensSaved ?? 0;
|
|
@@ -357,7 +345,6 @@ function printTraceReport(report, currency, exchangeRate) {
|
|
|
357
345
|
console.log("");
|
|
358
346
|
printTraceTable(report.budget_trace, { currency, exchangeRate });
|
|
359
347
|
printRtkSavings(report.rtk_savings);
|
|
360
|
-
printProxyStats(report.proxy_stats);
|
|
361
348
|
}
|
|
362
349
|
|
|
363
350
|
async function handlePgTaskReport({ dir, pgTask, list, sessionId, format, trace, currency }) {
|