karajan-code 1.9.6 → 1.10.1
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/commands/run.js +3 -35
- package/src/mcp/server-handlers.js +15 -0
- package/src/mcp/tools.js +14 -1
- package/src/orchestrator.js +74 -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);
|
package/src/commands/run.js
CHANGED
|
@@ -4,7 +4,7 @@ import { assertAgentsAvailable } from "../agents/availability.js";
|
|
|
4
4
|
import { createActivityLog } from "../activity-log.js";
|
|
5
5
|
import { printHeader, printEvent } from "../utils/display.js";
|
|
6
6
|
import { resolveRole } from "../config.js";
|
|
7
|
-
import { parseCardId
|
|
7
|
+
import { parseCardId } from "../planning-game/adapter.js";
|
|
8
8
|
|
|
9
9
|
export async function runCommandHandler({ task, config, logger, flags }) {
|
|
10
10
|
const requiredProviders = [
|
|
@@ -22,24 +22,10 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
22
22
|
if (config.pipeline?.security?.enabled) requiredProviders.push(resolveRole(config, "security").provider);
|
|
23
23
|
await assertAgentsAvailable(requiredProviders);
|
|
24
24
|
|
|
25
|
-
// --- Planning Game: resolve card
|
|
25
|
+
// --- Planning Game: resolve card ID ---
|
|
26
26
|
const pgCardId = flags?.pgTask || parseCardId(task);
|
|
27
27
|
const pgProject = flags?.pgProject || config.planning_game?.project_id || null;
|
|
28
|
-
|
|
29
|
-
let enrichedTask = task;
|
|
30
|
-
|
|
31
|
-
if (pgCardId && pgProject && config.planning_game?.enabled !== false) {
|
|
32
|
-
try {
|
|
33
|
-
const { fetchCard, updateCard } = await import("../planning-game/client.js");
|
|
34
|
-
pgCard = await fetchCard({ projectId: pgProject, cardId: pgCardId });
|
|
35
|
-
if (pgCard) {
|
|
36
|
-
enrichedTask = buildTaskFromCard(pgCard);
|
|
37
|
-
logger.info(`Planning Game: loaded card ${pgCardId} from project ${pgProject}`);
|
|
38
|
-
}
|
|
39
|
-
} catch (err) {
|
|
40
|
-
logger.warn(`Planning Game: could not load card ${pgCardId}: ${err.message}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
28
|
+
const enrichedTask = task;
|
|
43
29
|
|
|
44
30
|
const jsonMode = flags?.json;
|
|
45
31
|
|
|
@@ -65,26 +51,8 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
65
51
|
printHeader({ task: enrichedTask, config });
|
|
66
52
|
}
|
|
67
53
|
|
|
68
|
-
const startDate = new Date().toISOString();
|
|
69
54
|
const result = await runFlow({ task: enrichedTask, config, logger, flags, emitter, pgTaskId: pgCardId || null, pgProject: pgProject || null });
|
|
70
55
|
|
|
71
|
-
// --- Planning Game: update card on completion ---
|
|
72
|
-
if (pgCard && pgProject && result?.approved) {
|
|
73
|
-
try {
|
|
74
|
-
const { updateCard } = await import("../planning-game/client.js");
|
|
75
|
-
const updates = buildCompletionUpdates({
|
|
76
|
-
approved: true,
|
|
77
|
-
commits: result.git?.commits || [],
|
|
78
|
-
startDate,
|
|
79
|
-
codeveloper: config.planning_game?.codeveloper || null
|
|
80
|
-
});
|
|
81
|
-
await updateCard({ projectId: pgProject, cardId: pgCardId, firebaseId: pgCard.firebaseId, updates });
|
|
82
|
-
logger.info(`Planning Game: updated ${pgCardId} to "${updates.status}"`);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
logger.warn(`Planning Game: could not update card ${pgCardId}: ${err.message}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
56
|
if (jsonMode) {
|
|
89
57
|
console.log(JSON.stringify(result, null, 2));
|
|
90
58
|
}
|
|
@@ -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
|
@@ -157,6 +157,32 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
157
157
|
|
|
158
158
|
eventBase.sessionId = session.id;
|
|
159
159
|
|
|
160
|
+
// --- Planning Game: mark card as In Progress ---
|
|
161
|
+
let pgCard = null;
|
|
162
|
+
if (pgTaskId && pgProject && config.planning_game?.enabled !== false) {
|
|
163
|
+
try {
|
|
164
|
+
const { fetchCard, updateCard } = await import("./planning-game/client.js");
|
|
165
|
+
pgCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId });
|
|
166
|
+
if (pgCard && pgCard.status !== "In Progress") {
|
|
167
|
+
await updateCard({
|
|
168
|
+
projectId: pgProject,
|
|
169
|
+
cardId: pgTaskId,
|
|
170
|
+
firebaseId: pgCard.firebaseId,
|
|
171
|
+
updates: {
|
|
172
|
+
status: "In Progress",
|
|
173
|
+
startDate: new Date().toISOString(),
|
|
174
|
+
developer: "dev_016",
|
|
175
|
+
codeveloper: config.planning_game?.codeveloper || null
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
logger.info(`Planning Game: ${pgTaskId} → In Progress`);
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
logger.warn(`Planning Game: could not update ${pgTaskId}: ${err.message}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
session.pg_card = pgCard || null;
|
|
185
|
+
|
|
160
186
|
emitProgress(
|
|
161
187
|
emitter,
|
|
162
188
|
makeEvent("session:start", eventBase, {
|
|
@@ -295,7 +321,13 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
295
321
|
|
|
296
322
|
await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
|
|
297
323
|
|
|
298
|
-
|
|
324
|
+
// Explicit stop: only when the user clearly chose option 4 or typed "stop".
|
|
325
|
+
// A null/empty answer (e.g. elicitInput failure, AI timeout) defaults to
|
|
326
|
+
// "continue 5 more minutes" so the session is not killed accidentally.
|
|
327
|
+
const trimmedAnswer = (answer || "").trim();
|
|
328
|
+
const isExplicitStop = trimmedAnswer === "4" || trimmedAnswer.toLowerCase().startsWith("stop");
|
|
329
|
+
|
|
330
|
+
if (isExplicitStop) {
|
|
299
331
|
await markSessionStatus(session, "stopped");
|
|
300
332
|
emitProgress(
|
|
301
333
|
emitter,
|
|
@@ -308,12 +340,15 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
308
340
|
return { approved: false, sessionId: session.id, reason: "user_stopped", elapsed_minutes: Number(elapsedStr) };
|
|
309
341
|
}
|
|
310
342
|
|
|
311
|
-
|
|
343
|
+
// No answer or unrecognized → default to continue 5 more minutes
|
|
344
|
+
if (!trimmedAnswer) {
|
|
345
|
+
lastCheckpointAt = Date.now();
|
|
346
|
+
} else if (trimmedAnswer === "2" || trimmedAnswer.toLowerCase().startsWith("continue until")) {
|
|
312
347
|
checkpointDisabled = true;
|
|
313
|
-
} else if (
|
|
348
|
+
} else if (trimmedAnswer === "1" || trimmedAnswer.toLowerCase().includes("5 m")) {
|
|
314
349
|
lastCheckpointAt = Date.now();
|
|
315
350
|
} else {
|
|
316
|
-
const customMinutes = parseInt(
|
|
351
|
+
const customMinutes = parseInt(trimmedAnswer.replace(/\D/g, ""), 10);
|
|
317
352
|
if (customMinutes > 0) {
|
|
318
353
|
lastCheckpointAt = Date.now();
|
|
319
354
|
config.session.checkpoint_interval_minutes = customMinutes;
|
|
@@ -484,6 +519,30 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
484
519
|
}
|
|
485
520
|
session.budget = budgetSummary();
|
|
486
521
|
await markSessionStatus(session, "approved");
|
|
522
|
+
|
|
523
|
+
// --- Planning Game: mark card as To Validate ---
|
|
524
|
+
if (pgCard && pgProject) {
|
|
525
|
+
try {
|
|
526
|
+
const { updateCard } = await import("./planning-game/client.js");
|
|
527
|
+
const { buildCompletionUpdates } = await import("./planning-game/adapter.js");
|
|
528
|
+
const pgUpdates = buildCompletionUpdates({
|
|
529
|
+
approved: true,
|
|
530
|
+
commits: gitResult?.commits || [],
|
|
531
|
+
startDate: session.pg_card?.startDate || session.created_at,
|
|
532
|
+
codeveloper: config.planning_game?.codeveloper || null
|
|
533
|
+
});
|
|
534
|
+
await updateCard({
|
|
535
|
+
projectId: pgProject,
|
|
536
|
+
cardId: session.pg_task_id,
|
|
537
|
+
firebaseId: pgCard.firebaseId,
|
|
538
|
+
updates: pgUpdates
|
|
539
|
+
});
|
|
540
|
+
logger.info(`Planning Game: ${session.pg_task_id} → To Validate`);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
logger.warn(`Planning Game: could not update ${session.pg_task_id} on completion: ${err.message}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
487
546
|
emitProgress(
|
|
488
547
|
emitter,
|
|
489
548
|
makeEvent("session:end", { ...eventBase, stage: "done" }, {
|
|
@@ -561,11 +620,20 @@ export async function resumeFlow({ sessionId, answer, config, logger, flags = {}
|
|
|
561
620
|
return session;
|
|
562
621
|
}
|
|
563
622
|
|
|
564
|
-
|
|
565
|
-
|
|
623
|
+
// Allow resuming "stopped" sessions (checkpoint stop) and "failed" sessions
|
|
624
|
+
const resumableStatuses = ["running", "stopped", "failed"];
|
|
625
|
+
if (!resumableStatuses.includes(session.status)) {
|
|
626
|
+
logger.info(`Session ${sessionId} has status ${session.status} — not resumable`);
|
|
566
627
|
return session;
|
|
567
628
|
}
|
|
568
629
|
|
|
630
|
+
// Mark as running again for stopped/failed sessions
|
|
631
|
+
if (session.status !== "running") {
|
|
632
|
+
logger.info(`Resuming ${session.status} session ${sessionId}`);
|
|
633
|
+
session.status = "running";
|
|
634
|
+
await saveSession(session);
|
|
635
|
+
}
|
|
636
|
+
|
|
569
637
|
// Session was paused and now resumed with answer - re-run the flow
|
|
570
638
|
const task = session.task;
|
|
571
639
|
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) {
|