karajan-code 1.6.2 → 1.8.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/README.md CHANGED
@@ -42,6 +42,7 @@ Instead of running one AI agent and manually reviewing its output, `kj` chains a
42
42
  - **Interactive checkpoints** — instead of killing long-running tasks, pauses every 5 minutes with a progress report and lets you decide: continue, stop, or adjust the time
43
43
  - **Task decomposition** — triage detects when tasks should be split and recommends subtasks; with Planning Game integration, creates linked cards with sequential blocking
44
44
  - **Retry with backoff** — automatic recovery from transient API errors (429, 5xx) with exponential backoff and jitter
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
45
46
  - **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
46
47
 
47
48
  > **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.
@@ -447,7 +448,7 @@ Use `kj roles show <role>` to inspect any template. Create a project override to
447
448
  git clone https://github.com/manufosela/karajan-code.git
448
449
  cd karajan-code
449
450
  npm install
450
- npm test # Run 1025+ tests with Vitest
451
+ npm test # Run 1040+ tests with Vitest
451
452
  npm run test:watch # Watch mode
452
453
  npm run validate # Lint + test
453
454
  ```
package/docs/README.es.md CHANGED
@@ -41,6 +41,7 @@ En lugar de ejecutar un agente de IA y revisar manualmente su output, `kj` encad
41
41
  - **Checkpoints interactivos** — en lugar de matar tareas largas, pausa cada 5 minutos con un informe de progreso y te deja decidir: continuar, parar o ajustar el tiempo
42
42
  - **Descomposicion de tareas** — triage detecta cuando una tarea debe dividirse y recomienda subtareas; con integracion Planning Game, crea cards vinculadas con bloqueo secuencial
43
43
  - **Retry con backoff** — recuperacion automatica ante errores transitorios de API (429, 5xx) con backoff exponencial y jitter
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
44
45
  - **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
45
46
 
46
47
  > **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.
@@ -231,7 +232,7 @@ Usa `kj roles show <rol>` para inspeccionar cualquier template. Crea un override
231
232
  git clone https://github.com/manufosela/karajan-code.git
232
233
  cd karajan-code
233
234
  npm install
234
- npm test # Ejecutar 899+ tests con Vitest
235
+ npm test # Ejecutar 1040+ tests con Vitest
235
236
  npm run test:watch # Modo watch
236
237
  npm run validate # Lint + test
237
238
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.6.2",
3
+ "version": "1.8.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",
package/src/cli.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
2
5
  import { Command } from "commander";
3
6
  import { applyRunOverrides, loadConfig, validateConfig } from "./config.js";
4
7
  import { createLogger } from "./utils/logger.js";
@@ -15,6 +18,9 @@ import { resumeCommand } from "./commands/resume.js";
15
18
  import { sonarCommand, sonarOpenCommand } from "./commands/sonar.js";
16
19
  import { rolesCommand } from "./commands/roles.js";
17
20
 
21
+ const PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
22
+ const PKG_VERSION = JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
23
+
18
24
  async function withConfig(commandName, flags, fn) {
19
25
  const { config } = await loadConfig();
20
26
  const merged = applyRunOverrides(config, flags || {});
@@ -24,7 +30,7 @@ async function withConfig(commandName, flags, fn) {
24
30
  }
25
31
 
26
32
  const program = new Command();
27
- program.name("kj").description("Karajan Code CLI").version("1.6.2");
33
+ program.name("kj").description("Karajan Code CLI").version(PKG_VERSION);
28
34
 
29
35
  program
30
36
  .command("init")
@@ -1,3 +1,6 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { watch } from "node:fs";
3
+
1
4
  const DEFAULT_INTERVAL_MS = 5000;
2
5
 
3
6
  export function setupOrphanGuard({ intervalMs = DEFAULT_INTERVAL_MS, exitFn = () => process.exit(0) } = {}) {
@@ -19,3 +22,27 @@ export function setupOrphanGuard({ intervalMs = DEFAULT_INTERVAL_MS, exitFn = ()
19
22
 
20
23
  return { timer, parentPid };
21
24
  }
25
+
26
+ export function setupVersionWatcher({ pkgPath, currentVersion, exitFn = () => process.exit(0) } = {}) {
27
+ if (!pkgPath) return null;
28
+
29
+ function checkVersion() {
30
+ try {
31
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
32
+ if (pkg.version !== currentVersion) {
33
+ exitFn();
34
+ return true;
35
+ }
36
+ } catch { /* ignore read errors */ }
37
+ return false;
38
+ }
39
+
40
+ let watcher = null;
41
+ try {
42
+ watcher = watch(pkgPath, { persistent: false }, () => {
43
+ checkVersion();
44
+ });
45
+ } catch { /* ignore watch errors */ }
46
+
47
+ return { watcher, checkVersion };
48
+ }
@@ -21,9 +21,74 @@ export const PROGRESS_STAGES = [
21
21
  "solomon:escalate",
22
22
  "question",
23
23
  "session:end",
24
- "dry-run:summary"
24
+ "dry-run:summary",
25
+ "pipeline:tracker"
25
26
  ];
26
27
 
28
+ const PIPELINE_ORDER = [
29
+ "triage", "researcher", "planner", "coder", "refactorer", "sonar", "reviewer", "tester", "security", "commiter"
30
+ ];
31
+
32
+ export function buildPipelineTracker(config, emitter) {
33
+ const pipeline = config.pipeline || {};
34
+
35
+ const stages = PIPELINE_ORDER
36
+ .filter(name => {
37
+ if (name === "coder") return true;
38
+ if (name === "reviewer") return pipeline.reviewer?.enabled !== false;
39
+ if (name === "sonar") return pipeline.sonar?.enabled || config.sonarqube?.enabled;
40
+ return pipeline[name]?.enabled;
41
+ })
42
+ .map(name => ({ name, status: "pending", summary: undefined }));
43
+
44
+ const findStage = (name) => stages.find(s => s.name === name);
45
+
46
+ const emitTracker = () => {
47
+ emitter.emit("progress", {
48
+ type: "pipeline:tracker",
49
+ detail: { stages: stages.map(s => ({ ...s })) }
50
+ });
51
+ };
52
+
53
+ emitter.on("progress", (event) => {
54
+ const match = event.type?.match(/^(\w+):(start|end)$/);
55
+ if (!match) return;
56
+
57
+ const [, name, phase] = match;
58
+ const stage = findStage(name);
59
+ if (!stage) return;
60
+
61
+ if (phase === "start") {
62
+ stage.status = "running";
63
+ stage.summary = event.detail?.[name] || stage.summary;
64
+ } else {
65
+ stage.status = event.status === "fail" ? "failed" : "done";
66
+ stage.summary = event.detail?.summary || event.detail?.gateStatus || stage.summary;
67
+ }
68
+
69
+ emitTracker();
70
+ });
71
+
72
+ return { stages };
73
+ }
74
+
75
+ export function sendTrackerLog(server, stageName, status, summary) {
76
+ try {
77
+ server.sendLoggingMessage({
78
+ level: "info",
79
+ logger: "karajan",
80
+ data: {
81
+ type: "pipeline:tracker",
82
+ detail: {
83
+ stages: [{ name: stageName, status, summary: summary || undefined }]
84
+ }
85
+ }
86
+ });
87
+ } catch {
88
+ // best-effort
89
+ }
90
+ }
91
+
27
92
  export function buildProgressHandler(server) {
28
93
  return (event) => {
29
94
  try {
@@ -4,13 +4,21 @@
4
4
  */
5
5
 
6
6
  import { EventEmitter } from "node:events";
7
+ import fs from "node:fs/promises";
7
8
  import { runKjCommand } from "./run-kj.js";
8
9
  import { normalizePlanArgs } from "./tool-arg-normalizers.js";
9
- import { buildProgressHandler, buildProgressNotifier } from "./progress.js";
10
+ import { buildProgressHandler, buildProgressNotifier, buildPipelineTracker, sendTrackerLog } from "./progress.js";
10
11
  import { runFlow, resumeFlow } from "../orchestrator.js";
11
12
  import { loadConfig, applyRunOverrides, validateConfig, resolveRole } from "../config.js";
12
13
  import { createLogger } from "../utils/logger.js";
13
14
  import { assertAgentsAvailable } from "../agents/availability.js";
15
+ import { createAgent } from "../agents/index.js";
16
+ import { buildPlannerPrompt } from "../prompts/planner.js";
17
+ import { buildCoderPrompt } from "../prompts/coder.js";
18
+ import { buildReviewerPrompt } from "../prompts/reviewer.js";
19
+ import { parseMaybeJsonString } from "../review/parser.js";
20
+ import { computeBaseRef, generateDiff } from "../review/diff-generator.js";
21
+ import { resolveReviewProfile } from "../review/profiles.js";
14
22
 
15
23
  export function asObject(value) {
16
24
  if (value && typeof value === "object") return value;
@@ -93,10 +101,10 @@ export function enrichedFailPayload(error, toolName) {
93
101
  return payload;
94
102
  }
95
103
 
96
- export async function buildConfig(options) {
104
+ export async function buildConfig(options, commandName = "run") {
97
105
  const { config } = await loadConfig();
98
106
  const merged = applyRunOverrides(config, options || {});
99
- validateConfig(merged, "run");
107
+ validateConfig(merged, commandName);
100
108
  return merged;
101
109
  }
102
110
 
@@ -143,6 +151,7 @@ export async function handleRunDirect(a, server, extra) {
143
151
  emitter.on("progress", buildProgressHandler(server));
144
152
  const progressNotifier = buildProgressNotifier(extra);
145
153
  if (progressNotifier) emitter.on("progress", progressNotifier);
154
+ buildPipelineTracker(config, emitter);
146
155
 
147
156
  const askQuestion = buildAskQuestion(server);
148
157
  const pgTaskId = a.pgTask || null;
@@ -173,6 +182,82 @@ export async function handleResumeDirect(a, server, extra) {
173
182
  return { ok: true, ...result };
174
183
  }
175
184
 
185
+ export async function handlePlanDirect(a, server, extra) {
186
+ const options = normalizePlanArgs(a);
187
+ const config = await buildConfig(options, "plan");
188
+ const logger = createLogger(config.output.log_level, "mcp");
189
+
190
+ const plannerRole = resolveRole(config, "planner");
191
+ await assertAgentsAvailable([plannerRole.provider]);
192
+
193
+ const planner = createAgent(plannerRole.provider, config, logger);
194
+ const prompt = buildPlannerPrompt({ task: a.task, context: a.context });
195
+ sendTrackerLog(server, "planner", "running", plannerRole.provider);
196
+ const result = await planner.runTask({ prompt, role: "planner" });
197
+
198
+ if (!result.ok) {
199
+ sendTrackerLog(server, "planner", "failed");
200
+ throw new Error(result.error || result.output || "Planner failed");
201
+ }
202
+
203
+ sendTrackerLog(server, "planner", "done");
204
+ const parsed = parseMaybeJsonString(result.output);
205
+ return { ok: true, plan: parsed || result.output, raw: result.output };
206
+ }
207
+
208
+ export async function handleCodeDirect(a, server, extra) {
209
+ const config = await buildConfig(a, "code");
210
+ const logger = createLogger(config.output.log_level, "mcp");
211
+
212
+ const coderRole = resolveRole(config, "coder");
213
+ await assertAgentsAvailable([coderRole.provider]);
214
+
215
+ const coder = createAgent(coderRole.provider, config, logger);
216
+ let coderRules = null;
217
+ if (config.coder_rules) {
218
+ try {
219
+ coderRules = await fs.readFile(config.coder_rules, "utf8");
220
+ } catch { /* no coder rules file */ }
221
+ }
222
+ const prompt = buildCoderPrompt({ task: a.task, coderRules, methodology: config.development?.methodology || "tdd" });
223
+ sendTrackerLog(server, "coder", "running", coderRole.provider);
224
+ const result = await coder.runTask({ prompt, role: "coder" });
225
+
226
+ if (!result.ok) {
227
+ sendTrackerLog(server, "coder", "failed");
228
+ throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
229
+ }
230
+
231
+ sendTrackerLog(server, "coder", "done");
232
+ return { ok: true, output: result.output, exitCode: result.exitCode };
233
+ }
234
+
235
+ export async function handleReviewDirect(a, server, extra) {
236
+ const config = await buildConfig(a, "review");
237
+ const logger = createLogger(config.output.log_level, "mcp");
238
+
239
+ const reviewerRole = resolveRole(config, "reviewer");
240
+ await assertAgentsAvailable([reviewerRole.provider, config.reviewer_options?.fallback_reviewer]);
241
+
242
+ const reviewer = createAgent(reviewerRole.provider, config, logger);
243
+ const resolvedBase = await computeBaseRef({ baseBranch: config.base_branch, baseRef: a.baseRef });
244
+ const diff = await generateDiff({ baseRef: resolvedBase });
245
+ const { rules } = await resolveReviewProfile({ mode: config.review_mode, projectDir: process.cwd() });
246
+
247
+ const prompt = buildReviewerPrompt({ task: a.task, diff, reviewRules: rules, mode: config.review_mode });
248
+ sendTrackerLog(server, "reviewer", "running", reviewerRole.provider);
249
+ const result = await reviewer.reviewTask({ prompt, role: "reviewer" });
250
+
251
+ if (!result.ok) {
252
+ sendTrackerLog(server, "reviewer", "failed");
253
+ throw new Error(result.error || result.output || `Reviewer failed (exit ${result.exitCode})`);
254
+ }
255
+
256
+ sendTrackerLog(server, "reviewer", "done");
257
+ const parsed = parseMaybeJsonString(result.output);
258
+ return { ok: true, review: parsed || result.output, raw: result.output };
259
+ }
260
+
176
261
  export async function handleToolCall(name, args, server, extra) {
177
262
  const a = asObject(args);
178
263
 
@@ -240,22 +325,21 @@ export async function handleToolCall(name, args, server, extra) {
240
325
  if (!a.task) {
241
326
  return failPayload("Missing required field: task");
242
327
  }
243
- return runKjCommand({ command: "code", commandArgs: [a.task], options: a });
328
+ return handleCodeDirect(a, server, extra);
244
329
  }
245
330
 
246
331
  if (name === "kj_review") {
247
332
  if (!a.task) {
248
333
  return failPayload("Missing required field: task");
249
334
  }
250
- return runKjCommand({ command: "review", commandArgs: [a.task], options: a });
335
+ return handleReviewDirect(a, server, extra);
251
336
  }
252
337
 
253
338
  if (name === "kj_plan") {
254
339
  if (!a.task) {
255
340
  return failPayload("Missing required field: task");
256
341
  }
257
- const options = normalizePlanArgs(a);
258
- return runKjCommand({ command: "plan", commandArgs: [a.task], options });
342
+ return handlePlanDirect(a, server, extra);
259
343
  }
260
344
 
261
345
  return failPayload(`Unknown tool: ${name}`);
package/src/mcp/server.js CHANGED
@@ -1,14 +1,26 @@
1
1
  #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
2
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
7
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
8
  import { tools } from "./tools.js";
6
9
  import { handleToolCall, responseText, enrichedFailPayload } from "./server-handlers.js";
7
10
 
11
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
12
+ const PKG_PATH = path.resolve(MODULE_DIR, "../../package.json");
13
+
14
+ function readVersion() {
15
+ return JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
16
+ }
17
+
18
+ const LOADED_VERSION = readVersion();
19
+
8
20
  const server = new Server(
9
21
  {
10
22
  name: "karajan-mcp",
11
- version: "1.0.0"
23
+ version: LOADED_VERSION
12
24
  },
13
25
  {
14
26
  capabilities: {
@@ -22,6 +34,9 @@ const server = new Server(
22
34
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
23
35
 
24
36
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
37
+ // Auto-exit if package version changed (stale process)
38
+ if (readVersion() !== LOADED_VERSION) process.exit(0);
39
+
25
40
  const name = request.params?.name;
26
41
  const args = request.params?.arguments || {};
27
42
 
@@ -33,9 +48,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
33
48
  }
34
49
  });
35
50
 
36
- // --- Orphan process protection ---
37
- import { setupOrphanGuard } from "./orphan-guard.js";
51
+ // --- Orphan process protection + version watcher ---
52
+ import { setupOrphanGuard, setupVersionWatcher } from "./orphan-guard.js";
38
53
  setupOrphanGuard();
54
+ setupVersionWatcher({ pkgPath: PKG_PATH, currentVersion: LOADED_VERSION });
39
55
 
40
56
  const transport = new StdioServerTransport();
41
57
  await server.connect(transport);
package/src/mcp/tools.js CHANGED
@@ -146,8 +146,7 @@ export const tools = [
146
146
  task: { type: "string" },
147
147
  coder: { type: "string" },
148
148
  coderModel: { type: "string" },
149
- kjHome: { type: "string" },
150
- timeoutMs: { type: "number" }
149
+ kjHome: { type: "string" }
151
150
  }
152
151
  }
153
152
  },
@@ -162,8 +161,7 @@ export const tools = [
162
161
  reviewer: { type: "string" },
163
162
  reviewerModel: { type: "string" },
164
163
  baseRef: { type: "string" },
165
- kjHome: { type: "string" },
166
- timeoutMs: { type: "number" }
164
+ kjHome: { type: "string" }
167
165
  }
168
166
  }
169
167
  },
@@ -179,8 +177,7 @@ export const tools = [
179
177
  plannerModel: { type: "string" },
180
178
  coder: { type: "string", description: "Legacy alias for planner" },
181
179
  coderModel: { type: "string", description: "Legacy alias for plannerModel" },
182
- kjHome: { type: "string" },
183
- timeoutMs: { type: "number" }
180
+ kjHome: { type: "string" }
184
181
  }
185
182
  }
186
183
  }
@@ -1,3 +1,10 @@
1
+ import path from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const DISPLAY_PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
6
+ const DISPLAY_VERSION = JSON.parse(readFileSync(DISPLAY_PKG_PATH, "utf8")).version;
7
+
1
8
  const ANSI = {
2
9
  reset: "\x1b[0m",
3
10
  bold: "\x1b[1m",
@@ -57,7 +64,7 @@ export function formatElapsed(ms) {
57
64
  const BAR = `${ANSI.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${ANSI.reset}`;
58
65
 
59
66
  export function printHeader({ task, config }) {
60
- const version = "0.1.0";
67
+ const version = DISPLAY_VERSION;
61
68
  console.log(BAR);
62
69
  console.log(`${ANSI.bold}${ANSI.cyan}\u25b6 Karajan Code v${version}${ANSI.reset}`);
63
70
  console.log(BAR);
@@ -336,6 +343,26 @@ export function printEvent(event) {
336
343
  console.log(`${ANSI.dim}Resume with: kj resume ${event.sessionId} --answer "<response>"${ANSI.reset}`);
337
344
  break;
338
345
 
346
+ case "pipeline:tracker": {
347
+ const trackerStages = event.detail?.stages || [];
348
+ console.log(` ${ANSI.dim}\u250c Pipeline${ANSI.reset}`);
349
+ for (const stage of trackerStages) {
350
+ let stIcon, stColor;
351
+ switch (stage.status) {
352
+ case "done": stIcon = "\u2713"; stColor = ANSI.green; break;
353
+ case "running": stIcon = "\u25b6"; stColor = ANSI.cyan; break;
354
+ case "failed": stIcon = "\u2717"; stColor = ANSI.red; break;
355
+ default: stIcon = "\u00b7"; stColor = ANSI.dim; break;
356
+ }
357
+ const suffix = stage.summary
358
+ ? stage.status === "running" ? ` (${stage.summary})` : ` \u2192 ${stage.summary}`
359
+ : "";
360
+ console.log(` ${ANSI.dim}\u2502${ANSI.reset} ${stColor}${stIcon} ${stage.name}${suffix}${ANSI.reset}`);
361
+ }
362
+ console.log(` ${ANSI.dim}\u2514${ANSI.reset}`);
363
+ break;
364
+ }
365
+
339
366
  case "agent:output":
340
367
  console.log(` \u2502 ${ANSI.dim}${event.message}${ANSI.reset}`);
341
368
  break;