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.
Files changed (60) hide show
  1. package/package.json +1 -1
  2. package/src/agents/aider-agent.js +7 -0
  3. package/src/agents/claude-agent.js +36 -2
  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/cli.js +1 -0
  9. package/src/commands/doctor.js +19 -0
  10. package/src/commands/report.js +13 -0
  11. package/src/commands/status.js +7 -0
  12. package/src/config.js +35 -0
  13. package/src/domains/domain-loader.js +85 -4
  14. package/src/git/automation.js +32 -4
  15. package/src/guards/policy-guard.js +90 -0
  16. package/src/mcp/handlers/run-handler.js +20 -3
  17. package/src/mcp/shared-helpers.js +4 -2
  18. package/src/mcp/sovereignty-guard.js +1 -1
  19. package/src/mcp/tools.js +2 -1
  20. package/src/orchestrator.js +77 -20
  21. package/src/prompts/coder.js +5 -4
  22. package/src/prompts/reviewer.js +5 -4
  23. package/src/prompts/rtk-snippet.js +12 -0
  24. package/src/proxy/adapters/anthropic.js +131 -0
  25. package/src/proxy/adapters/gemini.js +106 -0
  26. package/src/proxy/adapters/index.js +51 -0
  27. package/src/proxy/adapters/openai.js +71 -0
  28. package/src/proxy/compression/ai-compressor.js +209 -0
  29. package/src/proxy/compression/dedup-cache.js +100 -0
  30. package/src/proxy/compression/deterministic/bash-build.js +60 -0
  31. package/src/proxy/compression/deterministic/bash-git.js +101 -0
  32. package/src/proxy/compression/deterministic/bash-infra.js +64 -0
  33. package/src/proxy/compression/deterministic/bash-misc.js +53 -0
  34. package/src/proxy/compression/deterministic/bash-pkg.js +49 -0
  35. package/src/proxy/compression/deterministic/bash-test.js +61 -0
  36. package/src/proxy/compression/deterministic/glob.js +26 -0
  37. package/src/proxy/compression/deterministic/grep.js +53 -0
  38. package/src/proxy/compression/deterministic/index.js +58 -0
  39. package/src/proxy/compression/deterministic/read.js +22 -0
  40. package/src/proxy/compression/deterministic/utils.js +33 -0
  41. package/src/proxy/compression/pipeline.js +270 -0
  42. package/src/proxy/middleware/prompt-injector.js +165 -0
  43. package/src/proxy/middleware/response-interceptor.js +223 -0
  44. package/src/proxy/progress-monitor.js +98 -0
  45. package/src/proxy/proxy-lifecycle.js +201 -0
  46. package/src/proxy/proxy-server.js +239 -0
  47. package/src/proxy/security.js +45 -0
  48. package/src/review/diff-generator.js +10 -8
  49. package/src/roles/coder-role.js +1 -0
  50. package/src/roles/impeccable-role.js +2 -4
  51. package/src/roles/researcher-role.js +2 -4
  52. package/src/roles/reviewer-role.js +10 -8
  53. package/src/roles/solomon-role.js +2 -4
  54. package/src/session-cleanup.js +39 -15
  55. package/src/sonar/cloud-scanner.js +6 -3
  56. package/src/sonar/scanner.js +12 -7
  57. package/src/utils/banner.js +66 -0
  58. package/src/utils/display.js +46 -7
  59. package/src/utils/git.js +5 -0
  60. package/src/utils/welcome.js +3 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.58.2",
3
+ "version": "1.59.1",
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
@@ -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
- 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
 
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)")
@@ -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;
@@ -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
- return Array.from(merged.values());
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
+ }
@@ -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
+ }