karajan-code 1.10.1 → 1.11.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/README.md +25 -9
- package/bin/kj-tail +70 -0
- package/docs/README.es.md +8 -5
- package/package.json +2 -1
- package/src/agents/claude-agent.js +12 -2
- 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/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Instead of running one AI agent and manually reviewing its output, `kj` chains a
|
|
|
30
30
|
**Key features:**
|
|
31
31
|
- **Multi-agent pipeline** with 11 configurable roles
|
|
32
32
|
- **4 AI agents supported**: Claude, Codex, Gemini, Aider
|
|
33
|
-
- **MCP server** with
|
|
33
|
+
- **MCP server** with 15 tools — use `kj` from Claude, Codex, or any MCP-compatible host without leaving your agent. [See MCP setup](#mcp-server)
|
|
34
34
|
- **TDD enforcement** — test changes required when source files change
|
|
35
35
|
- **SonarQube integration** — static analysis with quality gate enforcement (requires [Docker](#requirements))
|
|
36
36
|
- **Review profiles** — standard, strict, relaxed, paranoid
|
|
@@ -44,6 +44,9 @@ Instead of running one AI agent and manually reviewing its output, `kj` chains a
|
|
|
44
44
|
- **Retry with backoff** — automatic recovery from transient API errors (429, 5xx) with exponential backoff and jitter
|
|
45
45
|
- **Pipeline stage tracker** — cumulative progress view during `kj_run` showing which stages are done, running, or pending — both in CLI and via MCP events for real-time host rendering
|
|
46
46
|
- **Planner observability guardrails** — continuous heartbeat/stall telemetry, configurable max-silence protection (`session.max_agent_silence_minutes`), and hard runtime cap (`session.max_planner_minutes`) to avoid long stuck planner runs
|
|
47
|
+
- **Rate-limit standby** — when agents hit rate limits, Karajan parses cooldown times, waits with exponential backoff, and auto-resumes instead of failing
|
|
48
|
+
- **Preflight handshake** — `kj_preflight` requires human confirmation of agent assignments before execution, preventing AI from silently overriding your config
|
|
49
|
+
- **3-tier config** — session > project > global config layering with `kj_agents` scoping
|
|
47
50
|
- **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
|
|
48
51
|
|
|
49
52
|
> **Best with MCP** — Karajan Code is designed to be used as an MCP server inside your AI agent (Claude, Codex, etc.). The agent sends tasks to `kj_run`, gets real-time progress notifications, and receives structured results — no copy-pasting needed.
|
|
@@ -62,16 +65,16 @@ triage? ─> researcher? ─> planner? ─> coder ─> refactorer? ─> sonar?
|
|
|
62
65
|
|
|
63
66
|
| Role | Description | Default |
|
|
64
67
|
|------|-------------|---------|
|
|
65
|
-
| **triage** |
|
|
68
|
+
| **triage** | Pipeline director — analyzes task complexity and activates roles dynamically | **On** |
|
|
66
69
|
| **researcher** | Investigates codebase context before planning | Off |
|
|
67
70
|
| **planner** | Generates structured implementation plans | Off |
|
|
68
71
|
| **coder** | Writes code and tests following TDD methodology | **Always on** |
|
|
69
72
|
| **refactorer** | Improves code clarity without changing behavior | Off |
|
|
70
73
|
| **sonar** | Runs SonarQube static analysis and quality gate checks | On (if configured) |
|
|
71
74
|
| **reviewer** | Code review with configurable strictness profiles | **Always on** |
|
|
72
|
-
| **tester** | Test quality gate and coverage verification |
|
|
73
|
-
| **security** | OWASP security audit |
|
|
74
|
-
| **solomon** |
|
|
75
|
+
| **tester** | Test quality gate and coverage verification | **On** |
|
|
76
|
+
| **security** | OWASP security audit | **On** |
|
|
77
|
+
| **solomon** | Session supervisor — monitors iteration health with 4 rules, escalates on anomalies | **On** |
|
|
75
78
|
| **commiter** | Git commit, push, and PR automation after approval | Off |
|
|
76
79
|
|
|
77
80
|
Roles marked with `?` are optional and can be enabled per-run or via config.
|
|
@@ -272,6 +275,16 @@ Resume a paused session (e.g., after fail-fast).
|
|
|
272
275
|
kj resume s_2026-02-28T20-47-24-270Z --answer "yes, proceed with the fix"
|
|
273
276
|
```
|
|
274
277
|
|
|
278
|
+
### `kj agents`
|
|
279
|
+
|
|
280
|
+
List or change AI agent assignments per role.
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
kj agents # List current agents (with scope column)
|
|
284
|
+
kj agents set coder gemini # Set coder to gemini (project scope)
|
|
285
|
+
kj agents set reviewer claude --global # Set reviewer globally
|
|
286
|
+
```
|
|
287
|
+
|
|
275
288
|
### `kj roles`
|
|
276
289
|
|
|
277
290
|
Inspect pipeline roles and their template instructions.
|
|
@@ -416,9 +429,12 @@ After `npm install -g karajan-code`, the MCP server is auto-registered in Claude
|
|
|
416
429
|
| `kj_resume` | Resume a paused session |
|
|
417
430
|
| `kj_report` | Read session reports (supports `--trace`) |
|
|
418
431
|
| `kj_roles` | List roles or show role templates |
|
|
419
|
-
| `
|
|
420
|
-
| `
|
|
421
|
-
| `
|
|
432
|
+
| `kj_agents` | List or change agent assignments (session/project/global scope) |
|
|
433
|
+
| `kj_preflight` | Human confirms agent config before kj_run/kj_code executes |
|
|
434
|
+
| `kj_code` | Run coder-only mode (with progress notifications) |
|
|
435
|
+
| `kj_review` | Run reviewer-only mode (with progress notifications) |
|
|
436
|
+
| `kj_plan` | Generate implementation plan (with progress notifications) |
|
|
437
|
+
| `kj_status` | Live parsed status of current run (stage, agent, iteration, errors) |
|
|
422
438
|
|
|
423
439
|
### MCP restart after version updates
|
|
424
440
|
|
|
@@ -461,7 +477,7 @@ Use `kj roles show <role>` to inspect any template. Create a project override to
|
|
|
461
477
|
git clone https://github.com/manufosela/karajan-code.git
|
|
462
478
|
cd karajan-code
|
|
463
479
|
npm install
|
|
464
|
-
npm test # Run
|
|
480
|
+
npm test # Run 1180+ tests with Vitest
|
|
465
481
|
npm run test:watch # Watch mode
|
|
466
482
|
npm run validate # Lint + test
|
|
467
483
|
```
|
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/docs/README.es.md
CHANGED
|
@@ -30,7 +30,7 @@ En lugar de ejecutar un agente de IA y revisar manualmente su output, `kj` encad
|
|
|
30
30
|
**Caracteristicas principales:**
|
|
31
31
|
- **Pipeline multi-agente** con 11 roles configurables
|
|
32
32
|
- **4 agentes de IA soportados**: Claude, Codex, Gemini, Aider
|
|
33
|
-
- **Servidor MCP** con
|
|
33
|
+
- **Servidor MCP** con 15 herramientas — usa `kj` desde Claude, Codex o cualquier host compatible con MCP sin salir de tu agente. [Ver configuracion MCP](#servidor-mcp)
|
|
34
34
|
- **TDD obligatorio** — se exigen cambios en tests cuando se modifican ficheros fuente
|
|
35
35
|
- **Integracion con SonarQube** — analisis estatico con quality gates (requiere [Docker](#requisitos))
|
|
36
36
|
- **Perfiles de revision** — standard, strict, relaxed, paranoid
|
|
@@ -43,6 +43,9 @@ En lugar de ejecutar un agente de IA y revisar manualmente su output, `kj` encad
|
|
|
43
43
|
- **Retry con backoff** — recuperacion automatica ante errores transitorios de API (429, 5xx) con backoff exponencial y jitter
|
|
44
44
|
- **Pipeline stage tracker** — vista de progreso acumulativo durante `kj_run` mostrando que stages estan completadas, en ejecucion o pendientes — tanto en CLI como via eventos MCP para renderizado en tiempo real en el host
|
|
45
45
|
- **Guardarrailes de observabilidad del planner** — telemetria continua de heartbeat/stall, proteccion configurable por silencio maximo (`session.max_agent_silence_minutes`) y limite duro de ejecucion (`session.max_planner_minutes`) para evitar bloqueos prolongados en `kj_plan`/planner
|
|
46
|
+
- **Standby por rate-limit** — cuando un agente alcanza limites de uso, Karajan parsea el tiempo de espera, espera con backoff exponencial y reanuda automaticamente en vez de fallar
|
|
47
|
+
- **Preflight handshake** — `kj_preflight` requiere confirmacion humana de la configuracion de agentes antes de ejecutar, previniendo que la IA cambie asignaciones silenciosamente
|
|
48
|
+
- **Config de 3 niveles** — sesion > proyecto > global con scoping de `kj_agents`
|
|
46
49
|
- **Integracion con Planning Game** — combina opcionalmente con [Planning Game](https://github.com/AgenteIA-Geniova/planning-game) para gestion agil de proyectos (tareas, sprints, estimacion) — como Jira, pero open-source y nativo XP
|
|
47
50
|
|
|
48
51
|
> **Mejor con MCP** — Karajan Code esta disenado para usarse como servidor MCP dentro de tu agente de IA (Claude, Codex, etc.). El agente envia tareas a `kj_run`, recibe notificaciones de progreso en tiempo real, y obtiene resultados estructurados — sin copiar y pegar.
|
|
@@ -61,16 +64,16 @@ triage? ─> researcher? ─> planner? ─> coder ─> refactorer? ─> sonar?
|
|
|
61
64
|
|
|
62
65
|
| Rol | Descripcion | Por defecto |
|
|
63
66
|
|-----|-------------|-------------|
|
|
64
|
-
| **triage** |
|
|
67
|
+
| **triage** | Director de pipeline — analiza la complejidad y activa roles dinamicamente | **On** |
|
|
65
68
|
| **researcher** | Investiga el contexto del codebase antes de planificar | Off |
|
|
66
69
|
| **planner** | Genera planes de implementacion estructurados | Off |
|
|
67
70
|
| **coder** | Escribe codigo y tests siguiendo metodologia TDD | **Siempre activo** |
|
|
68
71
|
| **refactorer** | Mejora la claridad del codigo sin cambiar comportamiento | Off |
|
|
69
72
|
| **sonar** | Ejecuta analisis estatico SonarQube y quality gates | On (si configurado) |
|
|
70
73
|
| **reviewer** | Revision de codigo con perfiles de exigencia configurables | **Siempre activo** |
|
|
71
|
-
| **tester** | Quality gate de tests y verificacion de cobertura |
|
|
72
|
-
| **security** | Auditoria de seguridad OWASP |
|
|
73
|
-
| **solomon** |
|
|
74
|
+
| **tester** | Quality gate de tests y verificacion de cobertura | **On** |
|
|
75
|
+
| **security** | Auditoria de seguridad OWASP | **On** |
|
|
76
|
+
| **solomon** | Supervisor de sesion — monitoriza salud de iteraciones con 4 reglas, escala ante anomalias | **On** |
|
|
74
77
|
| **commiter** | Automatizacion de git commit, push y PR tras aprobacion | Off |
|
|
75
78
|
|
|
76
79
|
Los roles marcados con `?` son opcionales y se pueden activar por ejecucion o via config.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
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",
|
|
@@ -101,10 +101,20 @@ function pickOutput(res) {
|
|
|
101
101
|
return res.stdout || res.stderr || "";
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Default tools to allow for Claude subprocess.
|
|
106
|
+
* Since claude -p runs non-interactively (stdin: "ignore"), it cannot ask for
|
|
107
|
+
* permission approval. Without --allowedTools, it blocks waiting for approval
|
|
108
|
+
* that never comes.
|
|
109
|
+
*/
|
|
110
|
+
const ALLOWED_TOOLS = [
|
|
111
|
+
"Read", "Write", "Edit", "Bash", "Glob", "Grep"
|
|
112
|
+
];
|
|
113
|
+
|
|
104
114
|
export class ClaudeAgent extends BaseAgent {
|
|
105
115
|
async runTask(task) {
|
|
106
116
|
const role = task.role || "coder";
|
|
107
|
-
const args = ["-p", task.prompt];
|
|
117
|
+
const args = ["-p", task.prompt, "--allowedTools", ...ALLOWED_TOOLS];
|
|
108
118
|
const model = this.getRoleModel(role);
|
|
109
119
|
if (model) args.push("--model", model);
|
|
110
120
|
|
|
@@ -131,7 +141,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
async reviewTask(task) {
|
|
134
|
-
const args = ["-p", task.prompt, "--output-format", "stream-json"];
|
|
144
|
+
const args = ["-p", task.prompt, "--allowedTools", ...ALLOWED_TOOLS, "--output-format", "stream-json"];
|
|
135
145
|
const model = this.getRoleModel(task.role || "reviewer");
|
|
136
146
|
if (model) args.push("--model", model);
|
|
137
147
|
const res = await runCommand(resolveBin("claude"), args, cleanExecaOpts({
|
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
|
|