karajan-code 1.9.6 → 1.10.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 +1 -1
- package/src/cli.js +10 -0
- package/src/commands/agents.js +68 -0
- package/src/commands/doctor.js +18 -0
- package/src/mcp/server-handlers.js +15 -0
- package/src/mcp/tools.js +14 -1
- package/src/orchestrator.js +24 -6
- package/src/prompts/coder.js +10 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import { runCommandHandler } from "./commands/run.js";
|
|
|
17
17
|
import { resumeCommand } from "./commands/resume.js";
|
|
18
18
|
import { sonarCommand, sonarOpenCommand } from "./commands/sonar.js";
|
|
19
19
|
import { rolesCommand } from "./commands/roles.js";
|
|
20
|
+
import { agentsCommand } from "./commands/agents.js";
|
|
20
21
|
|
|
21
22
|
const PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
22
23
|
const PKG_VERSION = JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
|
|
@@ -161,6 +162,15 @@ program
|
|
|
161
162
|
});
|
|
162
163
|
});
|
|
163
164
|
|
|
165
|
+
program
|
|
166
|
+
.command("agents [subcommand] [role] [provider]")
|
|
167
|
+
.description("List or change AI agent assignments per role (e.g. kj agents set coder gemini)")
|
|
168
|
+
.action(async (subcommand, role, provider) => {
|
|
169
|
+
await withConfig("agents", {}, async ({ config }) => {
|
|
170
|
+
await agentsCommand({ config, subcommand: subcommand || "list", role, provider });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
164
174
|
program
|
|
165
175
|
.command("plan")
|
|
166
176
|
.description("Generate implementation plan")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { loadConfig, writeConfig, getConfigPath, resolveRole } from "../config.js";
|
|
2
|
+
import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
|
|
3
|
+
|
|
4
|
+
const ASSIGNABLE_ROLES = [
|
|
5
|
+
"coder", "reviewer", "planner", "refactorer", "triage",
|
|
6
|
+
"researcher", "tester", "security", "solomon"
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const VALID_PROVIDERS = KNOWN_AGENTS.map((a) => a.name);
|
|
10
|
+
|
|
11
|
+
export function listAgents(config) {
|
|
12
|
+
return ASSIGNABLE_ROLES.map((role) => {
|
|
13
|
+
const resolved = resolveRole(config, role);
|
|
14
|
+
return {
|
|
15
|
+
role,
|
|
16
|
+
provider: resolved.provider || "-",
|
|
17
|
+
model: resolved.model || "-"
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function setAgent(role, provider) {
|
|
23
|
+
if (!ASSIGNABLE_ROLES.includes(role)) {
|
|
24
|
+
throw new Error(`Unknown role "${role}". Valid roles: ${ASSIGNABLE_ROLES.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
if (!VALID_PROVIDERS.includes(provider)) {
|
|
27
|
+
const bin = await checkBinary(provider);
|
|
28
|
+
if (!bin.ok) {
|
|
29
|
+
throw new Error(`Provider "${provider}" not found. Available: ${VALID_PROVIDERS.join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { config } = await loadConfig();
|
|
34
|
+
config.roles = config.roles || {};
|
|
35
|
+
config.roles[role] = config.roles[role] || {};
|
|
36
|
+
config.roles[role].provider = provider;
|
|
37
|
+
|
|
38
|
+
const configPath = getConfigPath();
|
|
39
|
+
await writeConfig(configPath, config);
|
|
40
|
+
|
|
41
|
+
return { role, provider, configPath };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function agentsCommand({ config, subcommand, role, provider }) {
|
|
45
|
+
if (subcommand === "set") {
|
|
46
|
+
if (!role || !provider) {
|
|
47
|
+
console.log("Usage: kj agents set <role> <provider>");
|
|
48
|
+
console.log(`Roles: ${ASSIGNABLE_ROLES.join(", ")}`);
|
|
49
|
+
console.log(`Providers: ${VALID_PROVIDERS.join(", ")}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const result = await setAgent(role, provider);
|
|
53
|
+
console.log(`Set ${result.role} -> ${result.provider} (saved to ${result.configPath})`);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const agents = listAgents(config);
|
|
58
|
+
const roleWidth = Math.max(...agents.map((a) => a.role.length), 4);
|
|
59
|
+
const provWidth = Math.max(...agents.map((a) => a.provider.length), 8);
|
|
60
|
+
console.log(`${"Role".padEnd(roleWidth)} ${"Provider".padEnd(provWidth)} Model`);
|
|
61
|
+
console.log("-".repeat(roleWidth + provWidth + 10));
|
|
62
|
+
for (const a of agents) {
|
|
63
|
+
console.log(`${a.role.padEnd(roleWidth)} ${a.provider.padEnd(provWidth)} ${a.model}`);
|
|
64
|
+
}
|
|
65
|
+
return agents;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { ASSIGNABLE_ROLES, VALID_PROVIDERS };
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { runCommand } from "../utils/process.js";
|
|
2
5
|
import { exists } from "../utils/fs.js";
|
|
3
6
|
import { getConfigPath } from "../config.js";
|
|
@@ -6,9 +9,24 @@ import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
|
|
|
6
9
|
import { ensureGitRepo } from "../utils/git.js";
|
|
7
10
|
import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
|
|
8
11
|
|
|
12
|
+
function getPackageVersion() {
|
|
13
|
+
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
14
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
export async function runChecks({ config }) {
|
|
10
18
|
const checks = [];
|
|
11
19
|
|
|
20
|
+
// 0. Karajan version
|
|
21
|
+
const version = getPackageVersion();
|
|
22
|
+
checks.push({
|
|
23
|
+
name: "karajan",
|
|
24
|
+
label: "Karajan Code",
|
|
25
|
+
ok: true,
|
|
26
|
+
detail: `v${version}`,
|
|
27
|
+
fix: null
|
|
28
|
+
});
|
|
29
|
+
|
|
12
30
|
// 1. Config file
|
|
13
31
|
const configPath = getConfigPath();
|
|
14
32
|
const configExists = await exists(configPath);
|
|
@@ -442,6 +442,21 @@ export async function handleToolCall(name, args, server, extra) {
|
|
|
442
442
|
return runKjCommand({ command: "doctor", options: a });
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
if (name === "kj_agents") {
|
|
446
|
+
const action = a.action || "list";
|
|
447
|
+
if (action === "set") {
|
|
448
|
+
if (!a.role || !a.provider) {
|
|
449
|
+
return failPayload("Missing required fields: role and provider");
|
|
450
|
+
}
|
|
451
|
+
const { setAgent } = await import("../commands/agents.js");
|
|
452
|
+
const result = await setAgent(a.role, a.provider);
|
|
453
|
+
return { ok: true, ...result, message: `${result.role} now uses ${result.provider}` };
|
|
454
|
+
}
|
|
455
|
+
const config = await buildConfig(a);
|
|
456
|
+
const { listAgents } = await import("../commands/agents.js");
|
|
457
|
+
return { ok: true, agents: listAgents(config) };
|
|
458
|
+
}
|
|
459
|
+
|
|
445
460
|
if (name === "kj_config") {
|
|
446
461
|
return runKjCommand({
|
|
447
462
|
command: "config",
|
package/src/mcp/tools.js
CHANGED
|
@@ -97,7 +97,7 @@ export const tools = [
|
|
|
97
97
|
{
|
|
98
98
|
name: "kj_resume",
|
|
99
99
|
description:
|
|
100
|
-
"Resume a paused session by ID.
|
|
100
|
+
"Resume a paused, stopped, or failed session by ID. For paused sessions, provide an answer. For stopped/failed sessions, re-runs the flow from scratch. Sends real-time progress notifications via MCP logging.",
|
|
101
101
|
inputSchema: {
|
|
102
102
|
type: "object",
|
|
103
103
|
required: ["sessionId"],
|
|
@@ -136,6 +136,19 @@ export const tools = [
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: "kj_agents",
|
|
141
|
+
description: "List or change which AI agent (provider) is assigned to each pipeline role. Use action='list' to see current assignments. Use action='set' with role and provider to change it persistently (writes to kj.config.yml, no restart needed).",
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: "object",
|
|
144
|
+
properties: {
|
|
145
|
+
action: { type: "string", enum: ["list", "set"], description: "Action: list current agents or set a role's provider" },
|
|
146
|
+
role: { type: "string", description: "Role to change (e.g. coder, reviewer, planner, triage)" },
|
|
147
|
+
provider: { type: "string", description: "New provider to assign (e.g. claude, codex, gemini, aider)" },
|
|
148
|
+
kjHome: { type: "string" }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
139
152
|
{
|
|
140
153
|
name: "kj_code",
|
|
141
154
|
description: "Run coder-only mode",
|
package/src/orchestrator.js
CHANGED
|
@@ -295,7 +295,13 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
295
295
|
|
|
296
296
|
await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
|
|
297
297
|
|
|
298
|
-
|
|
298
|
+
// Explicit stop: only when the user clearly chose option 4 or typed "stop".
|
|
299
|
+
// A null/empty answer (e.g. elicitInput failure, AI timeout) defaults to
|
|
300
|
+
// "continue 5 more minutes" so the session is not killed accidentally.
|
|
301
|
+
const trimmedAnswer = (answer || "").trim();
|
|
302
|
+
const isExplicitStop = trimmedAnswer === "4" || trimmedAnswer.toLowerCase().startsWith("stop");
|
|
303
|
+
|
|
304
|
+
if (isExplicitStop) {
|
|
299
305
|
await markSessionStatus(session, "stopped");
|
|
300
306
|
emitProgress(
|
|
301
307
|
emitter,
|
|
@@ -308,12 +314,15 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
308
314
|
return { approved: false, sessionId: session.id, reason: "user_stopped", elapsed_minutes: Number(elapsedStr) };
|
|
309
315
|
}
|
|
310
316
|
|
|
311
|
-
|
|
317
|
+
// No answer or unrecognized → default to continue 5 more minutes
|
|
318
|
+
if (!trimmedAnswer) {
|
|
319
|
+
lastCheckpointAt = Date.now();
|
|
320
|
+
} else if (trimmedAnswer === "2" || trimmedAnswer.toLowerCase().startsWith("continue until")) {
|
|
312
321
|
checkpointDisabled = true;
|
|
313
|
-
} else if (
|
|
322
|
+
} else if (trimmedAnswer === "1" || trimmedAnswer.toLowerCase().includes("5 m")) {
|
|
314
323
|
lastCheckpointAt = Date.now();
|
|
315
324
|
} else {
|
|
316
|
-
const customMinutes = parseInt(
|
|
325
|
+
const customMinutes = parseInt(trimmedAnswer.replace(/\D/g, ""), 10);
|
|
317
326
|
if (customMinutes > 0) {
|
|
318
327
|
lastCheckpointAt = Date.now();
|
|
319
328
|
config.session.checkpoint_interval_minutes = customMinutes;
|
|
@@ -561,11 +570,20 @@ export async function resumeFlow({ sessionId, answer, config, logger, flags = {}
|
|
|
561
570
|
return session;
|
|
562
571
|
}
|
|
563
572
|
|
|
564
|
-
|
|
565
|
-
|
|
573
|
+
// Allow resuming "stopped" sessions (checkpoint stop) and "failed" sessions
|
|
574
|
+
const resumableStatuses = ["running", "stopped", "failed"];
|
|
575
|
+
if (!resumableStatuses.includes(session.status)) {
|
|
576
|
+
logger.info(`Session ${sessionId} has status ${session.status} — not resumable`);
|
|
566
577
|
return session;
|
|
567
578
|
}
|
|
568
579
|
|
|
580
|
+
// Mark as running again for stopped/failed sessions
|
|
581
|
+
if (session.status !== "running") {
|
|
582
|
+
logger.info(`Resuming ${session.status} session ${sessionId}`);
|
|
583
|
+
session.status = "running";
|
|
584
|
+
await saveSession(session);
|
|
585
|
+
}
|
|
586
|
+
|
|
569
587
|
// Session was paused and now resumed with answer - re-run the flow
|
|
570
588
|
const task = session.task;
|
|
571
589
|
const sessionConfig = config || session.config_snapshot;
|
package/src/prompts/coder.js
CHANGED
|
@@ -10,6 +10,14 @@ const SUBAGENT_PREAMBLE_SERENA = [
|
|
|
10
10
|
"Execute the task directly. Focus only on coding."
|
|
11
11
|
].join(" ");
|
|
12
12
|
|
|
13
|
+
const SUBPROCESS_CONSTRAINTS = [
|
|
14
|
+
"## Environment constraints",
|
|
15
|
+
"You run as a non-interactive subprocess (no stdin, no TTY).",
|
|
16
|
+
"- If the task requires a CLI wizard or interactive init (e.g. `create-react-app`, `pnpm create astro`, `npm init`), ALWAYS use non-interactive flags: `--yes`, `--no-input`, `--template <name>`, `--defaults`, etc.",
|
|
17
|
+
"- Never run a command that waits for user input — it will hang forever.",
|
|
18
|
+
"- If a task absolutely cannot be done non-interactively, say so explicitly instead of hanging."
|
|
19
|
+
].join("\n");
|
|
20
|
+
|
|
13
21
|
const SERENA_INSTRUCTIONS = [
|
|
14
22
|
"## Serena MCP — symbol-level code navigation",
|
|
15
23
|
"You have access to Serena MCP tools for efficient code navigation.",
|
|
@@ -26,7 +34,8 @@ export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary =
|
|
|
26
34
|
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
27
35
|
`Task:\n${task}`,
|
|
28
36
|
"Implement directly in the repository.",
|
|
29
|
-
"Keep changes minimal and production-ready."
|
|
37
|
+
"Keep changes minimal and production-ready.",
|
|
38
|
+
SUBPROCESS_CONSTRAINTS
|
|
30
39
|
];
|
|
31
40
|
|
|
32
41
|
if (serenaEnabled) {
|