karajan-code 1.10.1 → 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 +70 -0
- package/package.json +2 -1
- package/src/cli.js +3 -2
- package/src/commands/agents.js +45 -21
- package/src/config.js +37 -16
- package/src/mcp/preflight.js +28 -0
- package/src/mcp/server-handlers.js +106 -7
- package/src/mcp/tools.js +19 -1
- package/src/orchestrator/iteration-stages.js +30 -43
- package/src/orchestrator/solomon-rules.js +138 -0
- package/src/orchestrator/standby.js +70 -0
- package/src/orchestrator.js +107 -0
- package/src/prompts/triage.js +61 -0
- package/src/roles/triage-role.js +2 -26
- package/src/utils/display.js +21 -0
- package/src/utils/rate-limit-detector.js +65 -4
- package/src/utils/run-log.js +75 -1
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.
|
|
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
|
-
.
|
|
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
|
|
package/src/commands/agents.js
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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} (
|
|
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
|
|
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
|
-
|
|
61
|
-
console.log("
|
|
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
|
}
|
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:
|
|
24
|
+
solomon: { enabled: true },
|
|
25
25
|
researcher: { enabled: false },
|
|
26
|
-
tester: { enabled:
|
|
27
|
-
security: { enabled:
|
|
28
|
-
triage: { enabled:
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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: {
|
|
@@ -93,21 +93,16 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// No fallback or fallback also failed —
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
message: question,
|
|
107
|
-
detail: { agent: coderRole.provider, rateLimitMessage: rateLimitCheck.message, sessionId: session.id }
|
|
108
|
-
})
|
|
109
|
-
);
|
|
110
|
-
return { action: "pause", result: { paused: true, sessionId: session.id, question, context: "rate_limit" } };
|
|
96
|
+
// No fallback or fallback also failed — enter standby
|
|
97
|
+
return {
|
|
98
|
+
action: "standby",
|
|
99
|
+
standbyInfo: {
|
|
100
|
+
agent: coderRole.provider,
|
|
101
|
+
cooldownMs: rateLimitCheck.cooldownMs,
|
|
102
|
+
cooldownUntil: rateLimitCheck.cooldownUntil,
|
|
103
|
+
message: rateLimitCheck.message
|
|
104
|
+
}
|
|
105
|
+
};
|
|
111
106
|
}
|
|
112
107
|
|
|
113
108
|
await markSessionStatus(session, "failed");
|
|
@@ -167,20 +162,16 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
|
|
|
167
162
|
});
|
|
168
163
|
|
|
169
164
|
if (rateLimitCheck.isRateLimit) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
detail: { agent: refactorerRole.provider, rateLimitMessage: rateLimitCheck.message, sessionId: session.id }
|
|
181
|
-
})
|
|
182
|
-
);
|
|
183
|
-
return { action: "pause", result: { paused: true, sessionId: session.id, question, context: "rate_limit" } };
|
|
165
|
+
// Enter standby instead of pausing
|
|
166
|
+
return {
|
|
167
|
+
action: "standby",
|
|
168
|
+
standbyInfo: {
|
|
169
|
+
agent: refactorerRole.provider,
|
|
170
|
+
cooldownMs: rateLimitCheck.cooldownMs,
|
|
171
|
+
cooldownUntil: rateLimitCheck.cooldownUntil,
|
|
172
|
+
message: rateLimitCheck.message
|
|
173
|
+
}
|
|
174
|
+
};
|
|
184
175
|
}
|
|
185
176
|
|
|
186
177
|
await markSessionStatus(session, "failed");
|
|
@@ -451,20 +442,16 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
451
442
|
});
|
|
452
443
|
|
|
453
444
|
if (rateLimitCheck.isRateLimit) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
detail: { agent: reviewerRole.provider, rateLimitMessage: rateLimitCheck.message, sessionId: session.id }
|
|
465
|
-
})
|
|
466
|
-
);
|
|
467
|
-
return { action: "pause", result: { paused: true, sessionId: session.id, question, context: "rate_limit" } };
|
|
445
|
+
// Enter standby instead of pausing
|
|
446
|
+
return {
|
|
447
|
+
action: "standby",
|
|
448
|
+
standbyInfo: {
|
|
449
|
+
agent: reviewerRole.provider,
|
|
450
|
+
cooldownMs: rateLimitCheck.cooldownMs,
|
|
451
|
+
cooldownUntil: rateLimitCheck.cooldownUntil,
|
|
452
|
+
message: rateLimitCheck.message
|
|
453
|
+
}
|
|
454
|
+
};
|
|
468
455
|
}
|
|
469
456
|
|
|
470
457
|
await markSessionStatus(session, "failed");
|