karajan-code 1.10.0 → 1.11.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/bin/kj-tail ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # kj-tail — Colorized, filtered tail for Karajan run logs
3
+ # Usage: kj-tail [project-dir] [-v|--verbose]
4
+
5
+ set -euo pipefail
6
+
7
+ # Colors
8
+ RED='\033[0;31m'
9
+ GREEN='\033[0;32m'
10
+ YELLOW='\033[0;33m'
11
+ BLUE='\033[0;34m'
12
+ CYAN='\033[0;36m'
13
+ MAGENTA='\033[0;35m'
14
+ GRAY='\033[0;90m'
15
+ BOLD='\033[1m'
16
+ RESET='\033[0m'
17
+
18
+ VERBOSE=false
19
+ PROJECT_DIR=""
20
+
21
+ for arg in "$@"; do
22
+ case "$arg" in
23
+ -v|--verbose) VERBOSE=true ;;
24
+ *) PROJECT_DIR="$arg" ;;
25
+ esac
26
+ done
27
+
28
+ PROJECT_DIR="${PROJECT_DIR:-$(pwd)}"
29
+ LOG_FILE="${PROJECT_DIR}/.kj/run.log"
30
+
31
+ if [[ ! -f "$LOG_FILE" ]]; then
32
+ echo -e "${RED}No run.log found at ${LOG_FILE}${RESET}"
33
+ echo "Usage: kj-tail [project-dir] [-v|--verbose]"
34
+ exit 1
35
+ fi
36
+
37
+ echo -e "${BOLD}${CYAN}Karajan tail${RESET} ${GRAY}— ${LOG_FILE}${RESET}"
38
+ echo -e "${GRAY}Ctrl+C to stop. Use -v to include agent output.${RESET}"
39
+ echo ""
40
+
41
+ tail -F "$LOG_FILE" 2>/dev/null | while IFS= read -r line; do
42
+ # Strip timestamp (HH:MM:SS.mmm)
43
+ clean="${line#[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9] }"
44
+
45
+ # Strip [agent:output] tag — it's the default, no need to show it
46
+ clean="${clean/\[agent:output\] /}"
47
+
48
+ # Colorize by content
49
+ if [[ "$clean" == *"[coder:start]"* ]] || [[ "$clean" == *"[coder:done]"* ]] || [[ "$clean" == *"[coder]"* ]]; then
50
+ echo -e "${GREEN}${clean}${RESET}"
51
+ elif [[ "$clean" == *"[reviewer"* ]]; then
52
+ echo -e "${YELLOW}${clean}${RESET}"
53
+ elif [[ "$clean" == *"[sonar"* ]]; then
54
+ echo -e "${BLUE}${clean}${RESET}"
55
+ elif [[ "$clean" == *"[solomon"* ]]; then
56
+ echo -e "${MAGENTA}${clean}${RESET}"
57
+ elif [[ "$clean" == *"[iteration"* ]] || [[ "$clean" == *"[session"* ]] || [[ "$clean" == *"[kj_run]"* ]] || [[ "$clean" == *"[kj_code]"* ]]; then
58
+ echo -e "${BOLD}${CYAN}${clean}${RESET}"
59
+ elif [[ "$clean" == *"fail"* ]] || [[ "$clean" == *"error"* ]] || [[ "$clean" == *"FAIL"* ]] || [[ "$clean" == *"ERROR"* ]]; then
60
+ echo -e "${RED}${clean}${RESET}"
61
+ elif [[ "$clean" == *"[agent:heartbeat]"* ]]; then
62
+ echo -e "${GRAY}${clean}${RESET}"
63
+ elif [[ "$clean" == *"[standby]"* ]] || [[ "$clean" == *"standby"* ]]; then
64
+ echo -e "${YELLOW}${clean}${RESET}"
65
+ elif [[ "$clean" == "---"* ]]; then
66
+ echo -e "${BOLD}${clean}${RESET}"
67
+ else
68
+ echo "$clean"
69
+ fi
70
+ done
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.10.0",
3
+ "version": "1.11.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",
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "files": [
30
30
  "src/",
31
+ "bin/",
31
32
  "templates/",
32
33
  "scripts/",
33
34
  "docs/karajan-code-logo-small.png",
package/src/cli.js CHANGED
@@ -165,9 +165,10 @@ program
165
165
  program
166
166
  .command("agents [subcommand] [role] [provider]")
167
167
  .description("List or change AI agent assignments per role (e.g. kj agents set coder gemini)")
168
- .action(async (subcommand, role, provider) => {
168
+ .option("--global", "Persist change to kj.config.yml (default for CLI)")
169
+ .action(async (subcommand, role, provider, flags) => {
169
170
  await withConfig("agents", {}, async ({ config }) => {
170
- await agentsCommand({ config, subcommand: subcommand || "list", role, provider });
171
+ await agentsCommand({ config, subcommand: subcommand || "list", role, provider, global: flags.global });
171
172
  });
172
173
  });
173
174
 
@@ -1,4 +1,4 @@
1
- import { loadConfig, writeConfig, getConfigPath, resolveRole } from "../config.js";
1
+ import { loadConfig, writeConfig, getConfigPath, getProjectConfigPath, loadProjectConfig, resolveRole } from "../config.js";
2
2
  import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
3
3
 
4
4
  const ASSIGNABLE_ROLES = [
@@ -8,18 +8,24 @@ const ASSIGNABLE_ROLES = [
8
8
 
9
9
  const VALID_PROVIDERS = KNOWN_AGENTS.map((a) => a.name);
10
10
 
11
- export function listAgents(config) {
11
+ export function listAgents(config, sessionOverrides = {}, projectConfig = null) {
12
12
  return ASSIGNABLE_ROLES.map((role) => {
13
13
  const resolved = resolveRole(config, role);
14
+ const sessionProvider = sessionOverrides[role];
15
+ const projectProvider = projectConfig?.roles?.[role]?.provider;
16
+ let scope = "global";
17
+ if (sessionProvider) scope = "session";
18
+ else if (projectProvider) scope = "project";
14
19
  return {
15
20
  role,
16
- provider: resolved.provider || "-",
17
- model: resolved.model || "-"
21
+ provider: sessionProvider || resolved.provider || "-",
22
+ model: resolved.model || "-",
23
+ scope
18
24
  };
19
25
  });
20
26
  }
21
27
 
22
- export async function setAgent(role, provider) {
28
+ export async function setAgent(role, provider, { global: isGlobal = false } = {}) {
23
29
  if (!ASSIGNABLE_ROLES.includes(role)) {
24
30
  throw new Error(`Unknown role "${role}". Valid roles: ${ASSIGNABLE_ROLES.join(", ")}`);
25
31
  }
@@ -30,37 +36,55 @@ export async function setAgent(role, provider) {
30
36
  }
31
37
  }
32
38
 
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);
39
+ if (isGlobal) {
40
+ const { config } = await loadConfig();
41
+ config.roles = config.roles || {};
42
+ config.roles[role] = config.roles[role] || {};
43
+ config.roles[role].provider = provider;
44
+ const configPath = getConfigPath();
45
+ await writeConfig(configPath, config);
46
+ return { role, provider, scope: "global", configPath };
47
+ }
40
48
 
41
- return { role, provider, configPath };
49
+ // Session scope try MCP session override first
50
+ try {
51
+ const { setSessionOverride } = await import("../mcp/preflight.js");
52
+ setSessionOverride(role, provider);
53
+ return { role, provider, scope: "session" };
54
+ } catch {
55
+ // preflight module not available (CLI mode) — write to project config
56
+ const projectConfigPath = getProjectConfigPath();
57
+ const projectConfig = (await loadProjectConfig()) || {};
58
+ projectConfig.roles = projectConfig.roles || {};
59
+ projectConfig.roles[role] = projectConfig.roles[role] || {};
60
+ projectConfig.roles[role].provider = provider;
61
+ await writeConfig(projectConfigPath, projectConfig);
62
+ return { role, provider, scope: "project", configPath: projectConfigPath };
63
+ }
42
64
  }
43
65
 
44
- export async function agentsCommand({ config, subcommand, role, provider }) {
66
+ export async function agentsCommand({ config, subcommand, role, provider, global: isGlobal }) {
45
67
  if (subcommand === "set") {
46
68
  if (!role || !provider) {
47
- console.log("Usage: kj agents set <role> <provider>");
69
+ console.log("Usage: kj agents set <role> <provider> [--global]");
48
70
  console.log(`Roles: ${ASSIGNABLE_ROLES.join(", ")}`);
49
71
  console.log(`Providers: ${VALID_PROVIDERS.join(", ")}`);
50
72
  return;
51
73
  }
52
- const result = await setAgent(role, provider);
53
- console.log(`Set ${result.role} -> ${result.provider} (saved to ${result.configPath})`);
74
+ const result = await setAgent(role, provider, { global: isGlobal ?? true });
75
+ console.log(`Set ${result.role} -> ${result.provider} (scope: ${result.scope})`);
54
76
  return result;
55
77
  }
56
78
 
57
- const agents = listAgents(config);
79
+ const projectConfig = await loadProjectConfig();
80
+ const agents = listAgents(config, {}, projectConfig);
58
81
  const roleWidth = Math.max(...agents.map((a) => a.role.length), 4);
59
82
  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));
83
+ const scopeWidth = Math.max(...agents.map((a) => a.scope.length), 5);
84
+ console.log(`${"Role".padEnd(roleWidth)} ${"Provider".padEnd(provWidth)} ${"Scope".padEnd(scopeWidth)} Model`);
85
+ console.log("-".repeat(roleWidth + provWidth + scopeWidth + 14));
62
86
  for (const a of agents) {
63
- console.log(`${a.role.padEnd(roleWidth)} ${a.provider.padEnd(provWidth)} ${a.model}`);
87
+ console.log(`${a.role.padEnd(roleWidth)} ${a.provider.padEnd(provWidth)} ${a.scope.padEnd(scopeWidth)} ${a.model}`);
64
88
  }
65
89
  return agents;
66
90
  }
@@ -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, buildTaskFromCard, buildCompletionUpdates } from "../planning-game/adapter.js";
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 context ---
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
- let pgCard = null;
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
  }
package/src/config.js CHANGED
@@ -21,11 +21,11 @@ const DEFAULTS = {
21
21
  pipeline: {
22
22
  planner: { enabled: false },
23
23
  refactorer: { enabled: false },
24
- solomon: { enabled: false },
24
+ solomon: { enabled: true },
25
25
  researcher: { enabled: false },
26
- tester: { enabled: false },
27
- security: { enabled: false },
28
- triage: { enabled: false }
26
+ tester: { enabled: true },
27
+ security: { enabled: true },
28
+ triage: { enabled: true }
29
29
  },
30
30
  review_mode: "standard",
31
31
  max_iterations: 5,
@@ -155,6 +155,19 @@ export function getConfigPath() {
155
155
  return path.join(getKarajanHome(), "kj.config.yml");
156
156
  }
157
157
 
158
+ export function getProjectConfigPath(projectDir = process.cwd()) {
159
+ return path.join(projectDir, ".karajan", "kj.config.yml");
160
+ }
161
+
162
+ export async function loadProjectConfig(projectDir = process.cwd()) {
163
+ const projectConfigPath = getProjectConfigPath(projectDir);
164
+ if (!(await exists(projectConfigPath))) {
165
+ return null;
166
+ }
167
+ const raw = await fs.readFile(projectConfigPath, "utf8");
168
+ return yaml.load(raw) || {};
169
+ }
170
+
158
171
  async function loadProjectPricingOverrides(projectDir = process.cwd()) {
159
172
  const projectConfigPath = path.join(projectDir, ".karajan.yml");
160
173
  if (!(await exists(projectConfigPath))) {
@@ -171,24 +184,32 @@ async function loadProjectPricingOverrides(projectDir = process.cwd()) {
171
184
  return pricing;
172
185
  }
173
186
 
174
- export async function loadConfig() {
187
+ export async function loadConfig(projectDir) {
175
188
  const configPath = getConfigPath();
176
- const projectPricing = await loadProjectPricingOverrides();
177
- if (!(await exists(configPath))) {
178
- const baseDefaults = mergeDeep(DEFAULTS, {});
179
- if (projectPricing) {
180
- baseDefaults.budget = mergeDeep(baseDefaults.budget || {}, { pricing: projectPricing });
181
- }
182
- return { config: baseDefaults, path: configPath, exists: false };
189
+ const projectPricing = await loadProjectPricingOverrides(projectDir);
190
+
191
+ // Load global config
192
+ let globalConfig = {};
193
+ const globalExists = await exists(configPath);
194
+ if (globalExists) {
195
+ const raw = await fs.readFile(configPath, "utf8");
196
+ globalConfig = yaml.load(raw) || {};
197
+ }
198
+
199
+ // Load project config (.karajan/kj.config.yml)
200
+ const projectConfig = await loadProjectConfig(projectDir);
201
+
202
+ // Merge: DEFAULTS < global < project
203
+ let merged = mergeDeep(DEFAULTS, globalConfig);
204
+ if (projectConfig) {
205
+ merged = mergeDeep(merged, projectConfig);
183
206
  }
184
207
 
185
- const raw = await fs.readFile(configPath, "utf8");
186
- const parsed = yaml.load(raw) || {};
187
- const merged = mergeDeep(DEFAULTS, parsed);
188
208
  if (projectPricing) {
189
209
  merged.budget = mergeDeep(merged.budget || {}, { pricing: projectPricing });
190
210
  }
191
- return { config: merged, path: configPath, exists: true };
211
+
212
+ return { config: merged, path: configPath, exists: globalExists, hasProjectConfig: !!projectConfig };
192
213
  }
193
214
 
194
215
  export async function writeConfig(configPath, config) {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Session-scoped preflight state.
3
+ * Lives in memory — dies when the MCP server restarts.
4
+ */
5
+ let preflightAcked = false;
6
+ let sessionOverrides = {};
7
+
8
+ export function isPreflightAcked() {
9
+ return preflightAcked;
10
+ }
11
+
12
+ export function ackPreflight(overrides = {}) {
13
+ preflightAcked = true;
14
+ sessionOverrides = { ...overrides };
15
+ }
16
+
17
+ export function getSessionOverrides() {
18
+ return { ...sessionOverrides };
19
+ }
20
+
21
+ export function setSessionOverride(key, value) {
22
+ sessionOverrides[key] = value;
23
+ }
24
+
25
+ export function resetPreflight() {
26
+ preflightAcked = false;
27
+ sessionOverrides = {};
28
+ }
@@ -22,6 +22,7 @@ import { computeBaseRef, generateDiff } from "../review/diff-generator.js";
22
22
  import { resolveReviewProfile } from "../review/profiles.js";
23
23
  import { createRunLog, readRunLog } from "../utils/run-log.js";
24
24
  import { currentBranch } from "../utils/git.js";
25
+ import { isPreflightAcked, ackPreflight, getSessionOverrides } from "./preflight.js";
25
26
 
26
27
  /**
27
28
  * Resolve the user's project directory via MCP roots.
@@ -249,7 +250,7 @@ export async function handleResumeDirect(a, server, extra) {
249
250
  return { ok: true, ...result };
250
251
  }
251
252
 
252
- function buildDirectEmitter(server, runLog) {
253
+ function buildDirectEmitter(server, runLog, extra) {
253
254
  const emitter = new EventEmitter();
254
255
  emitter.on("progress", (event) => {
255
256
  try {
@@ -260,6 +261,8 @@ function buildDirectEmitter(server, runLog) {
260
261
  } catch { /* best-effort */ }
261
262
  if (runLog) runLog.logEvent(event);
262
263
  });
264
+ const progressNotifier = buildProgressNotifier(extra);
265
+ if (progressNotifier) emitter.on("progress", progressNotifier);
263
266
  return emitter;
264
267
  }
265
268
 
@@ -282,7 +285,7 @@ export async function handlePlanDirect(a, server, extra) {
282
285
  runLog.logText(
283
286
  `[kj_plan] started — provider=${plannerRole.provider}, max_silence=${silenceTimeoutMs ? `${Math.round(silenceTimeoutMs / 1000)}s` : "disabled"}, max_runtime=${plannerTimeoutMs ? `${Math.round(plannerTimeoutMs / 1000)}s` : "disabled"}`
284
287
  );
285
- const emitter = buildDirectEmitter(server, runLog);
288
+ const emitter = buildDirectEmitter(server, runLog, extra);
286
289
  const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
287
290
  const onOutput = ({ stream, line }) => {
288
291
  emitter.emit("progress", { type: "agent:output", stage: "planner", message: line, detail: { stream, agent: plannerRole.provider } });
@@ -339,7 +342,7 @@ export async function handleCodeDirect(a, server, extra) {
339
342
  const projectDir = await resolveProjectDir(server);
340
343
  const runLog = createRunLog(projectDir);
341
344
  runLog.logText(`[kj_code] started — provider=${coderRole.provider}`);
342
- const emitter = buildDirectEmitter(server, runLog);
345
+ const emitter = buildDirectEmitter(server, runLog, extra);
343
346
  const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
344
347
  const onOutput = ({ stream, line }) => {
345
348
  emitter.emit("progress", { type: "agent:output", stage: "coder", message: line, detail: { stream, agent: coderRole.provider } });
@@ -388,7 +391,7 @@ export async function handleReviewDirect(a, server, extra) {
388
391
  const projectDir = await resolveProjectDir(server);
389
392
  const runLog = createRunLog(projectDir);
390
393
  runLog.logText(`[kj_review] started — provider=${reviewerRole.provider}`);
391
- const emitter = buildDirectEmitter(server, runLog);
394
+ const emitter = buildDirectEmitter(server, runLog, extra);
392
395
  const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
393
396
  const onOutput = ({ stream, line }) => {
394
397
  emitter.emit("progress", { type: "agent:output", stage: "reviewer", message: line, detail: { stream, agent: reviewerRole.provider } });
@@ -449,12 +452,68 @@ export async function handleToolCall(name, args, server, extra) {
449
452
  return failPayload("Missing required fields: role and provider");
450
453
  }
451
454
  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}` };
455
+ const result = await setAgent(a.role, a.provider, { global: false });
456
+ return { ok: true, ...result, message: `${result.role} now uses ${result.provider} (scope: ${result.scope})` };
454
457
  }
455
458
  const config = await buildConfig(a);
456
459
  const { listAgents } = await import("../commands/agents.js");
457
- return { ok: true, agents: listAgents(config) };
460
+ const sessionOvr = getSessionOverrides();
461
+ return { ok: true, agents: listAgents(config, sessionOvr) };
462
+ }
463
+
464
+ if (name === "kj_preflight") {
465
+ const overrides = {};
466
+ const AGENT_ROLES = ["coder", "reviewer", "tester", "security", "solomon"];
467
+
468
+ // Apply explicit param overrides
469
+ for (const role of AGENT_ROLES) {
470
+ if (a[role]) overrides[role] = a[role];
471
+ }
472
+ if (a.enableTester !== undefined) overrides.enableTester = a.enableTester;
473
+ if (a.enableSecurity !== undefined) overrides.enableSecurity = a.enableSecurity;
474
+
475
+ // Parse natural-language humanResponse for agent changes
476
+ const resp = (a.humanResponse || "").toLowerCase();
477
+ if (resp !== "ok") {
478
+ // Match patterns like "use gemini as coder", "coder: claude", "set reviewer to codex"
479
+ for (const role of AGENT_ROLES) {
480
+ const patterns = [
481
+ new RegExp(`use\\s+(\\w+)\\s+(?:as|for)\\s+${role}`, "i"),
482
+ new RegExp(`${role}\\s*[:=]\\s*(\\w+)`, "i"),
483
+ new RegExp(`set\\s+${role}\\s+(?:to|=)\\s*(\\w+)`, "i")
484
+ ];
485
+ for (const pat of patterns) {
486
+ const m = (a.humanResponse || "").match(pat);
487
+ if (m && !overrides[role]) {
488
+ overrides[role] = m[1];
489
+ break;
490
+ }
491
+ }
492
+ }
493
+ }
494
+
495
+ ackPreflight(overrides);
496
+
497
+ const config = await buildConfig(a);
498
+ const { listAgents } = await import("../commands/agents.js");
499
+ const agents = listAgents(config);
500
+ const lines = agents
501
+ .filter(ag => ag.provider !== "-")
502
+ .map(ag => {
503
+ const ovr = overrides[ag.role] ? ` -> ${overrides[ag.role]} (session override)` : "";
504
+ return ` ${ag.role}: ${ag.provider}${ag.model !== "-" ? ` (${ag.model})` : ""}${ovr}`;
505
+ });
506
+ const overrideLines = Object.entries(overrides)
507
+ .filter(([k]) => !AGENT_ROLES.includes(k))
508
+ .map(([k, v]) => ` ${k}: ${v}`);
509
+ const allLines = [...lines, ...overrideLines];
510
+
511
+ return {
512
+ ok: true,
513
+ message: `Preflight acknowledged. Agent configuration confirmed.`,
514
+ config: allLines.join("\n"),
515
+ overrides
516
+ };
458
517
  }
459
518
 
460
519
  if (name === "kj_config") {
@@ -506,6 +565,29 @@ export async function handleToolCall(name, args, server, extra) {
506
565
  if (!a.task) {
507
566
  return failPayload("Missing required field: task");
508
567
  }
568
+ if (!isPreflightAcked()) {
569
+ const { config } = await loadConfig();
570
+ const { listAgents } = await import("../commands/agents.js");
571
+ const agents = listAgents(config);
572
+ const agentSummary = agents
573
+ .filter(ag => ag.provider !== "-")
574
+ .map(ag => ` ${ag.role}: ${ag.provider}${ag.model !== "-" ? ` (${ag.model})` : ""}`)
575
+ .join("\n");
576
+ return responseText({
577
+ ok: false,
578
+ preflightRequired: true,
579
+ message: `PREFLIGHT REQUIRED\n\nCurrent agent configuration:\n${agentSummary}\n\nAsk the human to confirm or adjust this configuration, then call kj_preflight with their response.\n\nDo NOT pass coder/reviewer parameters to kj_run — use kj_preflight to set them.`
580
+ });
581
+ }
582
+ // Apply session overrides, ignoring agent params from tool call
583
+ const sessionOvr = getSessionOverrides();
584
+ if (sessionOvr.coder) { a.coder = sessionOvr.coder; }
585
+ if (sessionOvr.reviewer) { a.reviewer = sessionOvr.reviewer; }
586
+ if (sessionOvr.tester) { a.tester = sessionOvr.tester; }
587
+ if (sessionOvr.security) { a.security = sessionOvr.security; }
588
+ if (sessionOvr.solomon) { a.solomon = sessionOvr.solomon; }
589
+ if (sessionOvr.enableTester !== undefined) { a.enableTester = sessionOvr.enableTester; }
590
+ if (sessionOvr.enableSecurity !== undefined) { a.enableSecurity = sessionOvr.enableSecurity; }
509
591
  return handleRunDirect(a, server, extra);
510
592
  }
511
593
 
@@ -513,6 +595,23 @@ export async function handleToolCall(name, args, server, extra) {
513
595
  if (!a.task) {
514
596
  return failPayload("Missing required field: task");
515
597
  }
598
+ if (!isPreflightAcked()) {
599
+ const { config } = await loadConfig();
600
+ const { listAgents } = await import("../commands/agents.js");
601
+ const agents = listAgents(config);
602
+ const agentSummary = agents
603
+ .filter(ag => ag.provider !== "-")
604
+ .map(ag => ` ${ag.role}: ${ag.provider}${ag.model !== "-" ? ` (${ag.model})` : ""}`)
605
+ .join("\n");
606
+ return responseText({
607
+ ok: false,
608
+ preflightRequired: true,
609
+ message: `PREFLIGHT REQUIRED\n\nCurrent agent configuration:\n${agentSummary}\n\nAsk the human to confirm or adjust this configuration, then call kj_preflight with their response.\n\nDo NOT pass coder/reviewer parameters to kj_code — use kj_preflight to set them.`
610
+ });
611
+ }
612
+ // Apply session overrides, ignoring agent params from tool call
613
+ const sessionOvr = getSessionOverrides();
614
+ if (sessionOvr.coder) { a.coder = sessionOvr.coder; }
516
615
  return handleCodeDirect(a, server, extra);
517
616
  }
518
617
 
package/src/mcp/tools.js CHANGED
@@ -149,6 +149,24 @@ export const tools = [
149
149
  }
150
150
  }
151
151
  },
152
+ {
153
+ name: "kj_preflight",
154
+ description: "Confirm or adjust agent configuration before first kj_run/kj_code. REQUIRED before running any task via MCP. Show the config to the human, get their confirmation or adjustments, then call this tool with their response.",
155
+ inputSchema: {
156
+ type: "object",
157
+ required: ["humanResponse"],
158
+ properties: {
159
+ humanResponse: { type: "string", description: "The human's response: 'ok' to confirm defaults, or specific changes like 'use gemini as coder'" },
160
+ coder: { type: "string", description: "Override coder for this session" },
161
+ reviewer: { type: "string", description: "Override reviewer for this session" },
162
+ tester: { type: "string", description: "Override tester for this session" },
163
+ security: { type: "string", description: "Override security for this session" },
164
+ solomon: { type: "string", description: "Override solomon for this session" },
165
+ enableTester: { type: "boolean", description: "Enable/disable tester for this session" },
166
+ enableSecurity: { type: "boolean", description: "Enable/disable security for this session" }
167
+ }
168
+ }
169
+ },
152
170
  {
153
171
  name: "kj_code",
154
172
  description: "Run coder-only mode",
@@ -180,7 +198,7 @@ export const tools = [
180
198
  },
181
199
  {
182
200
  name: "kj_status",
183
- description: "Show real-time log of the current or last Karajan run. Use this to monitor progress while kj_run/kj_plan/kj_code is executing. Reads from .kj/run.log in the project directory.",
201
+ description: "Show real-time status and log of the current or last Karajan run. Returns a parsed status (current stage, agent, iteration, errors) plus recent log lines. Use this to monitor progress while kj_run/kj_plan/kj_code is executing.",
184
202
  inputSchema: {
185
203
  type: "object",
186
204
  properties: {