karajan-code 1.54.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/bootstrap.js +3 -2
- package/src/cli.js +21 -0
- package/src/commands/doctor.js +6 -5
- package/src/commands/init.js +30 -0
- package/src/commands/status.js +35 -0
- package/src/commands/undo.js +62 -0
- package/src/mcp/handlers/management-handlers.js +34 -1
- package/src/mcp/server-handlers.js +11 -9
- package/src/mcp/tools.js +11 -0
- package/src/orchestrator/preflight-checks.js +4 -3
- package/src/utils/doc-links.js +27 -0
- package/src/utils/stack-detect.js +135 -0
- package/src/utils/status-dashboard.js +254 -0
package/package.json
CHANGED
package/src/bootstrap.js
CHANGED
|
@@ -22,6 +22,7 @@ import { checkBinary } from "./utils/agent-detect.js";
|
|
|
22
22
|
import { exists } from "./utils/fs.js";
|
|
23
23
|
import { getConfigPath, resolveRole } from "./config.js";
|
|
24
24
|
import { isSonarReachable, sonarUp } from "./sonar/manager.js";
|
|
25
|
+
import { withDocLink } from "./utils/doc-links.js";
|
|
25
26
|
|
|
26
27
|
const BOOTSTRAP_VERSION = 1;
|
|
27
28
|
const BOOTSTRAP_TTL_HOURS = 24;
|
|
@@ -185,14 +186,14 @@ function formatBootstrapFailure(failures) {
|
|
|
185
186
|
const lines = failures.map(f =>
|
|
186
187
|
` FAIL ${f.name}: ${f.detail}\n Fix: ${f.fix}`
|
|
187
188
|
);
|
|
188
|
-
return [
|
|
189
|
+
return withDocLink([
|
|
189
190
|
"BOOTSTRAP FAILED — Environment not ready for Karajan Code.\n",
|
|
190
191
|
"The following prerequisite(s) are not met:\n",
|
|
191
192
|
...lines,
|
|
192
193
|
"",
|
|
193
194
|
"Run 'kj_doctor' for a complete environment diagnostic.",
|
|
194
195
|
"Do NOT work around these issues — fix them properly."
|
|
195
|
-
].join("\n");
|
|
196
|
+
].join("\n"), "bootstrap_failed");
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
// ── Public API ───────────────────────────────────────────────────────
|
package/src/cli.js
CHANGED
|
@@ -24,6 +24,8 @@ import { researcherCommand } from "./commands/researcher.js";
|
|
|
24
24
|
import { architectCommand } from "./commands/architect.js";
|
|
25
25
|
import { auditCommand } from "./commands/audit.js";
|
|
26
26
|
import { boardCommand } from "./commands/board.js";
|
|
27
|
+
import { undoCommand } from "./commands/undo.js";
|
|
28
|
+
import { statusCommand } from "./commands/status.js";
|
|
27
29
|
|
|
28
30
|
import { printUpdateNotice } from "./utils/update-check.js";
|
|
29
31
|
|
|
@@ -167,6 +169,14 @@ program
|
|
|
167
169
|
await withConfig("doctor", {}, doctorCommand);
|
|
168
170
|
});
|
|
169
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
|
+
|
|
170
180
|
program
|
|
171
181
|
.command("report")
|
|
172
182
|
.description("Show latest session report")
|
|
@@ -324,6 +334,17 @@ program
|
|
|
324
334
|
});
|
|
325
335
|
});
|
|
326
336
|
|
|
337
|
+
program
|
|
338
|
+
.command("undo")
|
|
339
|
+
.description("Revert last pipeline run")
|
|
340
|
+
.option("--hard", "Discard all changes (default: soft reset, keeps changes staged)")
|
|
341
|
+
.action(async (flags) => {
|
|
342
|
+
await withConfig("undo", flags, async ({ logger }) => {
|
|
343
|
+
const result = await undoCommand({ hard: !!flags.hard, logger });
|
|
344
|
+
if (!result.ok) process.exit(1);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
327
348
|
const sonar = program.command("sonar").description("Manage SonarQube container");
|
|
328
349
|
sonar.command("status").action(async () => sonarCommand({ action: "status" }));
|
|
329
350
|
sonar.command("start").action(async () => sonarCommand({ action: "start" }));
|
package/src/commands/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
|
|
|
11
11
|
import { ensureGitRepo } from "../utils/git.js";
|
|
12
12
|
import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
|
|
13
13
|
import { getInstallCommand } from "../utils/os-detect.js";
|
|
14
|
+
import { withDocLink } from "../utils/doc-links.js";
|
|
14
15
|
|
|
15
16
|
function getPackageVersion() {
|
|
16
17
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
@@ -36,7 +37,7 @@ async function checkConfigFile() {
|
|
|
36
37
|
label: "Config file",
|
|
37
38
|
ok: configExists,
|
|
38
39
|
detail: configExists ? configPath : "Not found",
|
|
39
|
-
fix: configExists ? null : "Run 'kj init' to create the config file."
|
|
40
|
+
fix: configExists ? null : withDocLink("Run 'kj init' to create the config file.", "config_missing")
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -63,7 +64,7 @@ async function checkDocker() {
|
|
|
63
64
|
label: "Docker",
|
|
64
65
|
ok: docker.ok,
|
|
65
66
|
detail: docker.ok ? docker.version : "Not found",
|
|
66
|
-
fix: docker.ok ? null : "Install Docker: https://docs.docker.com/get-docker/"
|
|
67
|
+
fix: docker.ok ? null : withDocLink("Install Docker: https://docs.docker.com/get-docker/", "sonar_docker")
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -91,7 +92,7 @@ async function checkSonarQube(config) {
|
|
|
91
92
|
detail: sonarDetail(config, sonarOk, sonarHost),
|
|
92
93
|
fix: isOkOrDisabled
|
|
93
94
|
? null
|
|
94
|
-
: "Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip."
|
|
95
|
+
: withDocLink("Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip.", "sonar_docker")
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -104,7 +105,7 @@ async function checkAgentCLIs() {
|
|
|
104
105
|
label: `Agent: ${agent.name}`,
|
|
105
106
|
ok: result.ok,
|
|
106
107
|
detail: result.ok ? `${result.version} (${result.path})` : "Not found",
|
|
107
|
-
fix: result.ok ? null : `Install: ${agent.install}
|
|
108
|
+
fix: result.ok ? null : withDocLink(`Install: ${agent.install}`, "agent_not_found")
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
return checks;
|
|
@@ -220,7 +221,7 @@ async function checkRtk() {
|
|
|
220
221
|
label: "RTK (Rust Token Killer)",
|
|
221
222
|
ok: false,
|
|
222
223
|
detail: "Not found — 60-90% token savings available",
|
|
223
|
-
fix: `Install: ${installCmd}
|
|
224
|
+
fix: withDocLink(`Install: ${installCmd}`, "rtk_install")
|
|
224
225
|
};
|
|
225
226
|
}
|
|
226
227
|
|
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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { loadMostRecentSession } from "../session-store.js";
|
|
2
|
+
import { runCommand } from "../utils/process.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Revert the last pipeline run by resetting to the pre-pipeline commit.
|
|
6
|
+
* Default: soft reset (keeps changes staged). With --hard: discards all changes.
|
|
7
|
+
*/
|
|
8
|
+
export async function undoCommand({ hard = false, logger } = {}) {
|
|
9
|
+
const session = await loadMostRecentSession();
|
|
10
|
+
if (!session) {
|
|
11
|
+
const msg = "No session to undo. No sessions found in the sessions directory.";
|
|
12
|
+
if (logger) logger.error(msg);
|
|
13
|
+
return { ok: false, error: msg };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sha = session.session_start_sha;
|
|
17
|
+
if (!sha) {
|
|
18
|
+
const msg = `Session ${session.id} has no session_start_sha. Cannot determine the pre-pipeline commit.`;
|
|
19
|
+
if (logger) logger.error(msg);
|
|
20
|
+
return { ok: false, error: msg };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Verify the SHA exists in git history
|
|
24
|
+
const verifyRes = await runCommand("git", ["cat-file", "-t", sha]);
|
|
25
|
+
if (verifyRes.exitCode !== 0 || verifyRes.stdout.trim() !== "commit") {
|
|
26
|
+
const msg = `Commit ${sha} not found in git history. It may have been garbage-collected or belong to a different repository.`;
|
|
27
|
+
if (logger) logger.error(msg);
|
|
28
|
+
return { ok: false, error: msg };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check there are commits after the SHA (HEAD is ahead)
|
|
32
|
+
const logRes = await runCommand("git", ["log", "--oneline", `${sha}..HEAD`]);
|
|
33
|
+
if (logRes.exitCode !== 0 || !logRes.stdout.trim()) {
|
|
34
|
+
const msg = `No commits to undo. HEAD is already at or before ${sha.slice(0, 8)}.`;
|
|
35
|
+
if (logger) logger.warn(msg);
|
|
36
|
+
return { ok: false, error: msg };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const mode = hard ? "--hard" : "--soft";
|
|
40
|
+
const resetRes = await runCommand("git", ["reset", mode, sha]);
|
|
41
|
+
if (resetRes.exitCode !== 0) {
|
|
42
|
+
const msg = `git reset ${mode} ${sha} failed: ${resetRes.stderr || resetRes.stdout}`;
|
|
43
|
+
if (logger) logger.error(msg);
|
|
44
|
+
return { ok: false, error: msg };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const shortSha = sha.slice(0, 8);
|
|
48
|
+
const message = hard
|
|
49
|
+
? `Reverted to pre-pipeline state (commit ${shortSha}). All changes discarded.`
|
|
50
|
+
: `Reverted to pre-pipeline state (commit ${shortSha}). Changes are staged. Use \`git diff --cached\` to review.`;
|
|
51
|
+
|
|
52
|
+
if (logger) logger.info(message);
|
|
53
|
+
console.log(message);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
message,
|
|
58
|
+
sessionId: session.id,
|
|
59
|
+
sha,
|
|
60
|
+
mode: hard ? "hard" : "soft"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -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) {
|
|
@@ -163,6 +184,18 @@ export async function handleBoard(a) {
|
|
|
163
184
|
}
|
|
164
185
|
}
|
|
165
186
|
|
|
187
|
+
export async function handleUndo(a, server) {
|
|
188
|
+
const projectDir = await resolveProjectDir(server, a.projectDir);
|
|
189
|
+
const origCwd = process.cwd();
|
|
190
|
+
try {
|
|
191
|
+
process.chdir(projectDir);
|
|
192
|
+
const { undoCommand } = await import("../../commands/undo.js");
|
|
193
|
+
return undoCommand({ hard: !!a.hard });
|
|
194
|
+
} finally {
|
|
195
|
+
process.chdir(origCwd);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
166
199
|
export async function buildPreflightRequiredResponse(toolName) {
|
|
167
200
|
const { config } = await loadConfig();
|
|
168
201
|
const { listAgents } = await import("../../commands/agents.js");
|
|
@@ -10,11 +10,12 @@ import { loadConfig, applyRunOverrides, validateConfig } from "../config.js";
|
|
|
10
10
|
import { currentBranch } from "../utils/git.js";
|
|
11
11
|
import { buildProgressNotifier } from "./progress.js";
|
|
12
12
|
import { compressResponse, compactStringify } from "./response-compressor.js";
|
|
13
|
+
import { withDocLink } from "../utils/doc-links.js";
|
|
13
14
|
|
|
14
15
|
// ── Sub-module re-exports ────────────────────────────────────────────
|
|
15
16
|
export { handleRunDirect, handleResumeDirect, validateResumeAnswer, handleRun, handleResume } from "./handlers/run-handler.js";
|
|
16
17
|
export { handlePlanDirect, handleCodeDirect, handleReviewDirect, handleDiscoverDirect, handleTriageDirect, handleResearcherDirect, handleAuditDirect, handleArchitectDirect, handleCode, handleReview, handlePlan, handleDiscover, handleTriage, handleResearcher, handleArchitect, handleAudit } from "./handlers/direct-handlers.js";
|
|
17
|
-
export { handleStatus, handleAgents, handlePreflight, handleRoles, handleReport, handleInit, handleDoctor, handleConfig, handleScan, handleBoard, buildPreflightRequiredResponse } from "./handlers/management-handlers.js";
|
|
18
|
+
export { handleStatus, handleAgents, handlePreflight, handleRoles, handleReport, handleInit, handleDoctor, handleConfig, handleScan, handleBoard, handleUndo, buildPreflightRequiredResponse } from "./handlers/management-handlers.js";
|
|
18
19
|
export { handleHu, handleSkills, handleSuggest } from "./handlers/hu-handlers.js";
|
|
19
20
|
|
|
20
21
|
// ── Shared helpers (used by sub-modules and external consumers) ──────
|
|
@@ -82,22 +83,22 @@ const ERROR_CLASSIFIERS = [
|
|
|
82
83
|
{
|
|
83
84
|
test: (lower) => lower.includes("sonar") && (lower.includes("connect") || lower.includes("econnrefused") || lower.includes("not available") || lower.includes("not running")),
|
|
84
85
|
category: "sonar_unavailable",
|
|
85
|
-
suggestion: "SonarQube is not reachable. Try: kj_init to set up SonarQube, or run 'docker start sonarqube' if already installed. Use --no-sonar to skip SonarQube."
|
|
86
|
+
suggestion: withDocLink("SonarQube is not reachable. Try: kj_init to set up SonarQube, or run 'docker start sonarqube' if already installed. Use --no-sonar to skip SonarQube.", "sonar_docker")
|
|
86
87
|
},
|
|
87
88
|
{
|
|
88
89
|
test: (lower) => lower.includes("401") || lower.includes("unauthorized") || lower.includes("invalid token"),
|
|
89
90
|
category: "auth_error",
|
|
90
|
-
suggestion: "Authentication failed. Regenerate the SonarQube token and update it via kj_init or in ~/.karajan/kj.config.yml under sonarqube.token."
|
|
91
|
+
suggestion: withDocLink("Authentication failed. Regenerate the SonarQube token and update it via kj_init or in ~/.karajan/kj.config.yml under sonarqube.token.", "sonar_token")
|
|
91
92
|
},
|
|
92
93
|
{
|
|
93
94
|
test: (lower) => lower.includes("config") && (lower.includes("missing") || lower.includes("not found") || lower.includes("invalid")),
|
|
94
95
|
category: "config_error",
|
|
95
|
-
suggestion: "Configuration issue detected. Run kj_doctor to diagnose, or kj_init to create a fresh config."
|
|
96
|
+
suggestion: withDocLink("Configuration issue detected. Run kj_doctor to diagnose, or kj_init to create a fresh config.", "config_missing")
|
|
96
97
|
},
|
|
97
98
|
{
|
|
98
99
|
test: (lower) => lower.includes("missing provider") || lower.includes("not found") && (lower.includes("claude") || lower.includes("codex") || lower.includes("gemini") || lower.includes("aider")),
|
|
99
100
|
category: "agent_missing",
|
|
100
|
-
suggestion: "Required agent CLI not found. Run kj_doctor to check which agents are installed and get installation instructions."
|
|
101
|
+
suggestion: withDocLink("Required agent CLI not found. Run kj_doctor to check which agents are installed and get installation instructions.", "agent_not_found")
|
|
101
102
|
},
|
|
102
103
|
{
|
|
103
104
|
test: (lower) => lower.includes("timed out") || lower.includes("timeout"),
|
|
@@ -107,7 +108,7 @@ const ERROR_CLASSIFIERS = [
|
|
|
107
108
|
{
|
|
108
109
|
test: (lower) => lower.includes("you are on the base branch"),
|
|
109
110
|
category: "branch_error",
|
|
110
|
-
suggestion: "Create a feature branch before running Karajan. Use 'git checkout -b feat/<task-description>' and then retry. Do NOT run kj_code directly on the base branch."
|
|
111
|
+
suggestion: withDocLink("Create a feature branch before running Karajan. Use 'git checkout -b feat/<task-description>' and then retry. Do NOT run kj_code directly on the base branch.", "branch_error")
|
|
111
112
|
},
|
|
112
113
|
{
|
|
113
114
|
test: (lower) => lower.includes("not a git repository"),
|
|
@@ -117,7 +118,7 @@ const ERROR_CLASSIFIERS = [
|
|
|
117
118
|
{
|
|
118
119
|
test: (lower) => lower.includes("bootstrap failed"),
|
|
119
120
|
category: "bootstrap_error",
|
|
120
|
-
suggestion: "Environment prerequisites not met. Run kj_doctor for diagnostics, then fix the issues listed. Do NOT work around these — fix them properly."
|
|
121
|
+
suggestion: withDocLink("Environment prerequisites not met. Run kj_doctor for diagnostics, then fix the issues listed. Do NOT work around these — fix them properly.", "bootstrap_failed")
|
|
121
122
|
}
|
|
122
123
|
];
|
|
123
124
|
|
|
@@ -218,7 +219,7 @@ import {
|
|
|
218
219
|
import {
|
|
219
220
|
handleStatus, handleAgents, handlePreflight, handleRoles,
|
|
220
221
|
handleReport, handleInit, handleDoctor, handleConfig,
|
|
221
|
-
handleScan, handleBoard
|
|
222
|
+
handleScan, handleBoard, handleUndo
|
|
222
223
|
} from "./handlers/management-handlers.js";
|
|
223
224
|
import { handleHu, handleSkills, handleSuggest } from "./handlers/hu-handlers.js";
|
|
224
225
|
|
|
@@ -245,7 +246,8 @@ const toolHandlers = {
|
|
|
245
246
|
kj_board: (a) => handleBoard(a),
|
|
246
247
|
kj_hu: (a, server) => handleHu(a, server),
|
|
247
248
|
kj_suggest: (a) => handleSuggest(a),
|
|
248
|
-
kj_skills: (a) => handleSkills(a)
|
|
249
|
+
kj_skills: (a) => handleSkills(a),
|
|
250
|
+
kj_undo: (a, server) => handleUndo(a, server)
|
|
249
251
|
};
|
|
250
252
|
|
|
251
253
|
export async function handleToolCall(name, args, server, extra) {
|
package/src/mcp/tools.js
CHANGED
|
@@ -364,5 +364,16 @@ export const tools = [
|
|
|
364
364
|
},
|
|
365
365
|
required: ["action"]
|
|
366
366
|
}
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "kj_undo",
|
|
370
|
+
description: "Revert the last pipeline run by resetting to the pre-pipeline commit. Default: soft reset (keeps changes staged). Use hard=true to discard all changes.",
|
|
371
|
+
inputSchema: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
hard: { type: "boolean", description: "If true, discard all changes (git reset --hard). Default: false (soft reset, keeps changes staged)." },
|
|
375
|
+
projectDir: { type: "string", description: "Absolute path to the project directory" }
|
|
376
|
+
}
|
|
377
|
+
}
|
|
367
378
|
}
|
|
368
379
|
];
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
resolveSonarToken,
|
|
20
20
|
resolveSonarCredentials,
|
|
21
21
|
} from "../sonar/config-resolver.js";
|
|
22
|
+
import { withDocLink } from "../utils/doc-links.js";
|
|
22
23
|
|
|
23
24
|
function parseJsonSafe(text) {
|
|
24
25
|
try {
|
|
@@ -188,7 +189,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
188
189
|
result.errors.push({
|
|
189
190
|
check: "docker",
|
|
190
191
|
message: "Docker not available but SonarQube is enabled.",
|
|
191
|
-
fix: "Start Docker, or disable SonarQube: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar."
|
|
192
|
+
fix: withDocLink("Start Docker, or disable SonarQube: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar.", "sonar_docker")
|
|
192
193
|
});
|
|
193
194
|
logger.error("Preflight: Docker not found — SonarQube requires Docker");
|
|
194
195
|
|
|
@@ -222,7 +223,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
222
223
|
result.errors.push({
|
|
223
224
|
check: "sonar-reachable",
|
|
224
225
|
message: `SonarQube not reachable at ${sonarHost}.`,
|
|
225
|
-
fix: "Start SonarQube: 'docker start karajan-sonarqube', or disable it: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar."
|
|
226
|
+
fix: withDocLink("Start SonarQube: 'docker start karajan-sonarqube', or disable it: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar.", "sonar_docker")
|
|
226
227
|
});
|
|
227
228
|
logger.error("Preflight: SonarQube not reachable after remediation attempt");
|
|
228
229
|
}
|
|
@@ -248,7 +249,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
248
249
|
result.errors.push({
|
|
249
250
|
check: "sonar-auth",
|
|
250
251
|
message: "SonarQube is running but no authentication token is configured.",
|
|
251
|
-
fix: "Fix: run 'kj init' to configure it, or set KJ_SONAR_TOKEN env var, or add sonarqube.token to ~/.karajan/kj.config.yml."
|
|
252
|
+
fix: withDocLink("Fix: run 'kj init' to configure it, or set KJ_SONAR_TOKEN env var, or add sonarqube.token to ~/.karajan/kj.config.yml.", "sonar_token")
|
|
252
253
|
});
|
|
253
254
|
logger.error("Preflight: Sonar auth failed");
|
|
254
255
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation links for error messages.
|
|
3
|
+
*
|
|
4
|
+
* Maps error types to their relevant documentation page so that
|
|
5
|
+
* every actionable error includes a "See: <url>" pointer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const BASE_URL = "https://karajancode.com/docs";
|
|
9
|
+
|
|
10
|
+
const ERROR_DOCS = {
|
|
11
|
+
sonar_token: `${BASE_URL}/guides/configuration/#sonarqube`,
|
|
12
|
+
sonar_docker: `${BASE_URL}/getting-started/installation/#docker`,
|
|
13
|
+
agent_not_found: `${BASE_URL}/getting-started/installation/#agents`,
|
|
14
|
+
bootstrap_failed: `${BASE_URL}/guides/troubleshooting/`,
|
|
15
|
+
config_missing: `${BASE_URL}/getting-started/quick-start/`,
|
|
16
|
+
branch_error: `${BASE_URL}/guides/pipeline/#git-workflow`,
|
|
17
|
+
rtk_install: `${BASE_URL}/guides/configuration/#rtk`,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function docLink(errorType) {
|
|
21
|
+
return ERROR_DOCS[errorType] || `${BASE_URL}/guides/troubleshooting/`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function withDocLink(message, errorType) {
|
|
25
|
+
const link = docLink(errorType);
|
|
26
|
+
return `${message}\n See: ${link}`;
|
|
27
|
+
}
|
|
@@ -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
|
+
}
|