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.
- 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/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/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/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 +428 -664
- 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/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
|
|
@@ -72,7 +72,7 @@ function collectAssistantText(obj) {
|
|
|
72
72
|
* usage.input_tokens/output_tokens, and modelUsage.
|
|
73
73
|
* Returns an object with tokens_in, tokens_out, cost_usd, model or null if not found.
|
|
74
74
|
*/
|
|
75
|
-
|
|
75
|
+
function extractUsageFromStreamJson(raw) {
|
|
76
76
|
const lines = (raw || "").split("\n").filter(Boolean);
|
|
77
77
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
78
78
|
const obj = tryParseJson(lines[i]);
|
|
@@ -165,9 +165,6 @@ function createStreamJsonFilter(onOutput) {
|
|
|
165
165
|
*/
|
|
166
166
|
function cleanExecaOpts(extra = {}) {
|
|
167
167
|
const { CLAUDECODE, ...env } = process.env;
|
|
168
|
-
// NOTE: Claude Code CLI does not respect ANTHROPIC_BASE_URL — proxy env
|
|
169
|
-
// injection is skipped for claude to avoid ECONNREFUSED errors.
|
|
170
|
-
// Other agents (codex, gemini, aider) use SDKs that do respect base URL vars.
|
|
171
168
|
return { env, stdin: "ignore", ...extra };
|
|
172
169
|
}
|
|
173
170
|
|
|
@@ -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>")
|
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 }) {
|
package/src/commands/review.js
CHANGED
|
@@ -7,24 +7,24 @@ import { resolveReviewProfile } from "../review/profiles.js";
|
|
|
7
7
|
|
|
8
8
|
export async function reviewCommand({ task, config, logger, baseRef }) {
|
|
9
9
|
const reviewerRole = resolveRole(config, "reviewer");
|
|
10
|
-
await assertAgentsAvailable([reviewerRole.provider
|
|
10
|
+
await assertAgentsAvailable([reviewerRole.provider]);
|
|
11
11
|
logger.info(`Reviewer (${reviewerRole.provider}) starting...`);
|
|
12
12
|
const reviewer = createAgent(reviewerRole.provider, config, logger);
|
|
13
13
|
|
|
14
14
|
let diff;
|
|
15
|
-
if (config.
|
|
16
|
-
//
|
|
17
|
-
const { detectRepo, detectPrNumber } = await import("../
|
|
18
|
-
const { getPrDiff } = await import("../
|
|
15
|
+
if (config.ci?.enabled) {
|
|
16
|
+
// CI mode: read diff from open PR
|
|
17
|
+
const { detectRepo, detectPrNumber } = await import("../ci/repo.js");
|
|
18
|
+
const { getPrDiff } = await import("../ci/pr-diff.js");
|
|
19
19
|
const repo = await detectRepo();
|
|
20
20
|
const prNumber = await detectPrNumber();
|
|
21
21
|
if (!prNumber) {
|
|
22
|
-
throw new Error("
|
|
22
|
+
throw new Error("CI enabled but no open PR found for current branch. Create a PR first or disable CI.");
|
|
23
23
|
}
|
|
24
|
-
logger.info(`
|
|
24
|
+
logger.info(`CI: reading PR diff #${prNumber}`);
|
|
25
25
|
diff = await getPrDiff(prNumber);
|
|
26
26
|
// Store for dispatch later
|
|
27
|
-
config.
|
|
27
|
+
config._ci_pr = { repo, prNumber };
|
|
28
28
|
} else {
|
|
29
29
|
const resolvedBase = await computeBaseRef({ baseBranch: config.base_branch, baseRef });
|
|
30
30
|
diff = await generateDiff({ baseRef: resolvedBase });
|
|
@@ -42,12 +42,12 @@ export async function reviewCommand({ task, config, logger, baseRef }) {
|
|
|
42
42
|
console.log(result.output);
|
|
43
43
|
logger.info(`Reviewer completed (exit ${result.exitCode})`);
|
|
44
44
|
|
|
45
|
-
//
|
|
46
|
-
if (config.
|
|
45
|
+
// CI: dispatch review result
|
|
46
|
+
if (config.ci?.enabled && config._ci_pr) {
|
|
47
47
|
try {
|
|
48
|
-
const { dispatchReview, dispatchComment } = await import("../
|
|
49
|
-
const { repo, prNumber } = config.
|
|
50
|
-
const bc = config.
|
|
48
|
+
const { dispatchReview, dispatchComment } = await import("../ci/dispatch.js");
|
|
49
|
+
const { repo, prNumber } = config._ci_pr;
|
|
50
|
+
const bc = config.ci;
|
|
51
51
|
|
|
52
52
|
// Try to parse structured review from output
|
|
53
53
|
let review;
|
|
@@ -61,18 +61,18 @@ export async function reviewCommand({ task, config, logger, baseRef }) {
|
|
|
61
61
|
await dispatchReview({
|
|
62
62
|
repo, prNumber, event,
|
|
63
63
|
body: review.summary || result.output.slice(0, 500),
|
|
64
|
-
agent: "Reviewer",
|
|
64
|
+
agent: "Reviewer", ciConfig: bc
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
await dispatchComment({
|
|
68
68
|
repo, prNumber, agent: "Reviewer",
|
|
69
69
|
body: `Standalone review: ${event}\n\n${review.summary || result.output.slice(0, 1000)}`,
|
|
70
|
-
|
|
70
|
+
ciConfig: bc
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
logger.info(`
|
|
73
|
+
logger.info(`CI: dispatched review for PR #${prNumber}`);
|
|
74
74
|
} catch (err) {
|
|
75
|
-
logger.warn(`
|
|
75
|
+
logger.warn(`CI dispatch failed (non-blocking): ${err.message}`);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
}
|
package/src/commands/run.js
CHANGED
|
@@ -35,8 +35,7 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
35
35
|
} catch { /* non-blocking */ }
|
|
36
36
|
|
|
37
37
|
const requiredProviders = [
|
|
38
|
-
resolveRole(config, "coder").provider
|
|
39
|
-
config.reviewer_options?.fallback_reviewer
|
|
38
|
+
resolveRole(config, "coder").provider
|
|
40
39
|
];
|
|
41
40
|
if (config.pipeline?.reviewer?.enabled !== false) {
|
|
42
41
|
requiredProviders.push(resolveRole(config, "reviewer").provider);
|
package/src/commands/status.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { readRunLog } from "../utils/run-log.js";
|
|
6
6
|
import { loadMostRecentSession } from "../session-store.js";
|
|
7
7
|
import { buildDashboard } from "../utils/status-dashboard.js";
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Run the `kj status` command: print a human-readable dashboard to stdout.
|
|
@@ -33,10 +33,4 @@ export async function statusCommand({ lines = 50, projectDir } = {}) {
|
|
|
33
33
|
|
|
34
34
|
const dashboard = buildDashboard(session, logResult.lines || [], { stories });
|
|
35
35
|
console.log(dashboard);
|
|
36
|
-
|
|
37
|
-
const proxyStats = getProxyStats();
|
|
38
|
-
if (proxyStats) {
|
|
39
|
-
console.log("");
|
|
40
|
-
console.log(`Proxy: port ${proxyStats.port}, ${proxyStats.requests} requests, ${proxyStats.bytes_in} bytes in, ${proxyStats.bytes_out} bytes out`);
|
|
41
|
-
}
|
|
42
36
|
}
|