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 +1 -1
- package/src/cli.js +9 -0
- package/src/commands/init.js +30 -0
- package/src/commands/status.js +35 -0
- package/src/mcp/handlers/management-handlers.js +22 -1
- package/src/utils/stack-detect.js +135 -0
- package/src/utils/status-dashboard.js +254 -0
package/package.json
CHANGED
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")
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|