karajan-code 1.34.0 → 1.34.2
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 +12 -0
- package/src/commands/board.js +125 -0
- package/src/commands/init.js +9 -0
- package/src/config.js +5 -0
- package/src/mcp/server-handlers.js +16 -1
- package/src/mcp/tools.js +11 -0
- package/src/orchestrator.js +24 -2
- package/templates/skills/kj-board.md +16 -0
- package/templates/skills/kj-run.md +4 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ import { triageCommand } from "./commands/triage.js";
|
|
|
23
23
|
import { researcherCommand } from "./commands/researcher.js";
|
|
24
24
|
import { architectCommand } from "./commands/architect.js";
|
|
25
25
|
import { auditCommand } from "./commands/audit.js";
|
|
26
|
+
import { boardCommand } from "./commands/board.js";
|
|
26
27
|
|
|
27
28
|
const PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
28
29
|
const PKG_VERSION = JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
|
|
@@ -305,6 +306,17 @@ program
|
|
|
305
306
|
}
|
|
306
307
|
});
|
|
307
308
|
|
|
309
|
+
program
|
|
310
|
+
.command("board [action]")
|
|
311
|
+
.description("Manage HU Board (start|stop|status|open)")
|
|
312
|
+
.option("--port <number>", "Port (default: 4000)", "4000")
|
|
313
|
+
.action(async (action = "start", opts) => {
|
|
314
|
+
await withConfig("board", opts, async ({ config, logger }) => {
|
|
315
|
+
const port = Number(opts.port) || config.hu_board?.port || 4000;
|
|
316
|
+
await boardCommand({ action, port, logger });
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
308
320
|
const sonar = program.command("sonar").description("Manage SonarQube container");
|
|
309
321
|
sonar.command("status").action(async () => sonarCommand({ action: "status" }));
|
|
310
322
|
sonar.command("start").action(async () => sonarCommand({ action: "start" }));
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
5
|
+
|
|
6
|
+
const BOARD_DIR = path.resolve(import.meta.dirname, "../../packages/hu-board");
|
|
7
|
+
const PID_FILE = path.join(getKarajanHome(), "hu-board.pid");
|
|
8
|
+
|
|
9
|
+
function readPid() {
|
|
10
|
+
try {
|
|
11
|
+
const pid = Number.parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
12
|
+
return Number.isFinite(pid) ? pid : null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isProcessAlive(pid) {
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function startBoard(port = 4000) {
|
|
28
|
+
const existingPid = readPid();
|
|
29
|
+
if (existingPid && isProcessAlive(existingPid)) {
|
|
30
|
+
return { ok: true, alreadyRunning: true, pid: existingPid, url: `http://localhost:${port}` };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const serverPath = path.join(BOARD_DIR, "src/server.js");
|
|
34
|
+
const karajanHome = getKarajanHome();
|
|
35
|
+
fs.mkdirSync(karajanHome, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const child = spawn("node", [serverPath], {
|
|
38
|
+
env: { ...process.env, PORT: String(port) },
|
|
39
|
+
detached: true,
|
|
40
|
+
stdio: "ignore",
|
|
41
|
+
cwd: BOARD_DIR
|
|
42
|
+
});
|
|
43
|
+
child.unref();
|
|
44
|
+
|
|
45
|
+
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
46
|
+
return { ok: true, alreadyRunning: false, pid: child.pid, url: `http://localhost:${port}` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function stopBoard() {
|
|
50
|
+
const pid = readPid();
|
|
51
|
+
if (!pid || !isProcessAlive(pid)) {
|
|
52
|
+
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
53
|
+
return { ok: true, wasRunning: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, "SIGTERM");
|
|
58
|
+
} catch {
|
|
59
|
+
// already dead
|
|
60
|
+
}
|
|
61
|
+
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
62
|
+
return { ok: true, wasRunning: true, pid };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function boardStatus(port = 4000) {
|
|
66
|
+
const pid = readPid();
|
|
67
|
+
const running = pid !== null && isProcessAlive(pid);
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
running,
|
|
71
|
+
pid: running ? pid : null,
|
|
72
|
+
url: running ? `http://localhost:${port}` : null
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function boardCommand({ action = "start", port = 4000, logger }) {
|
|
77
|
+
switch (action) {
|
|
78
|
+
case "start": {
|
|
79
|
+
const result = await startBoard(port);
|
|
80
|
+
if (result.alreadyRunning) {
|
|
81
|
+
logger.info(`HU Board already running (PID ${result.pid}) at ${result.url}`);
|
|
82
|
+
} else {
|
|
83
|
+
logger.info(`HU Board started (PID ${result.pid}) at ${result.url}`);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
case "stop": {
|
|
88
|
+
const result = await stopBoard();
|
|
89
|
+
if (result.wasRunning) {
|
|
90
|
+
logger.info(`HU Board stopped (PID ${result.pid})`);
|
|
91
|
+
} else {
|
|
92
|
+
logger.info("HU Board was not running");
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
case "status": {
|
|
97
|
+
const result = await boardStatus(port);
|
|
98
|
+
if (result.running) {
|
|
99
|
+
logger.info(`HU Board is running (PID ${result.pid}) at ${result.url}`);
|
|
100
|
+
} else {
|
|
101
|
+
logger.info("HU Board is not running");
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
case "open": {
|
|
106
|
+
const status = await boardStatus(port);
|
|
107
|
+
if (!status.running) {
|
|
108
|
+
logger.info("HU Board is not running. Starting it first...");
|
|
109
|
+
await startBoard(port);
|
|
110
|
+
}
|
|
111
|
+
const url = `http://localhost:${port}`;
|
|
112
|
+
const { default: open } = await import("open").catch(() => ({ default: null }));
|
|
113
|
+
if (open) {
|
|
114
|
+
await open(url);
|
|
115
|
+
logger.info(`Opened ${url}`);
|
|
116
|
+
} else {
|
|
117
|
+
logger.info(`Open in browser: ${url}`);
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, url };
|
|
120
|
+
}
|
|
121
|
+
default:
|
|
122
|
+
logger.error(`Unknown board action: ${action}. Use start|stop|status|open`);
|
|
123
|
+
return { ok: false, error: `Unknown action: ${action}` };
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -70,6 +70,15 @@ async function runWizard(config, logger) {
|
|
|
70
70
|
config.sonarqube.enabled = enableSonar;
|
|
71
71
|
logger.info(` -> SonarQube: ${enableSonar ? "enabled" : "disabled"}`);
|
|
72
72
|
|
|
73
|
+
const enableHuBoard = await wizard.confirm("Enable HU Board for story tracking?", false);
|
|
74
|
+
config.hu_board = config.hu_board || {};
|
|
75
|
+
config.hu_board.enabled = enableHuBoard;
|
|
76
|
+
if (enableHuBoard) {
|
|
77
|
+
config.hu_board.port = 4000;
|
|
78
|
+
config.hu_board.auto_start = true;
|
|
79
|
+
}
|
|
80
|
+
logger.info(` -> HU Board: ${enableHuBoard ? "enabled (auto-start on kj run)" : "disabled"}`);
|
|
81
|
+
|
|
73
82
|
const methodology = await wizard.select("Development methodology:", [
|
|
74
83
|
{ label: "TDD (test-driven development)", value: "tdd", available: true },
|
|
75
84
|
{ label: "Standard (no TDD enforcement)", value: "standard", available: true }
|
package/src/config.js
CHANGED
|
@@ -118,6 +118,11 @@ const DEFAULTS = {
|
|
|
118
118
|
test_inclusions: "**/*.test.js,**/*.spec.js,**/tests/**,**/__tests__/**"
|
|
119
119
|
}
|
|
120
120
|
},
|
|
121
|
+
hu_board: {
|
|
122
|
+
enabled: false,
|
|
123
|
+
port: 4000,
|
|
124
|
+
auto_start: false
|
|
125
|
+
},
|
|
121
126
|
policies: {},
|
|
122
127
|
serena: { enabled: false },
|
|
123
128
|
planning_game: { enabled: false, project_id: null, codeveloper: null },
|
|
@@ -863,6 +863,20 @@ async function handleAudit(a, server, extra) {
|
|
|
863
863
|
return handleAuditDirect(a, server, extra);
|
|
864
864
|
}
|
|
865
865
|
|
|
866
|
+
async function handleBoard(a) {
|
|
867
|
+
const action = a.action || "status";
|
|
868
|
+
const { loadConfig: lc } = await import("../config.js");
|
|
869
|
+
const { config } = await lc();
|
|
870
|
+
const port = a.port || config.hu_board?.port || 4000;
|
|
871
|
+
const { startBoard, stopBoard, boardStatus } = await import("../commands/board.js");
|
|
872
|
+
switch (action) {
|
|
873
|
+
case "start": return startBoard(port);
|
|
874
|
+
case "stop": return stopBoard();
|
|
875
|
+
case "status": return boardStatus(port);
|
|
876
|
+
default: return failPayload(`Unknown board action: ${action}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
866
880
|
/* ── Handler dispatch map ─────────────────────────────────────────── */
|
|
867
881
|
|
|
868
882
|
const toolHandlers = {
|
|
@@ -884,7 +898,8 @@ const toolHandlers = {
|
|
|
884
898
|
kj_triage: (a, server, extra) => handleTriage(a, server, extra),
|
|
885
899
|
kj_researcher: (a, server, extra) => handleResearcher(a, server, extra),
|
|
886
900
|
kj_architect: (a, server, extra) => handleArchitect(a, server, extra),
|
|
887
|
-
kj_audit: (a, server, extra) => handleAudit(a, server, extra)
|
|
901
|
+
kj_audit: (a, server, extra) => handleAudit(a, server, extra),
|
|
902
|
+
kj_board: (a) => handleBoard(a)
|
|
888
903
|
};
|
|
889
904
|
|
|
890
905
|
export async function handleToolCall(name, args, server, extra) {
|
package/src/mcp/tools.js
CHANGED
|
@@ -306,5 +306,16 @@ export const tools = [
|
|
|
306
306
|
kjHome: { type: "string" }
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: "kj_board",
|
|
312
|
+
description: "Start, stop, or check status of the HU Board dashboard",
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: "object",
|
|
315
|
+
properties: {
|
|
316
|
+
action: { type: "string", enum: ["start", "stop", "status"], description: "Action to perform (default: status)" },
|
|
317
|
+
port: { type: "number", description: "Port (default: 4000)" }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
309
320
|
}
|
|
310
321
|
];
|
package/src/orchestrator.js
CHANGED
|
@@ -156,8 +156,10 @@ function createBudgetManager({ config, emitter, eventBase }) {
|
|
|
156
156
|
return s;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
function trackBudget({ role, provider, model, result, duration_ms }) {
|
|
160
|
-
|
|
159
|
+
function trackBudget({ role, provider, model, result, duration_ms, promptSize }) {
|
|
160
|
+
// Attach promptSize to result if provided, so extractUsageMetrics can estimate tokens
|
|
161
|
+
const enrichedResult = promptSize && result ? { ...result, promptSize } : result;
|
|
162
|
+
const metrics = extractUsageMetrics(enrichedResult, model);
|
|
161
163
|
budgetTracker.record({ role, provider, ...metrics, duration_ms, stage_index: stageCounter++ });
|
|
162
164
|
|
|
163
165
|
if (!hasBudgetLimit) return;
|
|
@@ -1124,6 +1126,26 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1124
1126
|
}));
|
|
1125
1127
|
}
|
|
1126
1128
|
|
|
1129
|
+
// --- HU Board auto-start ---
|
|
1130
|
+
if (config.hu_board?.enabled && config.hu_board?.auto_start) {
|
|
1131
|
+
try {
|
|
1132
|
+
const { startBoard } = await import("./commands/board.js");
|
|
1133
|
+
const boardPort = config.hu_board.port || 4000;
|
|
1134
|
+
const boardResult = await startBoard(boardPort);
|
|
1135
|
+
if (boardResult.alreadyRunning) {
|
|
1136
|
+
logger.info(`HU Board already running at ${boardResult.url}`);
|
|
1137
|
+
} else {
|
|
1138
|
+
logger.info(`HU Board started at ${boardResult.url}`);
|
|
1139
|
+
}
|
|
1140
|
+
emitProgress(emitter, makeEvent("board:started", ctx.eventBase, {
|
|
1141
|
+
message: `HU Board running at ${boardResult.url}`,
|
|
1142
|
+
detail: { pid: boardResult.pid, port: boardPort }
|
|
1143
|
+
}));
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1127
1149
|
// --- Product Context ---
|
|
1128
1150
|
const ctxProjectDir = config.projectDir || process.cwd();
|
|
1129
1151
|
const { content: productContext, source: productContextSource } = await loadProductContext(ctxProjectDir);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# kj-board — HU Board Dashboard
|
|
2
|
+
|
|
3
|
+
Manage the Karajan HU Board for visualizing user stories and sessions.
|
|
4
|
+
|
|
5
|
+
## Your task
|
|
6
|
+
|
|
7
|
+
$ARGUMENTS
|
|
8
|
+
|
|
9
|
+
## Actions
|
|
10
|
+
|
|
11
|
+
- **Start**: Run `kj board start` to launch the dashboard
|
|
12
|
+
- **Stop**: Run `kj board stop` to stop it
|
|
13
|
+
- **Status**: Run `kj board status` to check if running
|
|
14
|
+
- **Open**: Run `kj board open` to open in browser
|
|
15
|
+
|
|
16
|
+
The board auto-syncs with KJ pipeline data. No manual refresh needed.
|
|
@@ -61,6 +61,10 @@ If all steps pass:
|
|
|
61
61
|
2. Commit with conventional commit message: `feat:`, `fix:`, `refactor:`, etc.
|
|
62
62
|
3. Do NOT push unless the user explicitly asks
|
|
63
63
|
|
|
64
|
+
## HU Board Integration
|
|
65
|
+
If HU Board is enabled (`hu_board.enabled: true` in config), stories and sessions
|
|
66
|
+
are automatically tracked and visible at the board URL. Run `kj board` to start it.
|
|
67
|
+
|
|
64
68
|
## Important rules
|
|
65
69
|
|
|
66
70
|
- **Never skip steps** — execute all applicable steps in order
|