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.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/src/agents/aider-agent.js +7 -0
  3. package/src/agents/claude-agent.js +3 -0
  4. package/src/agents/codex-agent.js +4 -0
  5. package/src/agents/gemini-agent.js +4 -0
  6. package/src/agents/opencode-agent.js +4 -0
  7. package/src/bootstrap.js +20 -9
  8. package/src/commands/doctor.js +19 -0
  9. package/src/commands/report.js +13 -0
  10. package/src/commands/status.js +7 -0
  11. package/src/config.js +35 -0
  12. package/src/git/automation.js +32 -4
  13. package/src/guards/policy-guard.js +90 -0
  14. package/src/mcp/handlers/run-handler.js +20 -3
  15. package/src/mcp/shared-helpers.js +4 -2
  16. package/src/orchestrator.js +67 -20
  17. package/src/prompts/coder.js +5 -4
  18. package/src/prompts/reviewer.js +5 -4
  19. package/src/prompts/rtk-snippet.js +12 -0
  20. package/src/proxy/adapters/anthropic.js +131 -0
  21. package/src/proxy/adapters/gemini.js +106 -0
  22. package/src/proxy/adapters/index.js +51 -0
  23. package/src/proxy/adapters/openai.js +71 -0
  24. package/src/proxy/compression/ai-compressor.js +209 -0
  25. package/src/proxy/compression/dedup-cache.js +100 -0
  26. package/src/proxy/compression/deterministic/bash-build.js +60 -0
  27. package/src/proxy/compression/deterministic/bash-git.js +101 -0
  28. package/src/proxy/compression/deterministic/bash-infra.js +64 -0
  29. package/src/proxy/compression/deterministic/bash-misc.js +53 -0
  30. package/src/proxy/compression/deterministic/bash-pkg.js +49 -0
  31. package/src/proxy/compression/deterministic/bash-test.js +61 -0
  32. package/src/proxy/compression/deterministic/glob.js +26 -0
  33. package/src/proxy/compression/deterministic/grep.js +53 -0
  34. package/src/proxy/compression/deterministic/index.js +58 -0
  35. package/src/proxy/compression/deterministic/read.js +22 -0
  36. package/src/proxy/compression/deterministic/utils.js +33 -0
  37. package/src/proxy/compression/pipeline.js +270 -0
  38. package/src/proxy/middleware/prompt-injector.js +165 -0
  39. package/src/proxy/middleware/response-interceptor.js +223 -0
  40. package/src/proxy/progress-monitor.js +98 -0
  41. package/src/proxy/proxy-lifecycle.js +198 -0
  42. package/src/proxy/proxy-server.js +239 -0
  43. package/src/proxy/security.js +45 -0
  44. package/src/review/diff-generator.js +10 -8
  45. package/src/roles/coder-role.js +1 -0
  46. package/src/roles/reviewer-role.js +6 -4
  47. package/src/session-cleanup.js +39 -15
  48. package/src/sonar/cloud-scanner.js +6 -3
  49. package/src/sonar/scanner.js +12 -7
  50. package/src/utils/display.js +44 -4
  51. package/src/utils/git.js +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.58.1",
3
+ "version": "1.59.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -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
- return {
46
- name: "gitRepo",
47
- ok,
48
- detail: ok ? "Inside a git repository" : "Not a git repository",
49
- fix: "Run 'git init' in your project directory."
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: false,
63
- detail: "origin remote not configured",
64
- fix: "Run 'git remote add origin <your-repo-url>'."
73
+ ok: true,
74
+ detail: "No remote configured (new project)",
75
+ fix: null
65
76
  };
66
77
  }
67
78
 
@@ -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
  }
@@ -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 }) {
@@ -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;
@@ -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 fetchBase(baseBranch);
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
- const validation = validateResumeAnswer(a.answer);
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. ` +