karajan-code 1.55.0 → 1.56.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.55.0",
3
+ "version": "1.56.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
@@ -25,6 +25,7 @@ import { architectCommand } from "./commands/architect.js";
25
25
  import { auditCommand } from "./commands/audit.js";
26
26
  import { boardCommand } from "./commands/board.js";
27
27
  import { undoCommand } from "./commands/undo.js";
28
+ import { statusCommand } from "./commands/status.js";
28
29
 
29
30
  import { printUpdateNotice } from "./utils/update-check.js";
30
31
 
@@ -168,6 +169,14 @@ program
168
169
  await withConfig("doctor", {}, doctorCommand);
169
170
  });
170
171
 
172
+ program
173
+ .command("status")
174
+ .description("Show pipeline dashboard with HU progress")
175
+ .option("-n, --lines <n>", "Max log lines to read", "50")
176
+ .action(async (flags) => {
177
+ await statusCommand({ lines: Number(flags.lines) });
178
+ });
179
+
171
180
  program
172
181
  .command("report")
173
182
  .description("Show latest session report")
@@ -12,6 +12,7 @@ import { getInstallCommand } from "../utils/os-detect.js";
12
12
  import { detectOsLocale, SUPPORTED_LANGUAGES } from "../utils/locale.js";
13
13
  import { detectRtk } from "../utils/rtk-detect.js";
14
14
  import { installRtk } from "../utils/rtk-install.js";
15
+ import { detectProjectStack } from "../utils/stack-detect.js";
15
16
 
16
17
  async function runWizard(config, logger) {
17
18
  const agents = await detectAvailableAgents();
@@ -330,6 +331,35 @@ export async function initCommand({ logger, flags = {} }) {
330
331
  await ensureCoderRules(coderRulesPath, logger);
331
332
  await installSkills(logger, interactive);
332
333
 
334
+ // Auto-detect project stack and preconfigure
335
+ const stack = await detectProjectStack(process.cwd());
336
+ if (stack.frameworks.length > 0 || stack.language) {
337
+ const parts = [];
338
+ if (stack.frameworks.length > 0) parts.push(stack.frameworks.join(" + "));
339
+ if (stack.language) parts.push(stack.language);
340
+ const stackType = stack.isFullstack ? "fullstack" : stack.isFrontend ? "frontend" : stack.isBackend ? "backend" : "unknown";
341
+ logger.info(`Detected stack: ${parts.join(" / ")} (${stackType})`);
342
+
343
+ if (stack.suggestions.impeccable) {
344
+ config.impeccable = config.impeccable || {};
345
+ config.impeccable.enabled = true;
346
+ logger.info(" -> Impeccable design audit enabled (frontend detected)");
347
+ }
348
+
349
+ if (stack.suggestions.skills.length > 0) {
350
+ logger.info(` -> Suggested skills: ${stack.suggestions.skills.join(", ")}`);
351
+ }
352
+
353
+ if (stack.language && !configExists) {
354
+ config.development = config.development || {};
355
+ config.development.methodology = config.development.methodology || "tdd";
356
+ }
357
+
358
+ await writeConfig(configPath, config);
359
+ } else {
360
+ logger.info("No project stack detected — using default configuration.");
361
+ }
362
+
333
363
  // Check RTK availability — auto-install if missing
334
364
  const rtk = await detectRtk();
335
365
  if (rtk.available) {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * CLI status command — shows a terminal dashboard of the current pipeline state.
3
+ */
4
+
5
+ import { readRunLog } from "../utils/run-log.js";
6
+ import { loadMostRecentSession } from "../session-store.js";
7
+ import { buildDashboard } from "../utils/status-dashboard.js";
8
+
9
+ /**
10
+ * Run the `kj status` command: print a human-readable dashboard to stdout.
11
+ * @param {object} options
12
+ * @param {number} [options.lines=50] - Max log lines to read.
13
+ * @param {string} [options.projectDir] - Project directory override.
14
+ */
15
+ export async function statusCommand({ lines = 50, projectDir } = {}) {
16
+ const dir = projectDir || process.cwd();
17
+ const logResult = readRunLog(dir, lines);
18
+
19
+ let session = null;
20
+ try {
21
+ session = await loadMostRecentSession();
22
+ } catch { /* no session available */ }
23
+
24
+ let stories = [];
25
+ if (session) {
26
+ try {
27
+ const { loadHuBatch } = await import("../hu/store.js");
28
+ const batch = await loadHuBatch(session.id);
29
+ if (batch?.stories) stories = batch.stories;
30
+ } catch { /* no HU batch */ }
31
+ }
32
+
33
+ const dashboard = buildDashboard(session, logResult.lines || [], { stories });
34
+ console.log(dashboard);
35
+ }
@@ -14,6 +14,8 @@ import {
14
14
  responseText,
15
15
  buildConfig,
16
16
  } from "../server-handlers.js";
17
+ import { buildDashboardJson } from "../../utils/status-dashboard.js";
18
+ import { loadMostRecentSession } from "../../session-store.js";
17
19
 
18
20
  const AGENT_ROLES = new Set(["coder", "reviewer", "tester", "security", "solomon"]);
19
21
 
@@ -29,7 +31,26 @@ async function runBootstrapGate(server, a) {
29
31
  export async function handleStatus(a, server) {
30
32
  const maxLines = a.lines || 50;
31
33
  const projectDir = await resolveProjectDir(server, a.projectDir);
32
- return readRunLog(projectDir, maxLines);
34
+ const logResult = readRunLog(projectDir, maxLines);
35
+
36
+ // Enrich with dashboard data when possible
37
+ try {
38
+ const session = await loadMostRecentSession();
39
+ const stories = [];
40
+ if (session) {
41
+ try {
42
+ const { loadHuBatch } = await import("../../hu/store.js");
43
+ const batch = await loadHuBatch(session.id);
44
+ if (batch?.stories) stories.push(...batch.stories);
45
+ } catch { /* no HU batch for this session */ }
46
+ }
47
+
48
+ const dashboard = buildDashboardJson(session, logResult.lines || [], { stories });
49
+ return { ...logResult, dashboard };
50
+ } catch {
51
+ // Fallback: return raw log without dashboard
52
+ return logResult;
53
+ }
33
54
  }
34
55
 
35
56
  export async function handleAgents(a) {
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Detects the project technology stack by scanning project files.
3
+ * Used by `kj init` to auto-configure roles, skills, and settings.
4
+ */
5
+
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+
9
+ /**
10
+ * Frameworks detectable from package.json dependencies.
11
+ * Maps dependency name -> { name, type } where type is "frontend" | "backend" | "fullstack".
12
+ */
13
+ const PKG_FRAMEWORKS = {
14
+ react: { name: "react", type: "frontend" },
15
+ "react-dom": { name: "react", type: "frontend" },
16
+ vue: { name: "vue", type: "frontend" },
17
+ svelte: { name: "svelte", type: "frontend" },
18
+ "@angular/core": { name: "angular", type: "frontend" },
19
+ astro: { name: "astro", type: "frontend" },
20
+ next: { name: "next", type: "fullstack" },
21
+ nuxt: { name: "nuxt", type: "fullstack" },
22
+ "solid-js": { name: "solid", type: "frontend" },
23
+ lit: { name: "lit", type: "frontend" },
24
+ express: { name: "express", type: "backend" },
25
+ fastify: { name: "fastify", type: "backend" },
26
+ "@nestjs/core": { name: "nestjs", type: "backend" },
27
+ hono: { name: "hono", type: "backend" },
28
+ koa: { name: "koa", type: "backend" },
29
+ };
30
+
31
+ /**
32
+ * Language markers: file presence -> { language, type }.
33
+ */
34
+ const LANGUAGE_FILE_MARKERS = [
35
+ { file: "go.mod", language: "go", type: "backend" },
36
+ { file: "Cargo.toml", language: "rust", type: "backend" },
37
+ { file: "pom.xml", language: "java", type: "backend" },
38
+ { file: "build.gradle", language: "java", type: "backend" },
39
+ { file: "build.gradle.kts", language: "kotlin", type: "backend" },
40
+ { file: "pyproject.toml", language: "python", type: "backend" },
41
+ { file: "setup.py", language: "python", type: "backend" },
42
+ { file: "requirements.txt", language: "python", type: "backend" },
43
+ { file: "Gemfile", language: "ruby", type: "backend" },
44
+ { file: "composer.json", language: "php", type: "backend" },
45
+ { file: "pubspec.yaml", language: "dart", type: "frontend" },
46
+ { file: "Package.swift", language: "swift", type: "backend" },
47
+ ];
48
+
49
+ /**
50
+ * Maps framework names to skill names for auto-install suggestions.
51
+ */
52
+ const FRAMEWORK_TO_SKILL = {
53
+ react: "react",
54
+ vue: "vue",
55
+ svelte: "svelte",
56
+ angular: "angular",
57
+ astro: "astro",
58
+ next: "nextjs",
59
+ nuxt: "vue",
60
+ solid: "solid",
61
+ lit: "lit",
62
+ express: "express",
63
+ fastify: "fastify",
64
+ nestjs: "nestjs",
65
+ };
66
+
67
+ /**
68
+ * Detect the project technology stack.
69
+ * @param {string} projectDir - Absolute path to project root.
70
+ * @returns {Promise<{frameworks: string[], language: string|null, isFrontend: boolean, isBackend: boolean, isFullstack: boolean, suggestions: {impeccable: boolean, skills: string[]}}>}
71
+ */
72
+ export async function detectProjectStack(projectDir) {
73
+ const frameworks = new Set();
74
+ let language = null;
75
+ let hasFrontend = false;
76
+ let hasBackend = false;
77
+
78
+ // 1. Scan package.json for known frameworks
79
+ try {
80
+ const pkgRaw = await fs.readFile(path.join(projectDir, "package.json"), "utf8");
81
+ const pkg = JSON.parse(pkgRaw);
82
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
83
+
84
+ if (!language) language = "javascript";
85
+
86
+ // Check for TypeScript
87
+ if (allDeps.typescript || allDeps["ts-node"]) {
88
+ language = "typescript";
89
+ }
90
+
91
+ for (const [dep, info] of Object.entries(PKG_FRAMEWORKS)) {
92
+ if (allDeps[dep]) {
93
+ frameworks.add(info.name);
94
+ if (info.type === "frontend" || info.type === "fullstack") hasFrontend = true;
95
+ if (info.type === "backend" || info.type === "fullstack") hasBackend = true;
96
+ }
97
+ }
98
+ } catch { /* no package.json or parse error */ }
99
+
100
+ // 2. Check language file markers
101
+ for (const marker of LANGUAGE_FILE_MARKERS) {
102
+ try {
103
+ await fs.access(path.join(projectDir, marker.file));
104
+ if (!language || language === "javascript") {
105
+ language = marker.language;
106
+ }
107
+ if (marker.type === "frontend") hasFrontend = true;
108
+ if (marker.type === "backend") hasBackend = true;
109
+ } catch { /* file not found */ }
110
+ }
111
+
112
+ const frameworkList = Array.from(frameworks);
113
+ const isFullstack = hasFrontend && hasBackend;
114
+
115
+ // Build suggestions
116
+ const skills = [];
117
+ for (const fw of frameworkList) {
118
+ const skill = FRAMEWORK_TO_SKILL[fw];
119
+ if (skill && !skills.includes(skill)) {
120
+ skills.push(skill);
121
+ }
122
+ }
123
+
124
+ return {
125
+ frameworks: frameworkList,
126
+ language,
127
+ isFrontend: hasFrontend,
128
+ isBackend: hasBackend,
129
+ isFullstack,
130
+ suggestions: {
131
+ impeccable: hasFrontend,
132
+ skills,
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Status dashboard formatter.
3
+ * Builds a structured terminal dashboard from session data and run-log lines.
4
+ */
5
+
6
+ import { HU_STATUS } from "../hu/store.js";
7
+
8
+ /**
9
+ * Status label mapping with visual indicators for terminal output.
10
+ * @type {Record<string, string>}
11
+ */
12
+ const STATUS_LABELS = {
13
+ [HU_STATUS.PENDING]: "pending",
14
+ [HU_STATUS.CODING]: "coding",
15
+ [HU_STATUS.REVIEWING]: "reviewing",
16
+ [HU_STATUS.DONE]: "done",
17
+ [HU_STATUS.FAILED]: "failed",
18
+ [HU_STATUS.BLOCKED]: "blocked",
19
+ [HU_STATUS.CERTIFIED]: "certified",
20
+ [HU_STATUS.NEEDS_CONTEXT]: "needs_ctx",
21
+ };
22
+
23
+ /**
24
+ * Format a millisecond duration into a human-readable string (e.g. "3m 42s").
25
+ * @param {number} ms
26
+ * @returns {string}
27
+ */
28
+ export function formatDuration(ms) {
29
+ if (!ms || ms < 0) return "0s";
30
+ const totalSec = Math.floor(ms / 1000);
31
+ const hours = Math.floor(totalSec / 3600);
32
+ const minutes = Math.floor((totalSec % 3600) / 60);
33
+ const seconds = totalSec % 60;
34
+
35
+ const parts = [];
36
+ if (hours > 0) parts.push(`${hours}h`);
37
+ if (minutes > 0) parts.push(`${minutes}m`);
38
+ if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
39
+ return parts.join(" ");
40
+ }
41
+
42
+ /**
43
+ * Parse the current pipeline stage from log lines.
44
+ * @param {string[]} lines - Raw log lines.
45
+ * @returns {{ currentStage: string|null, lastEvent: string|null }}
46
+ */
47
+ export function parseStageFromLog(lines) {
48
+ let currentStage = null;
49
+ let lastEvent = null;
50
+
51
+ for (const line of lines) {
52
+ const stageStart = /\[(\w+):start\]/.exec(line);
53
+ if (stageStart) currentStage = stageStart[1];
54
+
55
+ const stageDone = /\[(\w+):done\]|\[(\w+)\] finished/.exec(line);
56
+ if (stageDone) {
57
+ const doneName = stageDone[1] || stageDone[2];
58
+ if (doneName === currentStage) currentStage = "idle";
59
+ }
60
+
61
+ if (line.includes("[standby]") || line.includes("standby")) {
62
+ currentStage = "standby";
63
+ }
64
+
65
+ if (line.trim()) lastEvent = line.trim();
66
+ }
67
+
68
+ return { currentStage, lastEvent };
69
+ }
70
+
71
+ /**
72
+ * Format a single HU line for the dashboard.
73
+ * @param {object} story - HU story object.
74
+ * @param {boolean} isCurrent - Whether this is the currently active HU.
75
+ * @returns {string}
76
+ */
77
+ function formatHuLine(story, isCurrent) {
78
+ const id = (story.id || "???").padEnd(12);
79
+ const rawStatus = story.status || "pending";
80
+ const label = STATUS_LABELS[rawStatus] || rawStatus;
81
+ const statusStr = `[${label}]`.padEnd(13);
82
+ const title = story.title || story.original?.text || "(untitled)";
83
+ const truncTitle = title.length > 40 ? title.slice(0, 37) + "..." : title;
84
+
85
+ let timing = "";
86
+ if (story.duration_ms) {
87
+ timing = formatDuration(story.duration_ms);
88
+ }
89
+
90
+ const blockedInfo = story.blocked_by?.length
91
+ ? ` (needs ${story.blocked_by.join(", ")})`
92
+ : "";
93
+
94
+ const marker = isCurrent ? " <-- current" : "";
95
+ const timingStr = timing ? ` ${timing}` : "";
96
+
97
+ return ` ${id} ${statusStr} ${truncTitle}${blockedInfo}${timingStr}${marker}`;
98
+ }
99
+
100
+ /**
101
+ * Build a terminal dashboard string from session data and log lines.
102
+ *
103
+ * @param {object|null} sessionData - Session JSON (from session-store) or null.
104
+ * @param {string[]} logLines - Raw run-log lines.
105
+ * @param {object} [options] - Optional overrides.
106
+ * @param {object[]|null} [options.stories] - HU stories array (from hu batch).
107
+ * @returns {string}
108
+ */
109
+ export function buildDashboard(sessionData, logLines, options = {}) {
110
+ if (!sessionData) {
111
+ return "No active pipeline";
112
+ }
113
+
114
+ const lines = [];
115
+ const config = sessionData.config_snapshot || {};
116
+ const maxIter = config.max_iterations ?? "?";
117
+ const status = sessionData.status || "unknown";
118
+
119
+ // Parse log for current stage info
120
+ const logInfo = parseStageFromLog(logLines || []);
121
+
122
+ // Determine iteration from session or log
123
+ const iteration = sessionData.reviewer_retry_count != null
124
+ ? sessionData.reviewer_retry_count + 1
125
+ : null;
126
+ const iterFromLog = parseIterationFromLog(logLines || []);
127
+ const displayIter = iterFromLog || iteration || "?";
128
+
129
+ // Pipeline header
130
+ const statusUpper = status.toUpperCase();
131
+ lines.push(`Pipeline: ${statusUpper} (iteration ${displayIter}/${maxIter})`);
132
+
133
+ // Duration
134
+ const duration = computeDuration(sessionData);
135
+ lines.push(`Duration: ${formatDuration(duration)}`);
136
+
137
+ // Agent info
138
+ const coderProvider = config.coder?.provider || config.roles?.coder?.provider || "?";
139
+ const reviewerProvider = config.reviewer?.provider || config.roles?.reviewer?.provider || "?";
140
+ lines.push(`Agent: ${coderProvider} (coder) / ${reviewerProvider} (reviewer)`);
141
+
142
+ // HU stories section
143
+ const stories = options.stories || [];
144
+ if (stories.length > 0) {
145
+ lines.push("");
146
+ lines.push("HUs:");
147
+
148
+ // Determine which story is currently being worked on
149
+ const currentStory = stories.find(s =>
150
+ s.status === HU_STATUS.CODING || s.status === HU_STATUS.REVIEWING
151
+ );
152
+
153
+ for (const story of stories) {
154
+ const isCurrent = currentStory && story.id === currentStory.id;
155
+ lines.push(formatHuLine(story, isCurrent));
156
+ }
157
+ }
158
+
159
+ // Current stage and last event
160
+ lines.push("");
161
+ const stage = logInfo.currentStage || "idle";
162
+ const iterSuffix = displayIter !== "?" ? ` (iteration ${displayIter})` : "";
163
+ lines.push(`Current stage: ${stage}${iterSuffix}`);
164
+
165
+ if (logInfo.lastEvent) {
166
+ // Truncate last event for display
167
+ const maxLen = 120;
168
+ const event = logInfo.lastEvent.length > maxLen
169
+ ? logInfo.lastEvent.slice(0, maxLen - 3) + "..."
170
+ : logInfo.lastEvent;
171
+ lines.push(`Last event: ${event}`);
172
+ }
173
+
174
+ // Errors
175
+ if (sessionData.deferred_issues?.length) {
176
+ lines.push("");
177
+ lines.push(`Deferred issues: ${sessionData.deferred_issues.length}`);
178
+ }
179
+
180
+ return lines.join("\n");
181
+ }
182
+
183
+ /**
184
+ * Build a structured JSON response for MCP consumers.
185
+ *
186
+ * @param {object|null} sessionData - Session JSON or null.
187
+ * @param {string[]} logLines - Raw run-log lines.
188
+ * @param {object} [options] - Optional overrides.
189
+ * @param {object[]|null} [options.stories] - HU stories array.
190
+ * @returns {object}
191
+ */
192
+ export function buildDashboardJson(sessionData, logLines, options = {}) {
193
+ if (!sessionData) {
194
+ return { ok: false, message: "No active pipeline" };
195
+ }
196
+
197
+ const config = sessionData.config_snapshot || {};
198
+ const logInfo = parseStageFromLog(logLines || []);
199
+ const iterFromLog = parseIterationFromLog(logLines || []);
200
+ const iteration = iterFromLog
201
+ || (sessionData.reviewer_retry_count != null ? sessionData.reviewer_retry_count + 1 : null);
202
+
203
+ const stories = (options.stories || []).map(s => ({
204
+ id: s.id,
205
+ status: s.status || "pending",
206
+ title: s.title || s.original?.text || null,
207
+ blocked_by: s.blocked_by || [],
208
+ duration_ms: s.duration_ms || null,
209
+ }));
210
+
211
+ return {
212
+ ok: true,
213
+ pipeline: {
214
+ status: sessionData.status || "unknown",
215
+ iteration: iteration || null,
216
+ maxIterations: config.max_iterations ?? null,
217
+ duration_ms: computeDuration(sessionData),
218
+ coder: config.coder?.provider || config.roles?.coder?.provider || null,
219
+ reviewer: config.reviewer?.provider || config.roles?.reviewer?.provider || null,
220
+ },
221
+ hus: stories,
222
+ currentStage: logInfo.currentStage || "idle",
223
+ lastEvent: logInfo.lastEvent || null,
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Parse iteration number from log lines.
229
+ * @param {string[]} lines
230
+ * @returns {number|null}
231
+ */
232
+ function parseIterationFromLog(lines) {
233
+ let iteration = null;
234
+ for (const line of lines) {
235
+ const m = /[Ii]teration\s+(\d+)/.exec(line);
236
+ if (m) iteration = Number.parseInt(m[1], 10);
237
+ }
238
+ return iteration;
239
+ }
240
+
241
+ /**
242
+ * Compute pipeline duration in milliseconds from session timestamps.
243
+ * @param {object} sessionData
244
+ * @returns {number}
245
+ */
246
+ function computeDuration(sessionData) {
247
+ const created = sessionData.created_at;
248
+ if (!created) return 0;
249
+ const start = new Date(created).getTime();
250
+ const now = sessionData.updated_at
251
+ ? new Date(sessionData.updated_at).getTime()
252
+ : Date.now();
253
+ return Math.max(0, now - start);
254
+ }