karajan-code 1.58.1 → 1.59.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/package.json +1 -1
- package/src/agents/aider-agent.js +7 -0
- package/src/agents/claude-agent.js +3 -0
- 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/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/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/orchestrator.js +67 -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 +198 -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/reviewer-role.js +6 -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/display.js +44 -4
- package/src/utils/git.js +5 -0
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
|
|
@@ -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
|
* Safely parse a JSON line, returning null on failure.
|
|
@@ -134,6 +135,8 @@ function createStreamJsonFilter(onOutput) {
|
|
|
134
135
|
*/
|
|
135
136
|
function cleanExecaOpts(extra = {}) {
|
|
136
137
|
const { CLAUDECODE, ...env } = process.env;
|
|
138
|
+
const proxyEnv = getProxyEnv();
|
|
139
|
+
if (proxyEnv) Object.assign(env, proxyEnv);
|
|
137
140
|
return { env, stdin: "ignore", ...extra };
|
|
138
141
|
}
|
|
139
142
|
|
|
@@ -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/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;
|
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
|
+
}
|
|
@@ -8,6 +8,7 @@ import { runFlow, resumeFlow } from "../../orchestrator.js";
|
|
|
8
8
|
import { loadConfig, applyRunOverrides, validateConfig, resolveRole } from "../../config.js";
|
|
9
9
|
import { createLogger } from "../../utils/logger.js";
|
|
10
10
|
import { assertAgentsAvailable } from "../../agents/availability.js";
|
|
11
|
+
import { validatePolicyCompliance } from "../../guards/policy-guard.js";
|
|
11
12
|
import { createRunLog } from "../../utils/run-log.js";
|
|
12
13
|
import { buildProgressHandler, buildProgressNotifier, buildPipelineTracker } from "../progress.js";
|
|
13
14
|
import { isPreflightAcked, ackPreflight, getSessionOverrides } from "../preflight.js";
|
|
@@ -113,7 +114,7 @@ function collectRequiredProviders(config) {
|
|
|
113
114
|
|
|
114
115
|
export async function handleRunDirect(a, server, extra) {
|
|
115
116
|
const config = await buildConfig(a);
|
|
116
|
-
await assertNotOnBaseBranch(config);
|
|
117
|
+
await assertNotOnBaseBranch(config, { allowForRun: true });
|
|
117
118
|
const logger = createLogger(config.output.log_level, "mcp");
|
|
118
119
|
|
|
119
120
|
// Best-effort session cleanup before starting
|
|
@@ -203,7 +204,7 @@ const RESUME_INJECTION_PATTERNS = [
|
|
|
203
204
|
/force\s+(approve|merge|push|commit)\b/i,
|
|
204
205
|
];
|
|
205
206
|
|
|
206
|
-
export function validateResumeAnswer(answer) {
|
|
207
|
+
export function validateResumeAnswer(answer, pauseContext) {
|
|
207
208
|
if (answer == null || answer === "") return { valid: true, sanitized: answer ?? null };
|
|
208
209
|
if (typeof answer !== "string") return { valid: true, sanitized: String(answer) };
|
|
209
210
|
if (answer.length > RESUME_MAX_ANSWER_LENGTH) {
|
|
@@ -214,6 +215,14 @@ export function validateResumeAnswer(answer) {
|
|
|
214
215
|
return { valid: false, reason: "Answer rejected: matches guardrail bypass pattern" };
|
|
215
216
|
}
|
|
216
217
|
}
|
|
218
|
+
// Policy compliance: reject attempts to bypass TDD, reviewer, security, etc.
|
|
219
|
+
const policy = validatePolicyCompliance(answer, pauseContext);
|
|
220
|
+
if (!policy.valid) {
|
|
221
|
+
const suggestionsText = policy.suggestions.length > 0
|
|
222
|
+
? `\nValid alternatives:\n${policy.suggestions.map(s => ` - ${s}`).join("\n")}`
|
|
223
|
+
: "";
|
|
224
|
+
return { valid: false, reason: `${policy.reason}${suggestionsText}` };
|
|
225
|
+
}
|
|
217
226
|
return { valid: true, sanitized: answer.trim() };
|
|
218
227
|
}
|
|
219
228
|
|
|
@@ -240,7 +249,15 @@ export async function handleResume(a, server, extra) {
|
|
|
240
249
|
}
|
|
241
250
|
await runBootstrapGate(server, a);
|
|
242
251
|
if (a.answer) {
|
|
243
|
-
|
|
252
|
+
// Load session to get pause context for policy-aware validation
|
|
253
|
+
let pauseContext;
|
|
254
|
+
try {
|
|
255
|
+
const { loadSession } = await import("../../session-store.js");
|
|
256
|
+
const session = await loadSession(a.sessionId);
|
|
257
|
+
const ctx = session?.paused_state?.context;
|
|
258
|
+
pauseContext = typeof ctx === "string" ? ctx : ctx?.reason;
|
|
259
|
+
} catch { /* session read error — validate without context */ }
|
|
260
|
+
const validation = validateResumeAnswer(a.answer, pauseContext);
|
|
244
261
|
if (!validation.valid) {
|
|
245
262
|
return failPayload(`Resume answer rejected: ${validation.reason}`);
|
|
246
263
|
}
|
|
@@ -67,15 +67,17 @@ export function failPayload(message, details = {}) {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export async function assertNotOnBaseBranch(config) {
|
|
70
|
+
export async function assertNotOnBaseBranch(config, { allowForRun = false } = {}) {
|
|
71
71
|
const baseBranch = config?.base_branch || "main";
|
|
72
72
|
let branch;
|
|
73
73
|
try {
|
|
74
74
|
branch = await currentBranch();
|
|
75
75
|
} catch {
|
|
76
|
-
return;
|
|
76
|
+
return; // No git repo or no commits — bootstrap/orchestrator will handle it
|
|
77
77
|
}
|
|
78
78
|
if (branch === baseBranch) {
|
|
79
|
+
// kj_run creates its own branch via prepareGitAutomation — allow it
|
|
80
|
+
if (allowForRun) return;
|
|
79
81
|
throw new Error(
|
|
80
82
|
`You are on the base branch '${baseBranch}'. Karajan needs a feature branch to compute the diff for review. ` +
|
|
81
83
|
`Create a new branch first (e.g. 'git checkout -b feat/<task-description>') and then run this command again. ` +
|