karajan-code 1.33.0 → 1.34.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.33.0",
3
+ "version": "1.34.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",
package/src/config.js CHANGED
@@ -420,6 +420,34 @@ export function applyRunOverrides(config, flags) {
420
420
  return out;
421
421
  }
422
422
 
423
+ /**
424
+ * Check if a model string is compatible with an agent provider.
425
+ * Only returns false when the model clearly belongs to a DIFFERENT provider.
426
+ * Returns true if we can't determine or if the model is ambiguous.
427
+ */
428
+ const AGENT_MODEL_SIGNATURES = {
429
+ claude: ["claude", "sonnet", "opus", "haiku"],
430
+ codex: ["o4-", "o3-", "gpt-", "codex"],
431
+ gemini: ["gemini", "flash-"]
432
+ };
433
+
434
+ export function isModelCompatible(agent, model) {
435
+ if (!model || !agent) return true;
436
+ const lower = model.toLowerCase();
437
+
438
+ // Check if model clearly belongs to a different provider
439
+ for (const [provider, signatures] of Object.entries(AGENT_MODEL_SIGNATURES)) {
440
+ if (provider === agent) continue;
441
+ if (signatures.some(s => lower.includes(s))) {
442
+ // Model belongs to a different provider — incompatible
443
+ return false;
444
+ }
445
+ }
446
+
447
+ // Model doesn't clearly belong to any other provider — allow it
448
+ return true;
449
+ }
450
+
423
451
  export function resolveRole(config, role) {
424
452
  const roles = config?.roles || {};
425
453
  const roleConfig = roles[role] || {};
@@ -434,10 +462,17 @@ export function resolveRole(config, role) {
434
462
  }
435
463
 
436
464
  let model = roleConfig.model ?? null;
465
+ let modelIsInherited = false;
437
466
  if (!model && role === "coder") model = config?.coder_options?.model ?? null;
438
467
  if (!model && role === "reviewer") model = config?.reviewer_options?.model ?? null;
439
468
  if (!model && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect" || role === "hu_reviewer" || role === "hu-reviewer")) {
440
469
  model = config?.coder_options?.model ?? null;
470
+ modelIsInherited = !!model;
471
+ }
472
+
473
+ // Drop inherited model if incompatible with the resolved provider
474
+ if (modelIsInherited && provider && model && !isModelCompatible(provider, model)) {
475
+ model = null;
441
476
  }
442
477
 
443
478
  return { provider, model };
@@ -790,7 +790,10 @@ async function handleRun(a, server, extra) {
790
790
  }
791
791
  }
792
792
  if (!isPreflightAcked()) {
793
- return buildPreflightRequiredResponse("kj_run");
793
+ // Auto-acknowledge with defaults for autonomous operation
794
+ ackPreflight({});
795
+ const logger = createLogger("info", "mcp");
796
+ logger.info("Preflight auto-acknowledged with default agent config");
794
797
  }
795
798
  applySessionOverrides(a, ["coder", "reviewer", "tester", "security", "solomon", "enableTester", "enableSecurity", "enableImpeccable"]);
796
799
  return handleRunDirect(a, server, extra);
@@ -801,7 +804,10 @@ async function handleCode(a, server, extra) {
801
804
  return failPayload("Missing required field: task");
802
805
  }
803
806
  if (!isPreflightAcked()) {
804
- return buildPreflightRequiredResponse("kj_code");
807
+ // Auto-acknowledge with defaults for autonomous operation
808
+ ackPreflight({});
809
+ const logger = createLogger("info", "mcp");
810
+ logger.info("Preflight auto-acknowledged with default agent config");
805
811
  }
806
812
  applySessionOverrides(a, ["coder"]);
807
813
  return handleCodeDirect(a, server, extra);
@@ -1,3 +1,5 @@
1
+ import { extractFirstJson } from "../utils/json-extract.js";
2
+
1
3
  const SUBAGENT_PREAMBLE = [
2
4
  "IMPORTANT: You are running as a Karajan sub-agent.",
3
5
  "Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
@@ -154,16 +156,8 @@ function parseRecommendation(raw) {
154
156
  }
155
157
 
156
158
  export function parseAuditOutput(raw) {
157
- const text = raw?.trim() || "";
158
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
159
- if (!jsonMatch) return null;
160
-
161
- let parsed;
162
- try {
163
- parsed = JSON.parse(jsonMatch[0]);
164
- } catch {
165
- return null;
166
- }
159
+ const parsed = extractFirstJson(raw);
160
+ if (!parsed) return null;
167
161
 
168
162
  // Handle both wrapped (result.summary) and flat structures
169
163
  const resultObj = parsed.result || parsed;
@@ -31,7 +31,7 @@ const SERENA_INSTRUCTIONS = [
31
31
  "Fall back to reading files only when Serena tools are not sufficient."
32
32
  ].join("\n");
33
33
 
34
- export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null }) {
34
+ export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, plan = null }) {
35
35
  const sections = [
36
36
  serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
37
37
  `Task:\n${task}`,
@@ -52,6 +52,10 @@ export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary =
52
52
  sections.push(`## Product Context\n${productContext}`);
53
53
  }
54
54
 
55
+ if (plan) {
56
+ sections.push(`## Implementation Plan (from planner)\nFollow these steps:\n${plan}`);
57
+ }
58
+
55
59
  if (coderRules) {
56
60
  sections.push(`Coder rules (MUST follow):\n${coderRules}`);
57
61
  }
@@ -1,3 +1,5 @@
1
+ import { extractFirstJson } from "../utils/json-extract.js";
2
+
1
3
  const SUBAGENT_PREAMBLE = [
2
4
  "IMPORTANT: You are running as a Karajan sub-agent.",
3
5
  "Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
@@ -205,16 +207,8 @@ function parseJtbds(rawJtbds) {
205
207
  }
206
208
 
207
209
  export function parseDiscoverOutput(raw) {
208
- const text = raw?.trim() || "";
209
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
210
- if (!jsonMatch) return null;
211
-
212
- let parsed;
213
- try {
214
- parsed = JSON.parse(jsonMatch[0]);
215
- } catch {
216
- return null;
217
- }
210
+ const parsed = extractFirstJson(raw);
211
+ if (!parsed) return null;
218
212
 
219
213
  return {
220
214
  verdict: VALID_VERDICTS.has(parsed.verdict) ? parsed.verdict : "ready",
@@ -1,3 +1,5 @@
1
+ import { extractFirstJson } from "../utils/json-extract.js";
2
+
1
3
  const SUBAGENT_PREAMBLE = [
2
4
  "IMPORTANT: You are running as a Karajan sub-agent.",
3
5
  "Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
@@ -135,16 +137,8 @@ export function normalizeAcceptanceCriteria(criteria) {
135
137
  * @returns {object|null} Parsed result with evaluations and batch_summary, or null.
136
138
  */
137
139
  export function parseHuReviewerOutput(raw) {
138
- const text = raw?.trim() || "";
139
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
140
- if (!jsonMatch) return null;
141
-
142
- let parsed;
143
- try {
144
- parsed = JSON.parse(jsonMatch[0]);
145
- } catch {
146
- return null;
147
- }
140
+ const parsed = extractFirstJson(raw);
141
+ if (!parsed) return null;
148
142
 
149
143
  if (!Array.isArray(parsed.evaluations)) return null;
150
144
 
@@ -3,23 +3,11 @@
3
3
  * Extracted from orchestrator.js to improve testability and reduce complexity.
4
4
  */
5
5
 
6
+ import { extractFirstJson } from "../utils/json-extract.js";
7
+
6
8
  export function parseMaybeJsonString(value) {
7
9
  if (typeof value !== "string") return null;
8
- try {
9
- return JSON.parse(value);
10
- } catch {
11
- const start = value.indexOf("{");
12
- const end = value.lastIndexOf("}");
13
- if (start >= 0 && end > start) {
14
- const candidate = value.slice(start, end + 1);
15
- try {
16
- return JSON.parse(candidate);
17
- } catch {
18
- return null;
19
- }
20
- }
21
- return null;
22
- }
10
+ return extractFirstJson(value);
23
11
  }
24
12
 
25
13
  function isReviewPayload(obj) {
@@ -1,5 +1,6 @@
1
1
  import { BaseRole } from "./base-role.js";
2
2
  import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
+ import { extractFirstJson } from "../utils/json-extract.js";
3
4
 
4
5
  const SUBAGENT_PREAMBLE = [
5
6
  "IMPORTANT: You are running as a Karajan sub-agent.",
@@ -38,10 +39,7 @@ function buildPrompt({ task, diff, instructions }) {
38
39
  }
39
40
 
40
41
  function parseSecurityOutput(raw) {
41
- const text = raw?.trim() || "";
42
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
43
- if (!jsonMatch) return null;
44
- return JSON.parse(jsonMatch[0]);
42
+ return extractFirstJson(raw);
45
43
  }
46
44
 
47
45
  function buildSummary(parsed) {
@@ -1,5 +1,6 @@
1
1
  import { BaseRole } from "./base-role.js";
2
2
  import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
+ import { extractFirstJson } from "../utils/json-extract.js";
3
4
 
4
5
  const SUBAGENT_PREAMBLE = [
5
6
  "IMPORTANT: You are running as a Karajan sub-agent.",
@@ -42,10 +43,7 @@ function buildPrompt({ task, diff, sonarIssues, instructions }) {
42
43
  }
43
44
 
44
45
  function parseTesterOutput(raw) {
45
- const text = raw?.trim() || "";
46
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
47
- if (!jsonMatch) return null;
48
- return JSON.parse(jsonMatch[0]);
46
+ return extractFirstJson(raw);
49
47
  }
50
48
 
51
49
  export class TesterRole extends BaseRole {
@@ -2,6 +2,7 @@ import { BaseRole } from "./base-role.js";
2
2
  import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
3
  import { buildTriagePrompt } from "../prompts/triage.js";
4
4
  import { VALID_TASK_TYPES } from "../guards/policy-resolver.js";
5
+ import { extractFirstJson } from "../utils/json-extract.js";
5
6
 
6
7
  const VALID_LEVELS = new Set(["trivial", "simple", "medium", "complex"]);
7
8
  const VALID_ROLES = new Set(["planner", "researcher", "refactorer", "reviewer", "tester", "security", "impeccable"]);
@@ -16,10 +17,7 @@ function resolveProvider(config) {
16
17
  }
17
18
 
18
19
  function parseTriageOutput(raw) {
19
- const text = raw?.trim() || "";
20
- const jsonMatch = /\{[\s\S]*\}/.exec(text);
21
- if (!jsonMatch) return null;
22
- return JSON.parse(jsonMatch[0]);
20
+ return extractFirstJson(raw);
23
21
  }
24
22
 
25
23
  function normalizeRoles(roles) {
@@ -1,5 +1,17 @@
1
1
  import { calculateUsageCostUsd, DEFAULT_MODEL_PRICING, mergePricing } from "./pricing.js";
2
2
 
3
+ /**
4
+ * Estimate token counts from character lengths when CLIs don't report usage.
5
+ * Rough heuristic: ~4 characters per token for English text.
6
+ */
7
+ export function estimateTokens(promptLength, responseLength) {
8
+ return {
9
+ tokens_in: Math.ceil((promptLength || 0) / 4),
10
+ tokens_out: Math.ceil((responseLength || 0) / 4),
11
+ estimated: true
12
+ };
13
+ }
14
+
3
15
  export function extractUsageMetrics(result, defaultModel = null) {
4
16
  const usage = result?.usage || result?.metrics || {};
5
17
  const tokens_in =
@@ -27,7 +39,22 @@ export function extractUsageMetrics(result, defaultModel = null) {
27
39
  defaultModel ??
28
40
  null;
29
41
 
30
- return { tokens_in, tokens_out, cost_usd, model };
42
+ // If no real token data AND no explicit cost, estimate from prompt/output sizes.
43
+ // Estimation is opt-in: only triggered when result.promptSize is explicitly provided.
44
+ let estimated = false;
45
+ let finalTokensIn = tokens_in;
46
+ let finalTokensOut = tokens_out;
47
+ const hasExplicitCost = cost_usd !== undefined && cost_usd !== null && cost_usd !== "";
48
+ if (!tokens_in && !tokens_out && !hasExplicitCost && result?.promptSize > 0) {
49
+ const promptSize = result.promptSize;
50
+ const outputSize = (result?.output || result?.summary || "").length;
51
+ const est = estimateTokens(promptSize, outputSize);
52
+ finalTokensIn = est.tokens_in;
53
+ finalTokensOut = est.tokens_out;
54
+ estimated = true;
55
+ }
56
+
57
+ return { tokens_in: finalTokensIn, tokens_out: finalTokensOut, cost_usd, model, estimated };
31
58
  }
32
59
 
33
60
  function toSafeNumber(value) {
@@ -63,7 +90,7 @@ export class BudgetTracker {
63
90
  this.pricing = mergePricing(DEFAULT_MODEL_PRICING, options.pricing || {});
64
91
  }
65
92
 
66
- record({ role, provider, model, tokens_in, tokens_out, cost_usd, duration_ms, stage_index } = {}) {
93
+ record({ role, provider, model, tokens_in, tokens_out, cost_usd, duration_ms, stage_index, estimated } = {}) {
67
94
  const safeTokensIn = toSafeNumber(tokens_in);
68
95
  const safeTokensOut = toSafeNumber(tokens_out);
69
96
  const hasExplicitCost = cost_usd !== undefined && cost_usd !== null && cost_usd !== "";
@@ -89,6 +116,9 @@ export class BudgetTracker {
89
116
  if (stage_index !== undefined && stage_index !== null) {
90
117
  entry.stage_index = Number(stage_index);
91
118
  }
119
+ if (estimated) {
120
+ entry.estimated = true;
121
+ }
92
122
  this.entries.push(entry);
93
123
  return entry;
94
124
  }
@@ -133,26 +163,33 @@ export class BudgetTracker {
133
163
  addToBreakdown(byRole, entry.role, entry);
134
164
  }
135
165
 
136
- return {
166
+ const hasEstimates = this.entries.some(e => e.estimated);
167
+ const result = {
137
168
  total_tokens: totals.tokens_in + totals.tokens_out,
138
169
  total_cost_usd: totals.cost_usd,
139
170
  breakdown_by_role: byRole,
140
171
  entries: [...this.entries],
141
172
  usage_available: this.hasUsageData()
142
173
  };
174
+ if (hasEstimates) result.includes_estimates = true;
175
+ return result;
143
176
  }
144
177
 
145
178
  trace() {
146
- return this.entries.map((entry, index) => ({
147
- index: entry.stage_index ?? index,
148
- role: entry.role,
149
- provider: entry.provider,
150
- model: entry.model,
151
- timestamp: entry.timestamp,
152
- duration_ms: entry.duration_ms ?? null,
153
- tokens_in: entry.tokens_in,
154
- tokens_out: entry.tokens_out,
155
- cost_usd: entry.cost_usd
156
- }));
179
+ return this.entries.map((entry, index) => {
180
+ const item = {
181
+ index: entry.stage_index ?? index,
182
+ role: entry.role,
183
+ provider: entry.provider,
184
+ model: entry.model,
185
+ timestamp: entry.timestamp,
186
+ duration_ms: entry.duration_ms ?? null,
187
+ tokens_in: entry.tokens_in,
188
+ tokens_out: entry.tokens_out,
189
+ cost_usd: entry.cost_usd
190
+ };
191
+ if (entry.estimated) item.estimated = true;
192
+ return item;
193
+ });
157
194
  }
158
195
  }
@@ -225,11 +225,13 @@ function printSessionBudget(budget) {
225
225
  console.log(` ${ANSI.dim}\ud83d\udcb0 Budget: N/A (provider does not report usage)${ANSI.reset}`);
226
226
  return;
227
227
  }
228
- console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${budget.total_tokens ?? 0}${ANSI.reset}`);
229
- console.log(` ${ANSI.dim}\ud83d\udcb0 Total cost: $${Number(budget.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`);
228
+ const estPrefix = budget.includes_estimates ? "~" : "";
229
+ const estNote = budget.includes_estimates ? " (includes estimates)" : "";
230
+ console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${estPrefix}${budget.total_tokens ?? 0}${estNote}${ANSI.reset}`);
231
+ console.log(` ${ANSI.dim}\ud83d\udcb0 Total cost: ${estPrefix}$${Number(budget.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`);
230
232
  for (const [role, metrics] of Object.entries(budget.breakdown_by_role || {})) {
231
233
  console.log(
232
- ` ${ANSI.dim} - ${role}: ${metrics.total_tokens ?? 0} tokens, $${Number(metrics.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`
234
+ ` ${ANSI.dim} - ${role}: ${estPrefix}${metrics.total_tokens ?? 0} tokens, ${estPrefix}$${Number(metrics.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`
233
235
  );
234
236
  }
235
237
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Robust JSON extraction from agent output.
3
+ * Extracts the first complete JSON object from a string,
4
+ * ignoring any trailing text that would cause parse errors.
5
+ */
6
+
7
+ /**
8
+ * Extract the first valid JSON object from a raw string.
9
+ * Handles cases where agents output valid JSON followed by extra text.
10
+ * @param {string} raw - Raw agent output.
11
+ * @returns {object|null} Parsed JSON object, or null if no valid JSON found.
12
+ */
13
+ export function extractFirstJson(raw) {
14
+ if (!raw) return null;
15
+ const str = typeof raw === "string" ? raw.trim() : String(raw).trim();
16
+ if (!str) return null;
17
+
18
+ // Fast path: try parsing the whole string first
19
+ try {
20
+ return JSON.parse(str);
21
+ } catch { /* fall through to extraction */ }
22
+
23
+ // Find the first '{' and match to its closing '}'
24
+ const start = str.indexOf("{");
25
+ if (start === -1) return null;
26
+
27
+ let depth = 0;
28
+ let inString = false;
29
+ let escaped = false;
30
+
31
+ for (let i = start; i < str.length; i++) {
32
+ const ch = str[i];
33
+
34
+ if (escaped) {
35
+ escaped = false;
36
+ continue;
37
+ }
38
+
39
+ if (ch === "\\") {
40
+ escaped = true;
41
+ continue;
42
+ }
43
+
44
+ if (ch === '"') {
45
+ inString = !inString;
46
+ continue;
47
+ }
48
+
49
+ if (inString) continue;
50
+
51
+ if (ch === "{") depth++;
52
+ if (ch === "}") depth--;
53
+
54
+ if (depth === 0) {
55
+ try {
56
+ return JSON.parse(str.substring(start, i + 1));
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+ }
62
+
63
+ return null;
64
+ }
@@ -21,6 +21,13 @@ Before reporting done, verify that ALL parts of the task are addressed:
21
21
  - Run the test suite after implementation to verify nothing is broken.
22
22
  - An incomplete implementation is worse than an error — never report success if parts are missing.
23
23
 
24
+ ## Implementation Rules
25
+ - NEVER generate placeholder, stub, or TODO code. Every function must be fully implemented.
26
+ - If the task says "create X", create the complete working implementation, not a skeleton.
27
+ - If tests exist, the implementation MUST make all tests pass.
28
+ - If you write tests first (TDD), the implementation MUST make those tests pass.
29
+ - Do NOT commit code that doesn't compile or doesn't pass tests.
30
+
24
31
  ## File modification safety
25
32
 
26
33
  - NEVER overwrite existing files entirely. Always make targeted, minimal edits.