karajan-code 1.58.2 → 1.59.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/package.json +1 -1
- package/src/agents/aider-agent.js +7 -0
- package/src/agents/claude-agent.js +36 -2
- package/src/agents/codex-agent.js +4 -0
- package/src/agents/gemini-agent.js +4 -0
- package/src/agents/opencode-agent.js +4 -0
- package/src/bootstrap.js +20 -9
- package/src/cli.js +1 -0
- package/src/commands/doctor.js +19 -0
- package/src/commands/report.js +13 -0
- package/src/commands/status.js +7 -0
- package/src/config.js +35 -0
- package/src/domains/domain-loader.js +85 -4
- package/src/git/automation.js +32 -4
- package/src/guards/policy-guard.js +90 -0
- package/src/mcp/handlers/run-handler.js +20 -3
- package/src/mcp/shared-helpers.js +4 -2
- package/src/mcp/sovereignty-guard.js +1 -1
- package/src/mcp/tools.js +2 -1
- package/src/orchestrator.js +77 -20
- package/src/prompts/coder.js +5 -4
- package/src/prompts/reviewer.js +5 -4
- package/src/prompts/rtk-snippet.js +12 -0
- package/src/proxy/adapters/anthropic.js +131 -0
- package/src/proxy/adapters/gemini.js +106 -0
- package/src/proxy/adapters/index.js +51 -0
- package/src/proxy/adapters/openai.js +71 -0
- package/src/proxy/compression/ai-compressor.js +209 -0
- package/src/proxy/compression/dedup-cache.js +100 -0
- package/src/proxy/compression/deterministic/bash-build.js +60 -0
- package/src/proxy/compression/deterministic/bash-git.js +101 -0
- package/src/proxy/compression/deterministic/bash-infra.js +64 -0
- package/src/proxy/compression/deterministic/bash-misc.js +53 -0
- package/src/proxy/compression/deterministic/bash-pkg.js +49 -0
- package/src/proxy/compression/deterministic/bash-test.js +61 -0
- package/src/proxy/compression/deterministic/glob.js +26 -0
- package/src/proxy/compression/deterministic/grep.js +53 -0
- package/src/proxy/compression/deterministic/index.js +58 -0
- package/src/proxy/compression/deterministic/read.js +22 -0
- package/src/proxy/compression/deterministic/utils.js +33 -0
- package/src/proxy/compression/pipeline.js +270 -0
- package/src/proxy/middleware/prompt-injector.js +165 -0
- package/src/proxy/middleware/response-interceptor.js +223 -0
- package/src/proxy/progress-monitor.js +98 -0
- package/src/proxy/proxy-lifecycle.js +201 -0
- package/src/proxy/proxy-server.js +239 -0
- package/src/proxy/security.js +45 -0
- package/src/review/diff-generator.js +10 -8
- package/src/roles/coder-role.js +1 -0
- package/src/roles/impeccable-role.js +2 -4
- package/src/roles/researcher-role.js +2 -4
- package/src/roles/reviewer-role.js +10 -8
- package/src/roles/solomon-role.js +2 -4
- package/src/session-cleanup.js +39 -15
- package/src/sonar/cloud-scanner.js +6 -3
- package/src/sonar/scanner.js +12 -7
- package/src/utils/banner.js +66 -0
- package/src/utils/display.js +46 -7
- package/src/utils/git.js +5 -0
- package/src/utils/welcome.js +3 -8
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
|
|
5
6
|
export class AiderAgent extends BaseAgent {
|
|
6
7
|
async runTask(task) {
|
|
@@ -28,7 +29,13 @@ export class AiderAgent extends BaseAgent {
|
|
|
28
29
|
async _exec(task, model) {
|
|
29
30
|
const args = ["--yes", "--message", task.prompt];
|
|
30
31
|
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;
|
|
31
37
|
const res = await runCommand(resolveBin("aider"), args, {
|
|
38
|
+
...(env && { env }),
|
|
32
39
|
onOutput: task.onOutput,
|
|
33
40
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
34
41
|
timeout: task.timeoutMs
|
|
@@ -11,6 +11,37 @@ function tryParseJson(line) {
|
|
|
11
11
|
} catch { return null; }
|
|
12
12
|
}
|
|
13
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
|
+
|
|
14
45
|
/**
|
|
15
46
|
* Try to extract a result string from a parsed JSON object.
|
|
16
47
|
* Returns the result string or null if the object is not a result message.
|
|
@@ -134,6 +165,9 @@ function createStreamJsonFilter(onOutput) {
|
|
|
134
165
|
*/
|
|
135
166
|
function cleanExecaOpts(extra = {}) {
|
|
136
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.
|
|
137
171
|
return { env, stdin: "ignore", ...extra };
|
|
138
172
|
}
|
|
139
173
|
|
|
@@ -194,7 +228,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
194
228
|
const raw = pickOutput(res);
|
|
195
229
|
const output = extractTextFromStreamJson(raw);
|
|
196
230
|
const usage = extractUsageFromStreamJson(raw);
|
|
197
|
-
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode, ...usage };
|
|
231
|
+
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : sanitizeClaudeError(raw), exitCode: res.exitCode, ...usage };
|
|
198
232
|
}
|
|
199
233
|
|
|
200
234
|
// Without streaming, use json output to get structured response via stderr
|
|
@@ -203,7 +237,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
203
237
|
const raw = pickOutput(res);
|
|
204
238
|
const output = extractTextFromStreamJson(raw);
|
|
205
239
|
const usage = extractUsageFromStreamJson(raw);
|
|
206
|
-
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : raw, exitCode: res.exitCode, ...usage };
|
|
240
|
+
return { ok: res.exitCode === 0, output, error: res.exitCode === 0 ? "" : sanitizeClaudeError(raw), exitCode: res.exitCode, ...usage };
|
|
207
241
|
}
|
|
208
242
|
|
|
209
243
|
async _reviewTaskExec(task, model) {
|
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Extract token usage from Codex CLI stdout.
|
|
@@ -45,7 +46,10 @@ export class CodexAgent extends BaseAgent {
|
|
|
45
46
|
if (model) args.push("--model", model);
|
|
46
47
|
if (role !== "reviewer" && this.isAutoApproveEnabled(role)) args.push("--full-auto");
|
|
47
48
|
args.push("-");
|
|
49
|
+
const proxyEnv = getProxyEnv();
|
|
50
|
+
const env = proxyEnv ? { ...process.env, ...proxyEnv } : undefined;
|
|
48
51
|
const res = await runCommand(resolveBin("codex"), args, {
|
|
52
|
+
...(env && { env }),
|
|
49
53
|
onOutput: task.onOutput,
|
|
50
54
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
51
55
|
timeout: task.timeoutMs,
|
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
|
|
5
6
|
export class GeminiAgent extends BaseAgent {
|
|
6
7
|
async runTask(task) {
|
|
@@ -29,7 +30,10 @@ export class GeminiAgent extends BaseAgent {
|
|
|
29
30
|
const args = ["-p", task.prompt];
|
|
30
31
|
if (mode === "review") args.push("--output-format", "json");
|
|
31
32
|
if (model) args.push("--model", model);
|
|
33
|
+
const proxyEnv = getProxyEnv();
|
|
34
|
+
const env = proxyEnv ? { ...process.env, ...proxyEnv } : undefined;
|
|
32
35
|
const res = await runCommand(resolveBin("gemini"), args, {
|
|
36
|
+
...(env && { env }),
|
|
33
37
|
onOutput: task.onOutput,
|
|
34
38
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
35
39
|
timeout: task.timeoutMs
|
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
|
|
5
6
|
export class OpenCodeAgent extends BaseAgent {
|
|
6
7
|
async runTask(task) {
|
|
@@ -30,7 +31,10 @@ export class OpenCodeAgent extends BaseAgent {
|
|
|
30
31
|
if (jsonFormat) args.push("--format", "json");
|
|
31
32
|
if (model) args.push("--model", model);
|
|
32
33
|
args.push(task.prompt);
|
|
34
|
+
const proxyEnv = getProxyEnv();
|
|
35
|
+
const env = proxyEnv ? { ...process.env, ...proxyEnv } : undefined;
|
|
33
36
|
const res = await runCommand(resolveBin("opencode"), args, {
|
|
37
|
+
...(env && { env }),
|
|
34
38
|
onOutput: task.onOutput,
|
|
35
39
|
silenceTimeoutMs: task.silenceTimeoutMs,
|
|
36
40
|
timeout: task.timeoutMs
|
package/src/bootstrap.js
CHANGED
|
@@ -42,12 +42,22 @@ async function checkGitRepo() {
|
|
|
42
42
|
} catch { /* git may not be installed */
|
|
43
43
|
ok = false;
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
|
|
46
|
+
if (!ok) {
|
|
47
|
+
// Auto-init: initialize git repo for new projects
|
|
48
|
+
const initResult = await runCommand("git", ["init"]);
|
|
49
|
+
if (initResult.exitCode !== 0) {
|
|
50
|
+
return {
|
|
51
|
+
name: "gitRepo",
|
|
52
|
+
ok: false,
|
|
53
|
+
detail: "Not a git repository and git init failed",
|
|
54
|
+
fix: "Ensure git is installed and you have write permissions to this directory."
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { name: "gitRepo", ok: true, detail: "Initialized new git repository", fix: null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { name: "gitRepo", ok: true, detail: "Inside a git repository", fix: null };
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
async function checkGitRemote() {
|
|
@@ -57,11 +67,12 @@ async function checkGitRemote() {
|
|
|
57
67
|
return { name: "gitRemote", ok: true, detail: res.stdout.trim(), fix: null };
|
|
58
68
|
}
|
|
59
69
|
} catch { /* fall through */ }
|
|
70
|
+
// No remote is acceptable for new projects — non-blocking
|
|
60
71
|
return {
|
|
61
72
|
name: "gitRemote",
|
|
62
|
-
ok:
|
|
63
|
-
detail: "
|
|
64
|
-
fix:
|
|
73
|
+
ok: true,
|
|
74
|
+
detail: "No remote configured (new project)",
|
|
75
|
+
fix: null
|
|
65
76
|
};
|
|
66
77
|
}
|
|
67
78
|
|
package/src/cli.js
CHANGED
|
@@ -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/doctor.js
CHANGED
|
@@ -12,6 +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
|
+
import { isProxyRunning } from "../proxy/proxy-lifecycle.js";
|
|
15
16
|
|
|
16
17
|
function getPackageVersion() {
|
|
17
18
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
@@ -342,6 +343,23 @@ async function checkAgentConfigs() {
|
|
|
342
343
|
return checks;
|
|
343
344
|
}
|
|
344
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
|
+
|
|
345
363
|
export async function runChecks({ config }) {
|
|
346
364
|
const checks = [];
|
|
347
365
|
|
|
@@ -366,6 +384,7 @@ export async function runChecks({ config }) {
|
|
|
366
384
|
checks.push(...await checkAgentConfigs());
|
|
367
385
|
checks.push(...await checkRuleFiles(config));
|
|
368
386
|
checks.push(await checkRtk());
|
|
387
|
+
checks.push(await checkProxy(config));
|
|
369
388
|
|
|
370
389
|
return checks;
|
|
371
390
|
}
|
package/src/commands/report.js
CHANGED
|
@@ -153,6 +153,7 @@ 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;
|
|
156
157
|
return report;
|
|
157
158
|
}
|
|
158
159
|
|
|
@@ -199,6 +200,7 @@ function printTextReport(report) {
|
|
|
199
200
|
console.log(formatBudgetText(report.budget_consumed));
|
|
200
201
|
console.log("Commits Generated:");
|
|
201
202
|
console.log(formatCommitsText(report.commits_generated));
|
|
203
|
+
printProxyStats(report.proxy_stats);
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
function formatDuration(ms) {
|
|
@@ -327,6 +329,16 @@ async function resolveTraceOptions(currency) {
|
|
|
327
329
|
return { cur, rate };
|
|
328
330
|
}
|
|
329
331
|
|
|
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
|
+
|
|
330
342
|
function printRtkSavings(rtkSavings) {
|
|
331
343
|
if (!rtkSavings || !rtkSavings.callCount) return;
|
|
332
344
|
const tokens = rtkSavings.estimatedTokensSaved ?? 0;
|
|
@@ -345,6 +357,7 @@ function printTraceReport(report, currency, exchangeRate) {
|
|
|
345
357
|
console.log("");
|
|
346
358
|
printTraceTable(report.budget_trace, { currency, exchangeRate });
|
|
347
359
|
printRtkSavings(report.rtk_savings);
|
|
360
|
+
printProxyStats(report.proxy_stats);
|
|
348
361
|
}
|
|
349
362
|
|
|
350
363
|
async function handlePgTaskReport({ dir, pgTask, list, sessionId, format, trace, currency }) {
|
package/src/commands/status.js
CHANGED
|
@@ -5,6 +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
|
+
import { getProxyStats } from "../proxy/proxy-lifecycle.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Run the `kj status` command: print a human-readable dashboard to stdout.
|
|
@@ -32,4 +33,10 @@ export async function statusCommand({ lines = 50, projectDir } = {}) {
|
|
|
32
33
|
|
|
33
34
|
const dashboard = buildDashboard(session, logResult.lines || [], { stories });
|
|
34
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
|
+
}
|
|
35
42
|
}
|
package/src/config.js
CHANGED
|
@@ -172,6 +172,37 @@ const DEFAULTS = {
|
|
|
172
172
|
thresholds: { lcp: 2500, cls: 0.1, inp: 200 }
|
|
173
173
|
},
|
|
174
174
|
telemetry: true,
|
|
175
|
+
proxy: {
|
|
176
|
+
enabled: true,
|
|
177
|
+
port: "auto",
|
|
178
|
+
compression: {
|
|
179
|
+
enabled: true,
|
|
180
|
+
ai_compression: false,
|
|
181
|
+
ai_model: "haiku",
|
|
182
|
+
ai_provider: "anthropic",
|
|
183
|
+
layers: {
|
|
184
|
+
git: true,
|
|
185
|
+
tests: true,
|
|
186
|
+
build: true,
|
|
187
|
+
infra: true,
|
|
188
|
+
packages: true,
|
|
189
|
+
read_dedup: true,
|
|
190
|
+
glob_truncate: true,
|
|
191
|
+
grep_collapse: true,
|
|
192
|
+
},
|
|
193
|
+
pressure_thresholds: {
|
|
194
|
+
low: 0.5,
|
|
195
|
+
medium: 0.8,
|
|
196
|
+
high: 0.9,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
cache: {
|
|
200
|
+
persist_to_disk: true,
|
|
201
|
+
flush_interval_ms: 5000,
|
|
202
|
+
},
|
|
203
|
+
inject_prompts: true,
|
|
204
|
+
monitor: true,
|
|
205
|
+
},
|
|
175
206
|
guards: {
|
|
176
207
|
output: {
|
|
177
208
|
enabled: true,
|
|
@@ -401,6 +432,10 @@ function applyMiscOverrides(out, flags) {
|
|
|
401
432
|
if (flags.pgTask) out.planning_game.enabled = true;
|
|
402
433
|
if (flags.pgProject) out.planning_game.project_id = flags.pgProject;
|
|
403
434
|
|
|
435
|
+
out.proxy = out.proxy || {};
|
|
436
|
+
if (flags.noProxy) out.proxy.enabled = false;
|
|
437
|
+
if (flags.proxyPort) out.proxy.port = flags.proxyPort;
|
|
438
|
+
|
|
404
439
|
out.model_selection = out.model_selection || { enabled: true, tiers: {}, role_overrides: {} };
|
|
405
440
|
if (flags.smartModels === true) out.model_selection.enabled = true;
|
|
406
441
|
if (flags.smartModels === false || flags.noSmartModels === true) out.model_selection.enabled = false;
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Project-local domains override user-global domains by directory name.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
9
|
-
import { join } from "node:path";
|
|
8
|
+
import { readdir, readFile, mkdir, writeFile, stat } from "node:fs/promises";
|
|
9
|
+
import { join, isAbsolute } from "node:path";
|
|
10
10
|
import yaml from "js-yaml";
|
|
11
11
|
import { getKarajanHome } from "../utils/paths.js";
|
|
12
12
|
|
|
@@ -103,8 +103,52 @@ function parseSections(content) {
|
|
|
103
103
|
return sections;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Auto-detect project context files (README.md, CLAUDE.md) as lightweight domains.
|
|
108
|
+
* Used as fallback when no explicit .karajan/domains/ are configured.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} projectDir — absolute path to project root
|
|
111
|
+
* @returns {Promise<DomainFile[]>}
|
|
112
|
+
*/
|
|
113
|
+
export async function autoDetectDomains(projectDir) {
|
|
114
|
+
if (!projectDir) return [];
|
|
115
|
+
|
|
116
|
+
const candidates = ["README.md", "CLAUDE.md", "docs/README.md"];
|
|
117
|
+
const detected = [];
|
|
118
|
+
|
|
119
|
+
for (const rel of candidates) {
|
|
120
|
+
const filePath = join(projectDir, rel);
|
|
121
|
+
let content;
|
|
122
|
+
try {
|
|
123
|
+
content = await readFile(filePath, "utf-8");
|
|
124
|
+
} catch { continue; }
|
|
125
|
+
|
|
126
|
+
if (!content || content.trim().length < 20) continue;
|
|
127
|
+
|
|
128
|
+
// Truncate to first 4000 chars to avoid bloating context
|
|
129
|
+
const trimmed = content.length > 4000 ? content.slice(0, 4000) + "\n\n[...truncated]" : content;
|
|
130
|
+
|
|
131
|
+
detected.push({
|
|
132
|
+
name: `auto:${rel}`,
|
|
133
|
+
description: `Auto-detected from ${rel}`,
|
|
134
|
+
tags: [],
|
|
135
|
+
version: "0.0.0",
|
|
136
|
+
author: "",
|
|
137
|
+
visibility: "private",
|
|
138
|
+
sources: [{ type: "auto-detect", note: rel }],
|
|
139
|
+
content: trimmed,
|
|
140
|
+
sections: parseSections(trimmed),
|
|
141
|
+
filePath,
|
|
142
|
+
origin: "auto-detect"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return detected;
|
|
147
|
+
}
|
|
148
|
+
|
|
106
149
|
/**
|
|
107
150
|
* Load all domains from project-local and user-global directories.
|
|
151
|
+
* Falls back to auto-detecting README.md/CLAUDE.md when no explicit domains found.
|
|
108
152
|
* Project-local domains override user-global domains when directory names match.
|
|
109
153
|
*
|
|
110
154
|
* @param {string|null} projectDir — absolute path to project root (null = user-global only)
|
|
@@ -119,7 +163,6 @@ export async function loadDomains(projectDir) {
|
|
|
119
163
|
const projectDomains = projectDomainDir ? await scanDomainDir(projectDomainDir, "project") : [];
|
|
120
164
|
|
|
121
165
|
// Build a map keyed by directory name for merge.
|
|
122
|
-
// We track by directory name (not domain name) because the override is by directory.
|
|
123
166
|
const merged = new Map();
|
|
124
167
|
|
|
125
168
|
for (const { dirName, domain } of userDomains) {
|
|
@@ -129,7 +172,14 @@ export async function loadDomains(projectDir) {
|
|
|
129
172
|
merged.set(dirName, domain); // project overrides user
|
|
130
173
|
}
|
|
131
174
|
|
|
132
|
-
|
|
175
|
+
const explicit = Array.from(merged.values());
|
|
176
|
+
|
|
177
|
+
// Fallback: auto-detect README.md, CLAUDE.md when no explicit domains found
|
|
178
|
+
if (explicit.length === 0 && projectDir) {
|
|
179
|
+
return autoDetectDomains(projectDir);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return explicit;
|
|
133
183
|
}
|
|
134
184
|
|
|
135
185
|
/**
|
|
@@ -162,3 +212,34 @@ async function scanDomainDir(dir, origin) {
|
|
|
162
212
|
|
|
163
213
|
return results;
|
|
164
214
|
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Persist an inline domain (text or file path) to .karajan/domains/inline/DOMAIN.md.
|
|
218
|
+
* If domainInput is a path to an existing .md file, reads its content.
|
|
219
|
+
* Otherwise treats it as inline text.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} domainInput — inline text or absolute path to a .md file
|
|
222
|
+
* @param {string} projectDir — project root
|
|
223
|
+
*/
|
|
224
|
+
export async function persistInlineDomain(domainInput, projectDir) {
|
|
225
|
+
if (!domainInput || !projectDir) return;
|
|
226
|
+
|
|
227
|
+
let content = domainInput;
|
|
228
|
+
|
|
229
|
+
// If it looks like a file path, try to read it
|
|
230
|
+
if (isAbsolute(domainInput) && domainInput.endsWith(".md")) {
|
|
231
|
+
try {
|
|
232
|
+
const s = await stat(domainInput);
|
|
233
|
+
if (s.isFile()) {
|
|
234
|
+
content = await readFile(domainInput, "utf-8");
|
|
235
|
+
}
|
|
236
|
+
} catch { /* treat as inline text */ }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const domainDir = join(projectDir, ".karajan", "domains", "inline");
|
|
240
|
+
await mkdir(domainDir, { recursive: true });
|
|
241
|
+
|
|
242
|
+
const domainFile = join(domainDir, DOMAIN_FILE);
|
|
243
|
+
const wrapped = `---\nname: inline-domain\ndescription: Domain knowledge provided via --domain parameter\ntags: []\nversion: "0.0.0"\n---\n\n${content}\n`;
|
|
244
|
+
await writeFile(domainFile, wrapped, "utf-8");
|
|
245
|
+
}
|
package/src/git/automation.js
CHANGED
|
@@ -7,6 +7,7 @@ import { addCheckpoint } from "../session-store.js";
|
|
|
7
7
|
import {
|
|
8
8
|
ensureGitRepo,
|
|
9
9
|
currentBranch,
|
|
10
|
+
hasCommits,
|
|
10
11
|
fetchBase,
|
|
11
12
|
syncBaseBranch,
|
|
12
13
|
ensureBranchUpToDateWithBase,
|
|
@@ -34,18 +35,45 @@ export async function prepareGitAutomation({ config, task, logger, session }) {
|
|
|
34
35
|
|
|
35
36
|
const baseBranch = config.base_branch;
|
|
36
37
|
const autoRebase = config.git.auto_rebase !== false;
|
|
37
|
-
await
|
|
38
|
+
const repoHasCommits = await hasCommits();
|
|
39
|
+
|
|
40
|
+
// New repo without commits: create branch directly (no fetch/sync possible)
|
|
41
|
+
if (!repoHasCommits) {
|
|
42
|
+
const created = buildBranchName(config.git.branch_prefix || "feat/", task);
|
|
43
|
+
// git checkout -b works even without commits (creates orphan-like branch on first commit)
|
|
44
|
+
await createBranch(created);
|
|
45
|
+
logger.info(`New repo — created working branch: ${created}`);
|
|
46
|
+
await addCheckpoint(session, { stage: "git-prep", branch: created, created: true, newRepo: true });
|
|
47
|
+
return { enabled: true, branch: created, baseBranch, autoRebase };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await fetchBase(baseBranch).catch(() => {
|
|
51
|
+
// No remote — skip fetch (new project without push)
|
|
52
|
+
logger.info("No remote configured — skipping fetch");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
let branch;
|
|
56
|
+
try {
|
|
57
|
+
branch = await currentBranch();
|
|
58
|
+
} catch {
|
|
59
|
+
// HEAD exists but branch detection failed — unusual, treat as main
|
|
60
|
+
branch = baseBranch;
|
|
61
|
+
}
|
|
38
62
|
|
|
39
|
-
let branch = await currentBranch();
|
|
40
63
|
if (branch === baseBranch) {
|
|
41
|
-
await syncBaseBranch({ baseBranch, autoRebase })
|
|
64
|
+
await syncBaseBranch({ baseBranch, autoRebase }).catch(() => {
|
|
65
|
+
// No remote tracking — skip sync for new projects
|
|
66
|
+
});
|
|
42
67
|
const created = buildBranchName(config.git.branch_prefix || "feat/", task);
|
|
43
68
|
await createBranch(created);
|
|
44
69
|
branch = created;
|
|
45
70
|
logger.info(`Created working branch: ${branch}`);
|
|
46
71
|
await addCheckpoint(session, { stage: "git-prep", branch, created: true });
|
|
47
72
|
} else {
|
|
48
|
-
await ensureBranchUpToDateWithBase({ branch, baseBranch, autoRebase })
|
|
73
|
+
await ensureBranchUpToDateWithBase({ branch, baseBranch, autoRebase }).catch(() => {
|
|
74
|
+
// No remote tracking — skip rebase for new projects
|
|
75
|
+
logger.info("No remote tracking — skipping rebase check");
|
|
76
|
+
});
|
|
49
77
|
await addCheckpoint(session, { stage: "git-prep", branch, created: false });
|
|
50
78
|
}
|
|
51
79
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy guard for kj_resume answers.
|
|
3
|
+
*
|
|
4
|
+
* Detects host-AI attempts to bypass pipeline policies (skip TDD, ignore
|
|
5
|
+
* reviewer, disable security, etc.) and rejects them with context-aware
|
|
6
|
+
* suggestions for valid alternatives.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BYPASS_PATTERNS = [
|
|
10
|
+
{ regex: /\b(?:skip|omit|drop)\s+(?:the\s+)?(?:tests?|tdd|testing)\b/i, policy: "tdd" },
|
|
11
|
+
{ regex: /\bdisable\s+(?:the\s+)?(?:tests?|tdd|testing)\b/i, policy: "tdd" },
|
|
12
|
+
{ regex: /\b(?:don'?t|do\s+not|no\s+need\s+(?:for|to))\s+(?:run\s+)?tests?\b/i, policy: "tdd" },
|
|
13
|
+
{ regex: /\bcontinue\s+without\s+test/i, policy: "tdd" },
|
|
14
|
+
{ regex: /\bswitch\s+(?:to\s+)?(?:standard\s+)?methodology/i, policy: "tdd" },
|
|
15
|
+
{ regex: /\bswitch\s+methodology/i, policy: "tdd" },
|
|
16
|
+
{ regex: /\b(?:skip|ignore|bypass|disable)\s+(?:the\s+)?(?:review(?:er)?|code\s+review)\b/i, policy: "reviewer" },
|
|
17
|
+
{ regex: /\b(?:approve|pass)\s+without\s+review/i, policy: "reviewer" },
|
|
18
|
+
{ regex: /\bmark\s+as\s+approved\b/i, policy: "reviewer" },
|
|
19
|
+
{ regex: /\bjust\s+ship\s+it/i, policy: "reviewer" },
|
|
20
|
+
{ regex: /\bignore\s+(?:the\s+)?(?:review(?:er)?\s+)?feedback\b/i, policy: "reviewer" },
|
|
21
|
+
{ regex: /\b(?:skip|ignore|bypass|disable)\s+(?:the\s+)?(?:sonar|sonarqube|quality\s+gate)/i, policy: "sonar" },
|
|
22
|
+
{ regex: /\bignore\s+sonar\s+issues?\b/i, policy: "sonar" },
|
|
23
|
+
{ regex: /\b(?:skip|ignore|bypass|disable)\s+(?:the\s+)?security(?:\s+checks?)?\b/i, policy: "security" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const SUGGESTIONS_BY_POLICY = {
|
|
27
|
+
tdd: [
|
|
28
|
+
"Provide specific guidance for the coder on how to fix the failing tests",
|
|
29
|
+
"Describe the expected behavior so the coder can write correct tests",
|
|
30
|
+
"Suggest a different approach to implement the feature that is easier to test",
|
|
31
|
+
],
|
|
32
|
+
reviewer: [
|
|
33
|
+
"Provide technical guidance to address the reviewer's feedback",
|
|
34
|
+
"Explain why the current approach is correct if you disagree with the review",
|
|
35
|
+
"Suggest alternative implementations that satisfy the reviewer's concerns",
|
|
36
|
+
],
|
|
37
|
+
sonar: [
|
|
38
|
+
"Provide guidance on how to resolve the SonarQube issues",
|
|
39
|
+
"Explain the code changes needed to satisfy quality gate requirements",
|
|
40
|
+
],
|
|
41
|
+
security: [
|
|
42
|
+
"Provide guidance on how to fix the security issues found",
|
|
43
|
+
"Explain the secure alternative for the flagged pattern",
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const CONTEXT_SUGGESTIONS = {
|
|
48
|
+
reviewer_fail_fast: [
|
|
49
|
+
"Provide specific technical guidance for the coder to address the reviewer's feedback",
|
|
50
|
+
"Suggest a different approach that resolves the reviewer's concerns",
|
|
51
|
+
],
|
|
52
|
+
max_iterations: [
|
|
53
|
+
"Provide focused guidance to help the coder converge on a solution",
|
|
54
|
+
"Narrow the scope: specify exactly which file/function to change and how",
|
|
55
|
+
"Continue with additional iterations by choosing option 1 or 2",
|
|
56
|
+
],
|
|
57
|
+
standby_exhausted: [
|
|
58
|
+
"Wait and retry — the provider may be temporarily overloaded",
|
|
59
|
+
"Continue to resume from where the pipeline left off",
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate a resume answer against pipeline policies.
|
|
65
|
+
*
|
|
66
|
+
* @param {string|null} answer - The resume answer from the host
|
|
67
|
+
* @param {string} [pauseContext] - The context of the pause (e.g. "reviewer_fail_fast")
|
|
68
|
+
* @returns {{ valid: boolean, reason?: string, suggestions?: string[] }}
|
|
69
|
+
*/
|
|
70
|
+
export function validatePolicyCompliance(answer, pauseContext) {
|
|
71
|
+
if (answer == null || answer === "") return { valid: true };
|
|
72
|
+
if (typeof answer !== "string") return { valid: true };
|
|
73
|
+
|
|
74
|
+
const text = answer.trim();
|
|
75
|
+
for (const { regex, policy } of BYPASS_PATTERNS) {
|
|
76
|
+
if (regex.test(text)) {
|
|
77
|
+
const suggestions = [
|
|
78
|
+
...(CONTEXT_SUGGESTIONS[pauseContext] || []),
|
|
79
|
+
...(SUGGESTIONS_BY_POLICY[policy] || []),
|
|
80
|
+
];
|
|
81
|
+
return {
|
|
82
|
+
valid: false,
|
|
83
|
+
reason: `Pipeline policy "${policy}" cannot be bypassed. Provide technical guidance instead.`,
|
|
84
|
+
suggestions: [...new Set(suggestions)],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { valid: true };
|
|
90
|
+
}
|