karajan-code 1.4.0 → 1.5.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/README.md CHANGED
@@ -38,6 +38,7 @@ Instead of running one AI agent and manually reviewing its output, `kj` chains a
38
38
  - **Git automation** — auto-commit, auto-push, auto-PR after approval
39
39
  - **Session management** — pause/resume with fail-fast detection and automatic cleanup of expired sessions
40
40
  - **Plugin system** — extend with custom agents via `.karajan/plugins/`
41
+ - **Smart model selection** — auto-selects optimal model per role based on triage complexity (lighter models for trivial tasks, powerful models for complex ones)
41
42
  - **Retry with backoff** — automatic recovery from transient API errors (429, 5xx) with exponential backoff and jitter
42
43
  - **Planning Game integration** — optionally pair with [Planning Game](https://github.com/AgenteIA-Geniova/planning-game) for agile project management (tasks, sprints, estimation) — like Jira, but open-source and XP-native
43
44
 
@@ -195,6 +196,8 @@ kj run "Fix the login bug" [options]
195
196
  | `--auto-pr` | Create PR after push |
196
197
  | `--no-auto-rebase` | Disable auto-rebase before push |
197
198
  | `--branch-prefix <prefix>` | Branch naming prefix (default: `feat/`) |
199
+ | `--smart-models` | Enable smart model selection based on triage complexity |
200
+ | `--no-smart-models` | Disable smart model selection |
198
201
  | `--no-sonar` | Skip SonarQube analysis |
199
202
  | `--pg-task <cardId>` | Planning Game card ID for task context |
200
203
  | `--pg-project <projectId>` | Planning Game project ID |
@@ -354,6 +357,16 @@ budget:
354
357
  currency: usd # usd | eur
355
358
  exchange_rate_eur: 0.92
356
359
 
360
+ # Smart model selection (requires --enable-triage)
361
+ model_selection:
362
+ enabled: true # Auto-select models based on triage complexity
363
+ tiers: # Override default tier map per provider
364
+ claude:
365
+ simple: claude/sonnet # Use sonnet even for simple tasks
366
+ role_overrides: # Override level mapping per role
367
+ reviewer:
368
+ trivial: medium # Reviewer always at least medium tier
369
+
357
370
  # Output
358
371
  output:
359
372
  report_dir: ./.reviews
@@ -430,7 +443,7 @@ Use `kj roles show <role>` to inspect any template. Create a project override to
430
443
  git clone https://github.com/manufosela/karajan-code.git
431
444
  cd karajan-code
432
445
  npm install
433
- npm test # Run 899+ tests with Vitest
446
+ npm test # Run 964+ tests with Vitest
434
447
  npm run test:watch # Watch mode
435
448
  npm run validate # Lint + test
436
449
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.4.0",
3
+ "version": "1.5.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/cli.js CHANGED
@@ -24,7 +24,7 @@ async function withConfig(commandName, flags, fn) {
24
24
  }
25
25
 
26
26
  const program = new Command();
27
- program.name("kj").description("Karajan Code CLI").version("1.2.1");
27
+ program.name("kj").description("Karajan Code CLI").version("1.5.0");
28
28
 
29
29
  program
30
30
  .command("init")
@@ -83,6 +83,8 @@ program
83
83
  .option("--no-sonar")
84
84
  .option("--pg-task <cardId>", "Planning Game card ID (e.g., KJC-TSK-0042)")
85
85
  .option("--pg-project <projectId>", "Planning Game project ID")
86
+ .option("--smart-models", "Enable smart model selection based on triage complexity")
87
+ .option("--no-smart-models", "Disable smart model selection")
86
88
  .option("--dry-run", "Show what would be executed without running anything")
87
89
  .option("--json", "Output JSON only (no styled display)")
88
90
  .action(async (task, flags) => {
package/src/config.js CHANGED
@@ -106,6 +106,11 @@ const DEFAULTS = {
106
106
  currency: "usd",
107
107
  exchange_rate_eur: 0.92
108
108
  },
109
+ model_selection: {
110
+ enabled: true,
111
+ tiers: {},
112
+ role_overrides: {}
113
+ },
109
114
  session: {
110
115
  max_iteration_minutes: 15,
111
116
  max_total_minutes: 120,
@@ -260,6 +265,9 @@ export function applyRunOverrides(config, flags) {
260
265
  out.planning_game = out.planning_game || {};
261
266
  if (flags.pgTask) out.planning_game.enabled = true;
262
267
  if (flags.pgProject) out.planning_game.project_id = flags.pgProject;
268
+ out.model_selection = out.model_selection || { enabled: true, tiers: {}, role_overrides: {} };
269
+ if (flags.smartModels === true) out.model_selection.enabled = true;
270
+ if (flags.smartModels === false || flags.noSmartModels === true) out.model_selection.enabled = false;
263
271
  return out;
264
272
  }
265
273
 
package/src/mcp/run-kj.js CHANGED
@@ -49,6 +49,8 @@ export async function runKjCommand({ command, commandArgs = [], options = {}, en
49
49
  normalizeBoolFlag(options.autoPr, "--auto-pr", args);
50
50
  if (options.autoRebase === false) args.push("--no-auto-rebase");
51
51
  normalizeBoolFlag(options.noSonar, "--no-sonar", args);
52
+ if (options.smartModels === true) args.push("--smart-models");
53
+ if (options.smartModels === false) args.push("--no-smart-models");
52
54
  addOptionalValue(args, "--pg-task", options.pgTask);
53
55
  addOptionalValue(args, "--pg-project", options.pgProject);
54
56
 
package/src/mcp/tools.js CHANGED
@@ -85,6 +85,7 @@ export const tools = [
85
85
  autoPr: { type: "boolean" },
86
86
  autoRebase: { type: "boolean" },
87
87
  branchPrefix: { type: "string" },
88
+ smartModels: { type: "boolean", description: "Enable/disable smart model selection based on triage complexity" },
88
89
  noSonar: { type: "boolean" },
89
90
  kjHome: { type: "string" },
90
91
  sonarToken: { type: "string" },
@@ -5,6 +5,7 @@ import { createAgent } from "../agents/index.js";
5
5
  import { addCheckpoint, markSessionStatus } from "../session-store.js";
6
6
  import { emitProgress, makeEvent } from "../utils/events.js";
7
7
  import { parsePlannerOutput } from "../prompts/planner.js";
8
+ import { selectModelsForRoles } from "../utils/model-selector.js";
8
9
 
9
10
  export async function runTriageStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
10
11
  logger.setContext({ iteration: 0, stage: "triage" });
@@ -47,6 +48,31 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
47
48
  reasoning: triageOutput.result?.reasoning || null
48
49
  };
49
50
 
51
+ let modelSelection = null;
52
+ if (triageOutput.ok && config?.model_selection?.enabled) {
53
+ const level = triageOutput.result?.level;
54
+ if (level) {
55
+ const { modelOverrides, reasoning } = selectModelsForRoles({ level, config });
56
+ for (const [role, model] of Object.entries(modelOverrides)) {
57
+ if (config.roles?.[role] && !config.roles[role].model) {
58
+ config.roles[role].model = model;
59
+ }
60
+ }
61
+ modelSelection = { modelOverrides, reasoning };
62
+ emitProgress(
63
+ emitter,
64
+ makeEvent("model-selection:applied", { ...eventBase, stage: "triage" }, {
65
+ message: "Smart model selection applied",
66
+ detail: modelSelection
67
+ })
68
+ );
69
+ }
70
+ }
71
+
72
+ if (modelSelection) {
73
+ stageResult.modelSelection = modelSelection;
74
+ }
75
+
50
76
  emitProgress(
51
77
  emitter,
52
78
  makeEvent("triage:end", { ...eventBase, stage: "triage" }, {
@@ -0,0 +1,107 @@
1
+ const DEFAULT_MODEL_TIERS = {
2
+ claude: { trivial: "claude/haiku", simple: "claude/haiku", medium: "claude/sonnet", complex: "claude/opus" },
3
+ codex: { trivial: "codex/o4-mini", simple: "codex/o4-mini", medium: "codex/o4-mini", complex: "codex/o3" },
4
+ gemini: { trivial: "gemini/flash", simple: "gemini/flash", medium: "gemini/pro", complex: "gemini/pro" },
5
+ aider: { trivial: null, simple: null, medium: null, complex: null }
6
+ };
7
+
8
+ const DEFAULT_ROLE_OVERRIDES = {
9
+ reviewer: { trivial: "medium", simple: "medium" },
10
+ triage: { medium: "simple", complex: "simple" }
11
+ };
12
+
13
+ const VALID_LEVELS = new Set(["trivial", "simple", "medium", "complex"]);
14
+
15
+ export function getDefaultModelTiers() {
16
+ return JSON.parse(JSON.stringify(DEFAULT_MODEL_TIERS));
17
+ }
18
+
19
+ export function getDefaultRoleOverrides() {
20
+ return JSON.parse(JSON.stringify(DEFAULT_ROLE_OVERRIDES));
21
+ }
22
+
23
+ export function resolveModelForRole({ role, provider, level, tierMap, roleOverrides }) {
24
+ if (!provider || !level || !VALID_LEVELS.has(level)) return null;
25
+
26
+ const tiers = tierMap || DEFAULT_MODEL_TIERS;
27
+ const providerTiers = tiers[provider];
28
+ if (!providerTiers) return null;
29
+
30
+ const overrides = roleOverrides || DEFAULT_ROLE_OVERRIDES;
31
+ const roleOvr = overrides[role];
32
+
33
+ let effectiveLevel = level;
34
+ if (roleOvr && roleOvr[level]) {
35
+ const mappedLevel = roleOvr[level];
36
+ if (VALID_LEVELS.has(mappedLevel)) {
37
+ effectiveLevel = mappedLevel;
38
+ }
39
+ }
40
+
41
+ return providerTiers[effectiveLevel] || null;
42
+ }
43
+
44
+ export function selectModelsForRoles({ level, config, roles }) {
45
+ if (!level || !VALID_LEVELS.has(level)) {
46
+ return { modelOverrides: {}, reasoning: "No valid triage level provided" };
47
+ }
48
+
49
+ const modelSelection = config?.model_selection || {};
50
+ const userTiers = modelSelection.tiers || {};
51
+ const userRoleOverrides = modelSelection.role_overrides || {};
52
+
53
+ const mergedTiers = { ...getDefaultModelTiers() };
54
+ for (const [provider, levels] of Object.entries(userTiers)) {
55
+ mergedTiers[provider] = { ...(mergedTiers[provider] || {}), ...levels };
56
+ }
57
+
58
+ const mergedRoleOverrides = { ...getDefaultRoleOverrides() };
59
+ for (const [role, levels] of Object.entries(userRoleOverrides)) {
60
+ mergedRoleOverrides[role] = { ...(mergedRoleOverrides[role] || {}), ...levels };
61
+ }
62
+
63
+ const allRoles = roles || Object.keys(config?.roles || {});
64
+ const modelOverrides = {};
65
+ const details = [];
66
+
67
+ for (const role of allRoles) {
68
+ const roleConfig = config?.roles?.[role];
69
+ if (!roleConfig) continue;
70
+
71
+ if (roleConfig.model) {
72
+ details.push(`${role}: skipped (explicit model "${roleConfig.model}")`);
73
+ continue;
74
+ }
75
+
76
+ if (roleConfig.disabled) {
77
+ details.push(`${role}: skipped (disabled)`);
78
+ continue;
79
+ }
80
+
81
+ const provider = roleConfig.provider;
82
+ if (!provider) {
83
+ details.push(`${role}: skipped (no provider)`);
84
+ continue;
85
+ }
86
+
87
+ const model = resolveModelForRole({
88
+ role,
89
+ provider,
90
+ level,
91
+ tierMap: mergedTiers,
92
+ roleOverrides: mergedRoleOverrides
93
+ });
94
+
95
+ if (model) {
96
+ modelOverrides[role] = model;
97
+ details.push(`${role}: ${model} (level=${level}, provider=${provider})`);
98
+ } else {
99
+ details.push(`${role}: no model for provider "${provider}"`);
100
+ }
101
+ }
102
+
103
+ return {
104
+ modelOverrides,
105
+ reasoning: `Smart model selection (level=${level}): ${details.join("; ")}`
106
+ };
107
+ }