karajan-code 1.59.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/scripts/install.js +18 -15
  4. package/src/agents/aider-agent.js +0 -7
  5. package/src/agents/claude-agent.js +34 -6
  6. package/src/agents/codex-agent.js +2 -6
  7. package/src/agents/gemini-agent.js +0 -4
  8. package/src/agents/opencode-agent.js +0 -4
  9. package/src/{becaria → ci}/dispatch.js +13 -13
  10. package/src/{becaria → ci}/pr-diff.js +1 -1
  11. package/src/cli.js +3 -2
  12. package/src/commands/agents.js +0 -1
  13. package/src/commands/doctor.js +22 -40
  14. package/src/commands/init.js +8 -8
  15. package/src/commands/report.js +0 -13
  16. package/src/commands/review.js +17 -17
  17. package/src/commands/run.js +1 -2
  18. package/src/commands/status.js +1 -7
  19. package/src/config.js +10 -52
  20. package/src/domains/domain-loader.js +85 -4
  21. package/src/git/automation.js +9 -9
  22. package/src/guards/output-guard.js +0 -17
  23. package/src/mcp/handlers/direct-handlers.js +1 -1
  24. package/src/mcp/handlers/run-handler.js +1 -2
  25. package/src/mcp/sovereignty-guard.js +2 -2
  26. package/src/mcp/tools.js +3 -2
  27. package/src/orchestrator/brain-coordinator.js +165 -0
  28. package/src/orchestrator/ci-integration.js +139 -0
  29. package/src/orchestrator/config-init.js +435 -0
  30. package/src/orchestrator/direct-actions.js +186 -0
  31. package/src/orchestrator/feedback-enrichment.js +140 -0
  32. package/src/orchestrator/feedback-queue.js +152 -0
  33. package/src/orchestrator/flow-control.js +159 -0
  34. package/src/orchestrator/post-loop-stages.js +20 -15
  35. package/src/orchestrator/preflight-checks.js +7 -3
  36. package/src/orchestrator/role-output-compressor.js +190 -0
  37. package/src/orchestrator/session-journal.js +344 -0
  38. package/src/orchestrator/solomon-escalation.js +38 -5
  39. package/src/orchestrator/solomon-rules.js +32 -33
  40. package/src/orchestrator/stages/reviewer-stage.js +16 -5
  41. package/src/orchestrator/stages/sonar-stage.js +24 -1
  42. package/src/orchestrator/verification-gate.js +146 -0
  43. package/src/orchestrator.js +435 -661
  44. package/src/planning-game/client.js +2 -2
  45. package/src/planning-game/pipeline-adapter.js +1 -1
  46. package/src/prompts/coder.js +17 -3
  47. package/src/prompts/reviewer.js +2 -2
  48. package/src/prompts/rtk-snippet.js +3 -5
  49. package/src/review/diff-generator.js +66 -17
  50. package/src/review/snapshot-diff.js +122 -0
  51. package/src/review/tdd-policy.js +4 -2
  52. package/src/roles/agent-role.js +135 -0
  53. package/src/roles/architect-role.js +61 -108
  54. package/src/roles/audit-role.js +25 -71
  55. package/src/roles/coder-role.js +32 -57
  56. package/src/roles/discover-role.js +30 -71
  57. package/src/roles/hu-reviewer-role.js +51 -99
  58. package/src/roles/impeccable-role.js +34 -91
  59. package/src/roles/index.js +2 -0
  60. package/src/roles/karajan-brain-role.js +134 -0
  61. package/src/roles/planner-role.js +40 -75
  62. package/src/roles/refactorer-role.js +22 -64
  63. package/src/roles/researcher-role.js +52 -118
  64. package/src/roles/reviewer-role.js +69 -126
  65. package/src/roles/security-role.js +38 -102
  66. package/src/roles/solomon-role.js +71 -15
  67. package/src/roles/tester-role.js +121 -95
  68. package/src/roles/triage-role.js +50 -111
  69. package/src/session-store.js +1 -1
  70. package/src/skills/skill-detector.js +11 -0
  71. package/src/sonar/config-resolver.js +20 -1
  72. package/src/sonar/credentials.js +17 -0
  73. package/src/sonar/project-key.js +1 -1
  74. package/src/sonar/scanner.js +1 -1
  75. package/src/utils/banner.js +66 -0
  76. package/src/utils/display.js +35 -17
  77. package/src/utils/injection-guard.js +23 -8
  78. package/src/utils/logger.js +6 -1
  79. package/src/utils/process.js +3 -4
  80. package/src/utils/role-assigner.js +131 -0
  81. package/src/utils/update-check.js +5 -1
  82. package/src/utils/welcome.js +3 -8
  83. package/src/webperf/devtools-detect.js +70 -23
  84. package/templates/roles/coder.md +7 -0
  85. package/templates/roles/karajan-brain.md +95 -0
  86. package/templates/roles/solomon.md +67 -106
  87. package/templates/workflows/automerge.yml +4 -4
  88. package/templates/workflows/houston-override.yml +4 -4
  89. package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
  90. package/src/proxy/adapters/anthropic.js +0 -131
  91. package/src/proxy/adapters/gemini.js +0 -106
  92. package/src/proxy/adapters/index.js +0 -51
  93. package/src/proxy/adapters/openai.js +0 -71
  94. package/src/proxy/compression/ai-compressor.js +0 -209
  95. package/src/proxy/compression/dedup-cache.js +0 -100
  96. package/src/proxy/compression/deterministic/bash-build.js +0 -60
  97. package/src/proxy/compression/deterministic/bash-git.js +0 -101
  98. package/src/proxy/compression/deterministic/bash-infra.js +0 -64
  99. package/src/proxy/compression/deterministic/bash-misc.js +0 -53
  100. package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
  101. package/src/proxy/compression/deterministic/bash-test.js +0 -61
  102. package/src/proxy/compression/deterministic/glob.js +0 -26
  103. package/src/proxy/compression/deterministic/grep.js +0 -53
  104. package/src/proxy/compression/deterministic/index.js +0 -58
  105. package/src/proxy/compression/deterministic/read.js +0 -22
  106. package/src/proxy/compression/deterministic/utils.js +0 -33
  107. package/src/proxy/compression/pipeline.js +0 -270
  108. package/src/proxy/middleware/prompt-injector.js +0 -165
  109. package/src/proxy/middleware/response-interceptor.js +0 -223
  110. package/src/proxy/progress-monitor.js +0 -98
  111. package/src/proxy/proxy-lifecycle.js +0 -198
  112. package/src/proxy/proxy-server.js +0 -239
  113. package/src/proxy/security.js +0 -45
  114. /package/src/{becaria → ci}/index.js +0 -0
  115. /package/src/{becaria → ci}/repo.js +0 -0
@@ -1,5 +1,4 @@
1
- import { BaseRole } from "./base-role.js";
2
- import { createAgent as defaultCreateAgent } from "../agents/index.js";
1
+ import { AgentRole } from "./agent-role.js";
3
2
  import { buildTriagePrompt } from "../prompts/triage.js";
4
3
  import { VALID_TASK_TYPES } from "../guards/policy-resolver.js";
5
4
  import { extractFirstJson } from "../utils/json-extract.js";
@@ -7,18 +6,7 @@ import { extractFirstJson } from "../utils/json-extract.js";
7
6
  const VALID_LEVELS = new Set(["trivial", "simple", "medium", "complex"]);
8
7
  const VALID_ROLES = new Set(["planner", "researcher", "refactorer", "reviewer", "tester", "security", "impeccable"]);
9
8
  const FALLBACK_TASK_TYPE = "sw";
10
-
11
- function resolveProvider(config) {
12
- return (
13
- config?.roles?.triage?.provider ||
14
- config?.roles?.coder?.provider ||
15
- "claude"
16
- );
17
- }
18
-
19
- function parseTriageOutput(raw) {
20
- return extractFirstJson(raw);
21
- }
9
+ const FALLBACK_RESULT = { level: "medium", roles: ["reviewer"], taskType: FALLBACK_TASK_TYPE, shouldDecompose: false };
22
10
 
23
11
  function normalizeRoles(roles) {
24
12
  if (!Array.isArray(roles)) return [];
@@ -27,115 +15,66 @@ function normalizeRoles(roles) {
27
15
 
28
16
  function normalizeSubtasks(subtasks) {
29
17
  if (!Array.isArray(subtasks)) return [];
30
- return subtasks
31
- .map((s) => (typeof s === "string" ? s.trim() : ""))
32
- .filter(Boolean)
33
- .slice(0, 5);
18
+ return subtasks.map((s) => (typeof s === "string" ? s.trim() : "")).filter(Boolean).slice(0, 5);
34
19
  }
35
20
 
36
21
  function normalizeDomainHints(hints) {
37
22
  if (!Array.isArray(hints)) return [];
38
- return hints
39
- .map((h) => (typeof h === "string" ? h.trim().toLowerCase() : ""))
40
- .filter(Boolean);
23
+ return hints.map((h) => (typeof h === "string" ? h.trim().toLowerCase() : "")).filter(Boolean);
41
24
  }
42
25
 
43
- export class TriageRole extends BaseRole {
44
- constructor({ config, logger, emitter = null, createAgentFn = null }) {
45
- super({ name: "triage", config, logger, emitter });
46
- this._createAgent = createAgentFn || defaultCreateAgent;
26
+ export class TriageRole extends AgentRole {
27
+ constructor(opts) {
28
+ super({ ...opts, name: "triage" });
47
29
  }
48
30
 
49
- async execute(input) {
50
- const task = typeof input === "string"
51
- ? input
52
- : input?.task || this.context?.task || "";
53
- const onOutput = typeof input === "string" ? null : input?.onOutput || null;
54
-
55
- const provider = resolveProvider(this.config);
56
- const agent = this._createAgent(provider, this.config, this.logger);
57
-
58
- const prompt = buildTriagePrompt({ task, instructions: this.instructions });
59
- const runArgs = { prompt, role: "triage" };
60
- if (onOutput) runArgs.onOutput = onOutput;
61
- const result = await agent.runTask(runArgs);
31
+ async buildPrompt({ task }) {
32
+ return { prompt: buildTriagePrompt({ task, instructions: this.instructions }) };
33
+ }
62
34
 
63
- if (!result.ok) {
64
- return {
65
- ok: false,
66
- result: {
67
- error: result.error || result.output || "Triage failed",
68
- provider
69
- },
70
- summary: `Triage failed: ${result.error || "unknown error"}`,
71
- usage: result.usage
72
- };
35
+ parseOutput(raw) { return extractFirstJson(raw); }
36
+
37
+ buildSuccessResult(parsed, provider) {
38
+ const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
39
+ const roles = normalizeRoles(parsed.roles);
40
+ const reasoning = String(parsed.reasoning || "").trim() || "No reasoning provided.";
41
+ const shouldDecompose = Boolean(parsed.shouldDecompose);
42
+ const subtasks = normalizeSubtasks(parsed.subtasks);
43
+ const taskType = VALID_TASK_TYPES.has(parsed.taskType) ? parsed.taskType : FALLBACK_TASK_TYPE;
44
+ const domainHints = normalizeDomainHints(parsed.domainHints);
45
+
46
+ const result = { level, roles, reasoning, taskType, domainHints, provider };
47
+ if (shouldDecompose && subtasks.length > 0) {
48
+ result.shouldDecompose = true;
49
+ result.subtasks = subtasks;
50
+ } else {
51
+ result.shouldDecompose = false;
73
52
  }
53
+ return result;
54
+ }
74
55
 
75
- try {
76
- const parsed = parseTriageOutput(result.output);
77
- if (!parsed) {
78
- return {
79
- ok: true,
80
- result: {
81
- level: "medium",
82
- roles: ["reviewer"],
83
- reasoning: "Unstructured output, using safe defaults.",
84
- taskType: FALLBACK_TASK_TYPE,
85
- provider,
86
- raw: result.output
87
- },
88
- summary: "Triage complete (fallback defaults)",
89
- usage: result.usage
90
- };
91
- }
92
-
93
- const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
94
- const roles = normalizeRoles(parsed.roles);
95
- const reasoning = String(parsed.reasoning || "").trim() || "No reasoning provided.";
96
- const shouldDecompose = Boolean(parsed.shouldDecompose);
97
- const subtasks = normalizeSubtasks(parsed.subtasks);
98
- const taskType = VALID_TASK_TYPES.has(parsed.taskType) ? parsed.taskType : FALLBACK_TASK_TYPE;
99
-
100
- const domainHints = normalizeDomainHints(parsed.domainHints);
101
-
102
- const triageResult = {
103
- level,
104
- roles,
105
- reasoning,
106
- taskType,
107
- domainHints,
108
- provider
109
- };
56
+ buildSummary(parsed) {
57
+ const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
58
+ const roles = normalizeRoles(parsed.roles);
59
+ const decomposeNote = parsed.shouldDecompose ? " — decomposition recommended" : "";
60
+ return `Triage: ${level} (${roles.length} role${roles.length === 1 ? "" : "s"})${decomposeNote}`;
61
+ }
110
62
 
111
- if (shouldDecompose && subtasks.length > 0) {
112
- triageResult.shouldDecompose = true;
113
- triageResult.subtasks = subtasks;
114
- } else {
115
- triageResult.shouldDecompose = false;
116
- }
63
+ handleParseNull(agentResult, provider) {
64
+ return {
65
+ ok: true,
66
+ result: { ...FALLBACK_RESULT, reasoning: "Unstructured output, using safe defaults.", provider, raw: agentResult.output },
67
+ summary: "Triage complete (fallback defaults)",
68
+ usage: agentResult.usage
69
+ };
70
+ }
117
71
 
118
- const decomposeNote = shouldDecompose ? " — decomposition recommended" : "";
119
- return {
120
- ok: true,
121
- result: triageResult,
122
- summary: `Triage: ${level} (${roles.length} role${roles.length === 1 ? "" : "s"})${decomposeNote}`,
123
- usage: result.usage
124
- };
125
- } catch { /* agent output is not structured JSON */
126
- return {
127
- ok: true,
128
- result: {
129
- level: "medium",
130
- roles: ["reviewer"],
131
- reasoning: "Failed to parse triage output, using safe defaults.",
132
- taskType: FALLBACK_TASK_TYPE,
133
- provider,
134
- raw: result.output
135
- },
136
- summary: "Triage complete (fallback defaults)",
137
- usage: result.usage
138
- };
139
- }
72
+ handleParseError(_err, agentResult, provider) {
73
+ return {
74
+ ok: true,
75
+ result: { ...FALLBACK_RESULT, reasoning: "Failed to parse triage output, using safe defaults.", provider, raw: agentResult.output },
76
+ summary: "Triage complete (fallback defaults)",
77
+ usage: agentResult.usage
78
+ };
140
79
  }
141
80
  }
@@ -5,7 +5,7 @@ import { getSessionRoot } from "./utils/paths.js";
5
5
 
6
6
  const SESSION_ROOT = getSessionRoot();
7
7
 
8
- export function newSessionId() {
8
+ function newSessionId() {
9
9
  const now = new Date();
10
10
  const stamp = now.toISOString().replaceAll(/[:.]/g, "-");
11
11
  return `s_${stamp}`;
@@ -63,6 +63,8 @@ const TASK_TEXT_PATTERNS = [
63
63
  { pattern: /\bgo(?:lang)?\b/i, skill: "go" },
64
64
  { pattern: /\brust\b/i, skill: "rust" },
65
65
  { pattern: /\bpython\b/i, skill: "python" },
66
+ { pattern: /\bdjango\b/i, skill: "python" },
67
+ { pattern: /\bflask\b/i, skill: "python" },
66
68
  { pattern: /\bruby\b/i, skill: "ruby" },
67
69
  { pattern: /\bflutter\b/i, skill: "flutter" },
68
70
  { pattern: /\bswift\b/i, skill: "swift" },
@@ -76,6 +78,15 @@ const TASK_TEXT_PATTERNS = [
76
78
  { pattern: /\breport\b/i, skill: "data-report" },
77
79
  { pattern: /\banalysis\b/i, skill: "data-report" },
78
80
  { pattern: /\bfindings\b/i, skill: "data-report" },
81
+ // Frontend/backend broad categories
82
+ { pattern: /\bfrontend\b/i, skill: "frontend-design" },
83
+ { pattern: /\bCSS\b/i, skill: "frontend-design" },
84
+ { pattern: /\bHTML\b/i, skill: "frontend-design" },
85
+ { pattern: /\bbackend\b/i, skill: "express" },
86
+ { pattern: /\bnode\.?js\b/i, skill: "express" },
87
+ { pattern: /\bAPI\b/, skill: "express" },
88
+ { pattern: /\bwebsocket\b/i, skill: "express" },
89
+ { pattern: /\breal[- ]time\b/i, skill: "express" },
79
90
  ];
80
91
 
81
92
  /**
@@ -27,6 +27,8 @@ export function resolveSonarHost(rawHost) {
27
27
  * 2. config.sonarqube.token
28
28
  * 3. SONAR_TOKEN env var
29
29
  *
30
+ * For async resolution (including credentials file), use resolveSonarTokenAsync.
31
+ *
30
32
  * @param {object} [config={}] - Karajan config object
31
33
  * @returns {string|null} Token string or null if none found
32
34
  */
@@ -39,6 +41,22 @@ export function resolveSonarToken(config = {}) {
39
41
  return token || null;
40
42
  }
41
43
 
44
+ /**
45
+ * Async version of resolveSonarToken that also checks the credentials file
46
+ * for previously generated tokens.
47
+ *
48
+ * @param {object} [config={}] - Karajan config object
49
+ * @returns {Promise<string|null>} Token string or null if none found
50
+ */
51
+ export async function resolveSonarTokenAsync(config = {}) {
52
+ const syncToken = resolveSonarToken(config);
53
+ if (syncToken) return syncToken;
54
+
55
+ // Check credentials file for persisted token
56
+ const fileCreds = (await loadSonarCredentials()) || {};
57
+ return fileCreds.token || null;
58
+ }
59
+
42
60
  /**
43
61
  * Resolve SonarQube admin credentials from (in priority order):
44
62
  * 1. Env vars: KJ_SONAR_ADMIN_USER / KJ_SONAR_ADMIN_PASSWORD
@@ -57,12 +75,13 @@ export async function resolveSonarCredentials(config = {}) {
57
75
  process.env.KJ_SONAR_ADMIN_USER ||
58
76
  config.sonarqube?.admin_user ||
59
77
  fileCreds.user ||
60
- null;
78
+ "admin"; // Default for fresh SonarQube installs
61
79
 
62
80
  const candidates = [
63
81
  process.env.KJ_SONAR_ADMIN_PASSWORD,
64
82
  config.sonarqube?.admin_password,
65
83
  fileCreds.password,
84
+ "admin", // Default password for fresh SonarQube installs (zero-config)
66
85
  ].filter(Boolean);
67
86
 
68
87
  return { user, passwords: [...new Set(candidates)] };
@@ -33,3 +33,20 @@ export async function loadSonarCredentials() {
33
33
  export function credentialsPath() {
34
34
  return path.join(getKarajanHome(), CREDENTIALS_FILENAME);
35
35
  }
36
+
37
+ /**
38
+ * Save a generated token to ~/.karajan/sonar-credentials.json so it persists across sessions.
39
+ * Merges with existing credentials (preserves user/password if present).
40
+ */
41
+ export async function saveSonarToken(token) {
42
+ const filePath = path.join(getKarajanHome(), CREDENTIALS_FILENAME);
43
+ let existing = {};
44
+ try {
45
+ const raw = await fs.readFile(filePath, "utf8");
46
+ existing = JSON.parse(raw);
47
+ } catch { /* file may not exist */ }
48
+ existing.token = token;
49
+ const dir = getKarajanHome();
50
+ await fs.mkdir(dir, { recursive: true });
51
+ await fs.writeFile(filePath, JSON.stringify(existing, null, 2), "utf8");
52
+ }
@@ -9,7 +9,7 @@ function slug(value) {
9
9
  .replaceAll(/(^-+)|(-+$)/g, "");
10
10
  }
11
11
 
12
- export function normalizeProjectKey(value) {
12
+ function normalizeProjectKey(value) {
13
13
  const out = slug(value);
14
14
  if (!out) return "kj-default";
15
15
  return /[a-z]/.test(out) ? out : `kj-${out}`;
@@ -150,7 +150,7 @@ async function resolveSonarTokenWithFallback(config, apiHost) {
150
150
  return null;
151
151
  }
152
152
 
153
- export async function ensureSonarProjectProperties(cwd = process.cwd()) {
153
+ async function ensureSonarProjectProperties(cwd = process.cwd()) {
154
154
  const propsPath = path.join(cwd, "sonar-project.properties");
155
155
  try {
156
156
  await fsPromises.access(propsPath);
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Karajan Code ASCII banner with brand colors.
3
+ *
4
+ * Brand palette:
5
+ * #AACCEE steel blue (primary text)
6
+ * #88CC88 green (accents, robot eyes)
7
+ * #464000 dark olive
8
+ * #444411 charcoal (secondary text)
9
+ */
10
+
11
+ const C = {
12
+ blue: "\x1b[38;2;170;204;238m", // #AACCEE
13
+ green: "\x1b[38;2;100;190;100m", // green accents
14
+ gray: "\x1b[38;2;140;140;140m", // borders/dim
15
+ white: "\x1b[38;2;220;220;220m", // bright text
16
+ charcoal: "\x1b[38;2;100;100;80m", // #444411 approx
17
+ bold: "\x1b[1m",
18
+ dim: "\x1b[2m",
19
+ reset: "\x1b[0m"
20
+ };
21
+
22
+ const R = C.reset;
23
+
24
+ const BANNER_LINES = [
25
+ ``,
26
+ `${C.gray} ╭───────────╮`,
27
+ `${C.gray} │ ${C.green}~${C.gray} │`,
28
+ `${C.gray} │ ${C.green}|${C.gray} ${C.green}|${C.gray} │ ${C.charcoal}|`,
29
+ `${C.gray} | ${C.green}v${C.gray} │ ${C.charcoal}/`,
30
+ `${C.gray} ${C.charcoal}\\${C.gray} ╰───────────╯${C.charcoal}/`,
31
+ `${C.charcoal} \\${C.gray} ╭───${C.white}⋈${C.gray}───╮ ${C.charcoal}/`,
32
+ `${C.charcoal} \\${C.gray} ${C.charcoal}/`,
33
+ `${C.gray} ╰───────╯`,
34
+ ``,
35
+ ` ${C.bold}${C.blue}╦╔═ ╔═╗ ╦═╗ ╔═╗ ╦ ╔═╗ ╔╗╔${R}`,
36
+ ` ${C.bold}${C.blue}╠╩╗ ╠═╣ ╠╦╝ ╠═╣ ║ ╠═╣ ║║║${R}`,
37
+ ` ${C.bold}${C.blue}╩ ╩ ╩ ╩ ╩╚═ ╩ ╩ ╚╝ ╩ ╩ ╝╚╝${R}`,
38
+ ` ${C.bold}${C.white} C O D E${R} %%VERSION%%`,
39
+ ` ${C.charcoal}multiagent coding orchestrator${R}`,
40
+ ``,
41
+ ];
42
+
43
+ /**
44
+ * Print the Karajan Code ASCII banner.
45
+ * @param {string} version - Package version to display
46
+ * @param {object} [opts]
47
+ * @param {boolean} [opts.compact] - Skip mascot, show only text
48
+ * @param {boolean} [opts.force] - Print even without TTY
49
+ */
50
+ export function printBanner(version, opts = {}) {
51
+ if (!opts.force && !process.stdout.isTTY) return;
52
+
53
+ const versionTag = `${C.dim}v${version}${R}`;
54
+
55
+ if (opts.compact) {
56
+ console.log();
57
+ for (const line of BANNER_LINES.slice(10)) {
58
+ console.log(line.replace("%%VERSION%%", versionTag));
59
+ }
60
+ return;
61
+ }
62
+
63
+ for (const line of BANNER_LINES) {
64
+ console.log(line.replace("%%VERSION%%", versionTag));
65
+ }
66
+ }
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { printBanner } from "./banner.js";
4
5
 
5
6
  // TODO: i18n display messages
6
7
  const DISPLAY_PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
@@ -83,9 +84,7 @@ const BAR = `${ANSI.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u
83
84
 
84
85
  export function printHeader({ task, config }) {
85
86
  const version = DISPLAY_VERSION;
86
- console.log(BAR);
87
- console.log(`${ANSI.bold}${ANSI.cyan}\u25b6 Karajan Code v${version}${ANSI.reset}`);
88
- console.log(BAR);
87
+ printBanner(version);
89
88
  console.log(`${ANSI.bold}Task:${ANSI.reset} ${task}`);
90
89
  console.log(
91
90
  `${ANSI.bold}Coder:${ANSI.reset} ${config.roles?.coder?.provider || config.coder} ${ANSI.dim}|${ANSI.reset} ${ANSI.bold}Reviewer:${ANSI.reset} ${config.roles?.reviewer?.provider || config.reviewer}`
@@ -100,7 +99,10 @@ export function printHeader({ task, config }) {
100
99
  if (pipeline.researcher?.enabled) activeRoles.push(`Researcher (${config.roles?.researcher?.provider || "?"})`);
101
100
  if (pipeline.tester?.enabled) activeRoles.push("Tester");
102
101
  if (pipeline.security?.enabled) activeRoles.push("Security");
103
- if (pipeline.solomon?.enabled) activeRoles.push(`Solomon (${config.roles?.solomon?.provider || "?"})`);
102
+ if (pipeline.solomon?.enabled) {
103
+ const solomonProvider = config.roles?.solomon?.provider;
104
+ activeRoles.push(solomonProvider ? `Solomon (${solomonProvider})` : "Solomon");
105
+ }
104
106
  if (activeRoles.length > 0) {
105
107
  const separator = ` ${ANSI.dim}|${ANSI.reset} `;
106
108
  console.log(`${ANSI.bold}Pipeline:${ANSI.reset} ${activeRoles.join(separator)}`);
@@ -257,7 +259,14 @@ function printSessionRtkSavings(rtkSavings) {
257
259
  const tokens = rtkSavings.estimatedTokensSaved ?? 0;
258
260
  const ratio = rtkSavings.savedPct ?? 0;
259
261
  const commands = rtkSavings.callCount ?? 0;
260
- console.log(` ${ANSI.dim}\u26a1 RTK: saved ~${tokens} tokens (${ratio}% compression, ${commands} commands)${ANSI.reset}`);
262
+ const original = rtkSavings.originalBytes ?? 0;
263
+ const compressed = rtkSavings.rtkBytes ?? 0;
264
+
265
+ if (tokens === 0 || ratio === 0) {
266
+ console.log(` ${ANSI.dim}⚡ RTK: ${commands} commands wrapped, 0% savings — output was already minimal (${original} bytes in, ${compressed} bytes out)${ANSI.reset}`);
267
+ } else {
268
+ console.log(` ${ANSI.dim}⚡ RTK: saved ~${tokens.toLocaleString()} tokens (${ratio}% compression, ${commands} commands, ${original.toLocaleString()} → ${compressed.toLocaleString()} bytes)${ANSI.reset}`);
269
+ }
261
270
  }
262
271
 
263
272
  function printSessionBudget(budget) {
@@ -268,11 +277,15 @@ function printSessionBudget(budget) {
268
277
  }
269
278
  const estPrefix = budget.includes_estimates ? "~" : "";
270
279
  const estNote = budget.includes_estimates ? " (includes estimates)" : "";
271
- console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${estPrefix}${budget.total_tokens ?? 0}${estNote}${ANSI.reset}`);
280
+ const fmtTokens = (n) => Number(n || 0).toLocaleString("en-US");
281
+ console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${estPrefix}${fmtTokens(budget.total_tokens)}${estNote}${ANSI.reset}`);
272
282
  console.log(` ${ANSI.dim}\ud83d\udcb0 Total cost: ${estPrefix}$${Number(budget.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`);
273
283
  for (const [role, metrics] of Object.entries(budget.breakdown_by_role || {})) {
284
+ const tokens = Number(metrics.total_tokens || 0);
285
+ const cost = Number(metrics.total_cost_usd || 0);
286
+ if (tokens === 0 && cost === 0) continue; // skip roles with no usage
274
287
  console.log(
275
- ` ${ANSI.dim} - ${role}: ${estPrefix}${metrics.total_tokens ?? 0} tokens, ${estPrefix}$${Number(metrics.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`
288
+ ` ${ANSI.dim} - ${role}: ${estPrefix}${fmtTokens(tokens)} tokens, ${estPrefix}$${cost.toFixed(2)}${ANSI.reset}`
276
289
  );
277
290
  }
278
291
  }
@@ -328,6 +341,10 @@ const EVENT_HANDLERS = {
328
341
 
329
342
  "tdd:result": (event, icon) => {
330
343
  const tdd = event.detail || {};
344
+ if (tdd.reason === "no_diff_available") {
345
+ console.log(` \u251c\u2500 ${icon} TDD policy: ${ANSI.yellow}SKIP${ANSI.reset} ${ANSI.dim}(no diff available)${ANSI.reset}`);
346
+ return;
347
+ }
331
348
  const label = tdd.ok ? `${ANSI.green}PASS${ANSI.reset}` : `${ANSI.red}FAIL${ANSI.reset}`;
332
349
  const files = tdd.sourceFiles === undefined ? "" : ` (${tdd.sourceFiles} src, ${tdd.testFiles} test)`;
333
350
  const executor = formatExecutor(event.detail);
@@ -435,14 +452,16 @@ const EVENT_HANDLERS = {
435
452
  const max = Number(d.max_budget_usd);
436
453
  const pct = Number(d.pct_used ?? 0);
437
454
  const warn = Number(d.warn_threshold_pct ?? 80);
438
- const hasEntries = (d.entries?.length ?? 0) > 0 || Object.keys(d.breakdown_by_role || {}).length > 0;
439
- if (hasEntries && totalTokens === 0 && total === 0) {
440
- console.log(` \u251c\u2500 ${icon} Budget: ${ANSI.dim}N/A (provider does not report usage)${ANSI.reset}`);
441
- return;
442
- }
455
+ // Don't show N/A — just skip budget display when there's nothing to report
456
+ if (total === 0 && totalTokens === 0) return;
457
+ const fmtTokens = (n) => n.toLocaleString("en-US");
458
+ const tokenStr = totalTokens > 0 ? ` / ${fmtTokens(totalTokens)} tokens` : "";
459
+ const costStr = `$${total.toFixed(2)}`;
443
460
  const color = budgetColor(max, pct, warn);
444
- if (Number.isFinite(max) && max >= 0) {
445
- console.log(` \u251c\u2500 ${icon} Budget: ${color}$${total.toFixed(2)} / $${max.toFixed(2)} (${pct.toFixed(1)}%)${ANSI.reset}`);
461
+ if (Number.isFinite(max) && max > 0) {
462
+ console.log(` \u251c\u2500 ${icon} Budget: ${color}${costStr}${tokenStr} / $${max.toFixed(2)} (${pct.toFixed(1)}%)${ANSI.reset}`);
463
+ } else {
464
+ console.log(` \u251c\u2500 ${icon} Budget: ${color}${costStr}${tokenStr}${ANSI.reset}`);
446
465
  }
447
466
  },
448
467
 
@@ -569,9 +588,9 @@ const EVENT_HANDLERS = {
569
588
  }
570
589
  },
571
590
 
572
- "becaria:pr-created": (event) => {
591
+ "ci:pr-created": (event) => {
573
592
  const url = event.detail?.prUrl || "";
574
- console.log(` \u251c\u2500 ${ANSI.green}\ud83d\ude80 BecarIA PR created: ${url}${ANSI.reset}`);
593
+ console.log(` \u251c\u2500 ${ANSI.green}\ud83d\ude80 CI PR created: ${url}${ANSI.reset}`);
575
594
  },
576
595
 
577
596
  "solomon:alert": (event) => {
@@ -597,7 +616,6 @@ const QUIET_SUPPRESSED = new Set([
597
616
  "skills:auto-install",
598
617
  "context:loaded",
599
618
  "rtk:detected",
600
- "proxy:started",
601
619
  "board:started",
602
620
  "plan:loaded",
603
621
  "tdd:auto-detect",
@@ -148,14 +148,29 @@ export function scanForInjection(text, opts = {}) {
148
148
  export function scanDiff(diff, opts = {}) {
149
149
  if (!diff) return { clean: true, findings: [], summary: "Empty diff" };
150
150
 
151
- // Extract only added lines (skip diff headers like +++ b/file)
152
- const addedLines = diff
153
- .split("\n")
154
- .filter((l) => l.startsWith("+") && !l.startsWith("+++"))
155
- .map((l) => l.slice(1))
156
- .join("\n");
157
-
158
- return scanForInjection(addedLines, opts);
151
+ // Split diff into per-file chunks to avoid false positives from
152
+ // concatenated added-lines across unrelated files.
153
+ const files = diff.split(/^diff --git /m).filter(Boolean);
154
+
155
+ const allFindings = [];
156
+ for (const fileChunk of files) {
157
+ const addedLines = fileChunk
158
+ .split("\n")
159
+ .filter((l) => l.startsWith("+") && !l.startsWith("+++"))
160
+ .map((l) => l.slice(1))
161
+ .join("\n");
162
+
163
+ if (!addedLines) continue;
164
+ const result = scanForInjection(addedLines, opts);
165
+ if (!result.clean) allFindings.push(...result.findings);
166
+ }
167
+
168
+ const clean = allFindings.length === 0;
169
+ return {
170
+ clean,
171
+ findings: allFindings,
172
+ summary: clean ? "No injection patterns detected" : `${allFindings.length} injection pattern(s) detected across diff`
173
+ };
159
174
  }
160
175
 
161
176
  // --- Helpers ---
@@ -19,7 +19,12 @@ const LEVEL_COLORS = {
19
19
  };
20
20
 
21
21
  function timestamp() {
22
- return new Date().toISOString().slice(11, 23);
22
+ const now = new Date();
23
+ const h = String(now.getHours()).padStart(2, "0");
24
+ const m = String(now.getMinutes()).padStart(2, "0");
25
+ const s = String(now.getSeconds()).padStart(2, "0");
26
+ const ms = String(now.getMilliseconds()).padStart(3, "0");
27
+ return `${h}:${m}:${s}.${ms}`;
23
28
  }
24
29
 
25
30
  function formatContext(ctx) {
@@ -76,10 +76,9 @@ async function awaitWithTimeout(subprocess, timeout, getStdout) {
76
76
 
77
77
  export async function runCommand(command, args = [], options = {}) {
78
78
  const { timeout, onOutput, silenceTimeoutMs, partialOutputFlushMs, ...rest } = options;
79
- // Always detach stdin: KJ subprocesses never need interactive input.
80
- // Without this, a subprocess prompting for input (sonar credentials,
81
- // agent approval, npm prompts) hangs forever since there is no TTY.
82
- if (!rest.stdin) rest.stdin = "ignore";
79
+ // Detach stdin unless the caller provides input (e.g. Codex uses input: prompt).
80
+ // Without this, a subprocess prompting for input hangs forever since there is no TTY.
81
+ if (!rest.stdin && !rest.input) rest.stdin = "ignore";
83
82
  const subprocess = execa(command, args, { reject: false, ...rest });
84
83
 
85
84
  let stdoutAccum = "";