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 +14 -1
- package/package.json +1 -1
- package/src/cli.js +3 -1
- package/src/config.js +8 -0
- package/src/mcp/run-kj.js +2 -0
- package/src/mcp/tools.js +1 -0
- package/src/orchestrator/pre-loop-stages.js +26 -0
- package/src/utils/model-selector.js +107 -0
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
|
|
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
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.
|
|
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
|
+
}
|