karajan-code 1.34.0 → 1.34.3
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/doctor.js +9 -27
- package/src/commands/init.js +9 -0
- package/src/commands/report.js +33 -37
- package/src/config.js +32 -15
- package/src/mcp/server-handlers.js +48 -18
- package/src/mcp/tools.js +11 -0
- package/src/orchestrator.js +112 -71
- package/src/utils/display.js +13 -7
- 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/doctor.js
CHANGED
|
@@ -162,23 +162,23 @@ async function checkBecariaSecrets() {
|
|
|
162
162
|
const { detectRepo } = await import("../becaria/repo.js");
|
|
163
163
|
const repo = await detectRepo();
|
|
164
164
|
if (!repo) return null;
|
|
165
|
+
|
|
165
166
|
const secretsRes = await runCommand("gh", ["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
|
|
166
167
|
if (secretsRes.exitCode !== 0) return null;
|
|
168
|
+
|
|
167
169
|
const names = new Set(secretsRes.stdout.split("\n").map((s) => s.trim()));
|
|
168
170
|
const hasAppId = names.has("BECARIA_APP_ID");
|
|
169
171
|
const hasKey = names.has("BECARIA_APP_PRIVATE_KEY");
|
|
170
172
|
const secretsOk = hasAppId && hasKey;
|
|
173
|
+
const missing = [!hasAppId && "BECARIA_APP_ID", !hasKey && "BECARIA_APP_PRIVATE_KEY"].filter(Boolean).join(" ");
|
|
171
174
|
return {
|
|
172
175
|
name: "becaria:secrets",
|
|
173
176
|
label: "BecarIA: GitHub secrets",
|
|
174
177
|
ok: secretsOk,
|
|
175
|
-
detail: secretsOk
|
|
176
|
-
? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found"
|
|
177
|
-
: `Missing: ${[!hasAppId && "BECARIA_APP_ID", !hasKey && "BECARIA_APP_PRIVATE_KEY"].filter(Boolean).join(" ")}`,
|
|
178
|
+
detail: secretsOk ? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found" : `Missing: ${missing}`,
|
|
178
179
|
fix: secretsOk ? null : "Add BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY as GitHub repository secrets"
|
|
179
180
|
};
|
|
180
181
|
} catch {
|
|
181
|
-
// Skip secrets check if we can't access the API
|
|
182
182
|
return null;
|
|
183
183
|
}
|
|
184
184
|
}
|
|
@@ -205,33 +205,15 @@ async function checkBecariaInfra(config) {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
async function checkRtk() {
|
|
208
|
+
const NOT_FOUND_DETAIL = "Not found — install for 60-90% token savings: brew install rtk";
|
|
209
|
+
let detail = NOT_FOUND_DETAIL;
|
|
208
210
|
try {
|
|
209
211
|
const res = await runCommand("rtk", ["--version"]);
|
|
210
212
|
if (res.exitCode === 0) {
|
|
211
|
-
|
|
212
|
-
name: "rtk",
|
|
213
|
-
label: "RTK (Rust Token Killer)",
|
|
214
|
-
ok: true,
|
|
215
|
-
detail: `${res.stdout.trim()} — token savings active`,
|
|
216
|
-
fix: null
|
|
217
|
-
};
|
|
213
|
+
detail = `${res.stdout.trim()} — token savings active`;
|
|
218
214
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
label: "RTK (Rust Token Killer)",
|
|
222
|
-
ok: true,
|
|
223
|
-
detail: "Not found — install for 60-90% token savings: brew install rtk",
|
|
224
|
-
fix: null
|
|
225
|
-
};
|
|
226
|
-
} catch {
|
|
227
|
-
return {
|
|
228
|
-
name: "rtk",
|
|
229
|
-
label: "RTK (Rust Token Killer)",
|
|
230
|
-
ok: true,
|
|
231
|
-
detail: "Not found — install for 60-90% token savings: brew install rtk",
|
|
232
|
-
fix: null
|
|
233
|
-
};
|
|
234
|
-
}
|
|
215
|
+
} catch { /* not installed */ }
|
|
216
|
+
return { name: "rtk", label: "RTK (Rust Token Killer)", ok: true, detail, fix: null };
|
|
235
217
|
}
|
|
236
218
|
|
|
237
219
|
async function checkRuleFiles(config) {
|
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/commands/report.js
CHANGED
|
@@ -155,50 +155,49 @@ async function buildReport(dir, sessionId) {
|
|
|
155
155
|
return report;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
: "";
|
|
164
|
-
budgetText = `$${report.budget_consumed.consumed_usd.toFixed(2)}${limitSuffix}`;
|
|
165
|
-
}
|
|
158
|
+
function formatBudgetText(budget) {
|
|
159
|
+
if (typeof budget?.consumed_usd !== "number") return "N/A";
|
|
160
|
+
const limitSuffix = typeof budget.limit_usd === "number" ? ` / $${budget.limit_usd.toFixed(2)}` : "";
|
|
161
|
+
return `$${budget.consumed_usd.toFixed(2)}${limitSuffix}`;
|
|
162
|
+
}
|
|
166
163
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
164
|
+
function formatIterationsText(iterations) {
|
|
165
|
+
if (iterations.length === 0) return "N/A";
|
|
166
|
+
return iterations
|
|
167
|
+
.map((item) => `#${item.iteration} coder=${item.coder_runs} reviewer_attempts=${item.reviewer_attempts} approved=${item.reviewer_approved}`)
|
|
168
|
+
.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatCommitsText(commits) {
|
|
172
|
+
return commits.ids.length > 0
|
|
173
|
+
? `${commits.count} (${commits.ids.join(", ")})`
|
|
174
|
+
: String(commits.count);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function printPgCardLine(report) {
|
|
178
|
+
if (!report.pg_task_id) return;
|
|
179
|
+
const projectLabel = report.pg_project_id ? ` (${report.pg_project_id})` : "";
|
|
180
|
+
console.log(`Planning Game Card: ${report.pg_task_id}${projectLabel}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function printTextReport(report) {
|
|
184
|
+
const sonar = report.sonar_issues_resolved;
|
|
181
185
|
|
|
182
186
|
console.log(`Session: ${report.session_id}`);
|
|
183
|
-
|
|
184
|
-
const projectLabel = report.pg_project_id ? ` (${report.pg_project_id})` : "";
|
|
185
|
-
console.log(`Planning Game Card: ${report.pg_task_id}${projectLabel}`);
|
|
186
|
-
}
|
|
187
|
+
printPgCardLine(report);
|
|
187
188
|
console.log(`Status: ${report.status}`);
|
|
188
189
|
console.log("Task Description:");
|
|
189
190
|
console.log(report.task_description || "N/A");
|
|
190
191
|
console.log("Plan Executed:");
|
|
191
|
-
console.log(
|
|
192
|
+
console.log(report.plan_executed.length > 0 ? report.plan_executed.join(" -> ") : "N/A");
|
|
192
193
|
console.log("Iterations (Coder/Reviewer):");
|
|
193
|
-
console.log(
|
|
194
|
+
console.log(formatIterationsText(report.iterations));
|
|
194
195
|
console.log("Sonar Issues Resolved:");
|
|
195
|
-
console.log(
|
|
196
|
-
`initial=${report.sonar_issues_resolved.initial_open_issues ?? "N/A"} final=${report.sonar_issues_resolved.final_open_issues ?? "N/A"} resolved=${report.sonar_issues_resolved.resolved}`
|
|
197
|
-
);
|
|
196
|
+
console.log(`initial=${sonar.initial_open_issues ?? "N/A"} final=${sonar.final_open_issues ?? "N/A"} resolved=${sonar.resolved}`);
|
|
198
197
|
console.log("Budget Consumed:");
|
|
199
|
-
console.log(
|
|
198
|
+
console.log(formatBudgetText(report.budget_consumed));
|
|
200
199
|
console.log("Commits Generated:");
|
|
201
|
-
console.log(
|
|
200
|
+
console.log(formatCommitsText(report.commits_generated));
|
|
202
201
|
}
|
|
203
202
|
|
|
204
203
|
function formatDuration(ms) {
|
|
@@ -329,10 +328,7 @@ async function resolveTraceOptions(currency) {
|
|
|
329
328
|
|
|
330
329
|
function printTraceReport(report, currency, exchangeRate) {
|
|
331
330
|
console.log(`Session: ${report.session_id}`);
|
|
332
|
-
|
|
333
|
-
const projectLabel = report.pg_project_id ? ` (${report.pg_project_id})` : "";
|
|
334
|
-
console.log(`Planning Game Card: ${report.pg_task_id}${projectLabel}`);
|
|
335
|
-
}
|
|
331
|
+
printPgCardLine(report);
|
|
336
332
|
console.log(`Status: ${report.status}`);
|
|
337
333
|
console.log(`Task: ${report.task_description || "N/A"}`);
|
|
338
334
|
console.log("");
|
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 },
|
|
@@ -448,27 +453,39 @@ export function isModelCompatible(agent, model) {
|
|
|
448
453
|
return true;
|
|
449
454
|
}
|
|
450
455
|
|
|
456
|
+
// Roles that inherit provider/model from the coder when not explicitly configured
|
|
457
|
+
const CODER_INHERITED_ROLES = new Set([
|
|
458
|
+
"planner", "refactorer", "solomon", "researcher", "tester", "security",
|
|
459
|
+
"impeccable", "triage", "discover", "architect", "audit", "hu_reviewer", "hu-reviewer"
|
|
460
|
+
]);
|
|
461
|
+
|
|
462
|
+
function resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer) {
|
|
463
|
+
if (roleConfig.provider) return roleConfig.provider;
|
|
464
|
+
if (role === "coder") return legacyCoder;
|
|
465
|
+
if (role === "reviewer") return legacyReviewer;
|
|
466
|
+
if (CODER_INHERITED_ROLES.has(role)) return roles.coder?.provider || legacyCoder;
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function resolveModel(roleConfig, role, config) {
|
|
471
|
+
if (roleConfig.model) return { model: roleConfig.model, inherited: false };
|
|
472
|
+
if (role === "coder") return { model: config?.coder_options?.model ?? null, inherited: false };
|
|
473
|
+
if (role === "reviewer") return { model: config?.reviewer_options?.model ?? null, inherited: false };
|
|
474
|
+
if (CODER_INHERITED_ROLES.has(role)) {
|
|
475
|
+
const model = config?.coder_options?.model ?? null;
|
|
476
|
+
return { model, inherited: !!model };
|
|
477
|
+
}
|
|
478
|
+
return { model: null, inherited: false };
|
|
479
|
+
}
|
|
480
|
+
|
|
451
481
|
export function resolveRole(config, role) {
|
|
452
482
|
const roles = config?.roles || {};
|
|
453
483
|
const roleConfig = roles[role] || {};
|
|
454
484
|
const legacyCoder = config?.coder || null;
|
|
455
485
|
const legacyReviewer = config?.reviewer || null;
|
|
456
486
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (!provider && role === "reviewer") provider = legacyReviewer;
|
|
460
|
-
if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect" || role === "audit" || role === "hu_reviewer" || role === "hu-reviewer")) {
|
|
461
|
-
provider = roles.coder?.provider || legacyCoder;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
let model = roleConfig.model ?? null;
|
|
465
|
-
let modelIsInherited = false;
|
|
466
|
-
if (!model && role === "coder") model = config?.coder_options?.model ?? null;
|
|
467
|
-
if (!model && role === "reviewer") model = config?.reviewer_options?.model ?? null;
|
|
468
|
-
if (!model && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect" || role === "hu_reviewer" || role === "hu-reviewer")) {
|
|
469
|
-
model = config?.coder_options?.model ?? null;
|
|
470
|
-
modelIsInherited = !!model;
|
|
471
|
-
}
|
|
487
|
+
const provider = resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer);
|
|
488
|
+
let { model, inherited: modelIsInherited } = resolveModel(roleConfig, role, config);
|
|
472
489
|
|
|
473
490
|
// Drop inherited model if incompatible with the resolved provider
|
|
474
491
|
if (modelIsInherited && provider && model && !isModelCompatible(provider, model)) {
|
|
@@ -252,6 +252,31 @@ async function attemptAutoResume({ err, config, logger, emitter, askQuestion, ru
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
const PIPELINE_PROVIDER_ROLES = [
|
|
256
|
+
["triage", true],
|
|
257
|
+
["planner", true],
|
|
258
|
+
["refactorer", true],
|
|
259
|
+
["researcher", true],
|
|
260
|
+
["tester", true],
|
|
261
|
+
["security", true]
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
function collectRequiredProviders(config) {
|
|
265
|
+
const providers = [
|
|
266
|
+
resolveRole(config, "coder").provider,
|
|
267
|
+
config.reviewer_options?.fallback_reviewer
|
|
268
|
+
];
|
|
269
|
+
if (config.pipeline?.reviewer?.enabled !== false) {
|
|
270
|
+
providers.push(resolveRole(config, "reviewer").provider);
|
|
271
|
+
}
|
|
272
|
+
for (const [role, requireEnabled] of PIPELINE_PROVIDER_ROLES) {
|
|
273
|
+
if (requireEnabled && config.pipeline?.[role]?.enabled) {
|
|
274
|
+
providers.push(resolveRole(config, role).provider);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return providers;
|
|
278
|
+
}
|
|
279
|
+
|
|
255
280
|
export async function handleRunDirect(a, server, extra) {
|
|
256
281
|
const config = await buildConfig(a);
|
|
257
282
|
await assertNotOnBaseBranch(config);
|
|
@@ -263,20 +288,7 @@ export async function handleRunDirect(a, server, extra) {
|
|
|
263
288
|
await cleanupExpiredSessions({ logger });
|
|
264
289
|
} catch { /* non-blocking */ }
|
|
265
290
|
|
|
266
|
-
|
|
267
|
-
resolveRole(config, "coder").provider,
|
|
268
|
-
config.reviewer_options?.fallback_reviewer
|
|
269
|
-
];
|
|
270
|
-
if (config.pipeline?.reviewer?.enabled !== false) {
|
|
271
|
-
requiredProviders.push(resolveRole(config, "reviewer").provider);
|
|
272
|
-
}
|
|
273
|
-
if (config.pipeline?.triage?.enabled) requiredProviders.push(resolveRole(config, "triage").provider);
|
|
274
|
-
if (config.pipeline?.planner?.enabled) requiredProviders.push(resolveRole(config, "planner").provider);
|
|
275
|
-
if (config.pipeline?.refactorer?.enabled) requiredProviders.push(resolveRole(config, "refactorer").provider);
|
|
276
|
-
if (config.pipeline?.researcher?.enabled) requiredProviders.push(resolveRole(config, "researcher").provider);
|
|
277
|
-
if (config.pipeline?.tester?.enabled) requiredProviders.push(resolveRole(config, "tester").provider);
|
|
278
|
-
if (config.pipeline?.security?.enabled) requiredProviders.push(resolveRole(config, "security").provider);
|
|
279
|
-
await assertAgentsAvailable(requiredProviders);
|
|
291
|
+
await assertAgentsAvailable(collectRequiredProviders(config));
|
|
280
292
|
|
|
281
293
|
const projectDir = await resolveProjectDir(server, a.projectDir);
|
|
282
294
|
const runLog = createRunLog(projectDir);
|
|
@@ -343,13 +355,16 @@ export async function handleResumeDirect(a, server, extra) {
|
|
|
343
355
|
}
|
|
344
356
|
}
|
|
345
357
|
|
|
358
|
+
const EVENT_LOG_LEVELS = {
|
|
359
|
+
"agent:stall": "warning",
|
|
360
|
+
"agent:heartbeat": "info"
|
|
361
|
+
};
|
|
362
|
+
|
|
346
363
|
function buildDirectEmitter(server, runLog, extra) {
|
|
347
364
|
const emitter = new EventEmitter();
|
|
348
365
|
emitter.on("progress", (event) => {
|
|
349
366
|
try {
|
|
350
|
-
|
|
351
|
-
if (event.type === "agent:stall") level = "warning";
|
|
352
|
-
else if (event.type === "agent:heartbeat") level = "info";
|
|
367
|
+
const level = EVENT_LOG_LEVELS[event.type] || "debug";
|
|
353
368
|
server.sendLoggingMessage({ level, logger: "karajan", data: event });
|
|
354
369
|
} catch { /* best-effort */ }
|
|
355
370
|
if (runLog) runLog.logEvent(event);
|
|
@@ -863,6 +878,20 @@ async function handleAudit(a, server, extra) {
|
|
|
863
878
|
return handleAuditDirect(a, server, extra);
|
|
864
879
|
}
|
|
865
880
|
|
|
881
|
+
async function handleBoard(a) {
|
|
882
|
+
const action = a.action || "status";
|
|
883
|
+
const { loadConfig: lc } = await import("../config.js");
|
|
884
|
+
const { config } = await lc();
|
|
885
|
+
const port = a.port || config.hu_board?.port || 4000;
|
|
886
|
+
const { startBoard, stopBoard, boardStatus } = await import("../commands/board.js");
|
|
887
|
+
switch (action) {
|
|
888
|
+
case "start": return startBoard(port);
|
|
889
|
+
case "stop": return stopBoard();
|
|
890
|
+
case "status": return boardStatus(port);
|
|
891
|
+
default: return failPayload(`Unknown board action: ${action}`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
866
895
|
/* ── Handler dispatch map ─────────────────────────────────────────── */
|
|
867
896
|
|
|
868
897
|
const toolHandlers = {
|
|
@@ -884,7 +913,8 @@ const toolHandlers = {
|
|
|
884
913
|
kj_triage: (a, server, extra) => handleTriage(a, server, extra),
|
|
885
914
|
kj_researcher: (a, server, extra) => handleResearcher(a, server, extra),
|
|
886
915
|
kj_architect: (a, server, extra) => handleArchitect(a, server, extra),
|
|
887
|
-
kj_audit: (a, server, extra) => handleAudit(a, server, extra)
|
|
916
|
+
kj_audit: (a, server, extra) => handleAudit(a, server, extra),
|
|
917
|
+
kj_board: (a) => handleBoard(a)
|
|
888
918
|
};
|
|
889
919
|
|
|
890
920
|
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;
|
|
@@ -474,7 +476,7 @@ async function checkBudgetExceeded({ budgetTracker, config, session, emitter, ev
|
|
|
474
476
|
}
|
|
475
477
|
|
|
476
478
|
async function handleStandbyResult({ stageResult, session, emitter, eventBase, i, stage, logger }) {
|
|
477
|
-
if (
|
|
479
|
+
if (stageResult?.action !== "standby") {
|
|
478
480
|
return { handled: false };
|
|
479
481
|
}
|
|
480
482
|
|
|
@@ -509,6 +511,47 @@ async function handleStandbyResult({ stageResult, session, emitter, eventBase, i
|
|
|
509
511
|
return { handled: true, action: "retry" };
|
|
510
512
|
}
|
|
511
513
|
|
|
514
|
+
function formatCommitList(commits) {
|
|
515
|
+
return commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function becariaIncrementalPush({ config, session, gitCtx, task, logger, repo, dispatchComment }) {
|
|
519
|
+
const pushResult = await incrementalPush({ gitCtx, task, logger, session });
|
|
520
|
+
if (!pushResult) return;
|
|
521
|
+
|
|
522
|
+
session.becaria_commits = [...(session.becaria_commits ?? []), ...pushResult.commits];
|
|
523
|
+
await saveSession(session);
|
|
524
|
+
|
|
525
|
+
if (!repo) return;
|
|
526
|
+
const feedback = session.last_reviewer_feedback || "N/A";
|
|
527
|
+
await dispatchComment({
|
|
528
|
+
repo, prNumber: session.becaria_pr_number, agent: "Coder",
|
|
529
|
+
body: `Issues corregidos:\n${feedback}\n\nCommits:\n${formatCommitList(pushResult.commits)}`,
|
|
530
|
+
becariaConfig: config.becaria
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function becariaCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i, repo, dispatchComment }) {
|
|
535
|
+
const earlyPr = await earlyPrCreation({ gitCtx, task, logger, session, stageResults });
|
|
536
|
+
if (!earlyPr) return;
|
|
537
|
+
|
|
538
|
+
session.becaria_pr_number = earlyPr.prNumber;
|
|
539
|
+
session.becaria_pr_url = earlyPr.prUrl;
|
|
540
|
+
session.becaria_commits = earlyPr.commits;
|
|
541
|
+
await saveSession(session);
|
|
542
|
+
emitProgress(emitter, makeEvent("becaria:pr-created", { ...eventBase, stage: "becaria" }, {
|
|
543
|
+
message: `Early PR created: #${earlyPr.prNumber}`,
|
|
544
|
+
detail: { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl }
|
|
545
|
+
}));
|
|
546
|
+
|
|
547
|
+
if (!repo) return;
|
|
548
|
+
await dispatchComment({
|
|
549
|
+
repo, prNumber: earlyPr.prNumber, agent: "Coder",
|
|
550
|
+
body: `Iteración ${i} completada.\n\nCommits:\n${formatCommitList(earlyPr.commits)}`,
|
|
551
|
+
becariaConfig: config.becaria
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
512
555
|
async function handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i }) {
|
|
513
556
|
if (!becariaEnabled) return;
|
|
514
557
|
|
|
@@ -518,48 +561,26 @@ async function handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emi
|
|
|
518
561
|
const repo = await detectRepo();
|
|
519
562
|
|
|
520
563
|
if (session.becaria_pr_number) {
|
|
521
|
-
|
|
522
|
-
if (pushResult) {
|
|
523
|
-
session.becaria_commits = [...(session.becaria_commits ?? []), ...pushResult.commits];
|
|
524
|
-
await saveSession(session);
|
|
525
|
-
|
|
526
|
-
if (repo) {
|
|
527
|
-
const feedback = session.last_reviewer_feedback || "N/A";
|
|
528
|
-
const commitList = pushResult.commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
529
|
-
await dispatchComment({
|
|
530
|
-
repo, prNumber: session.becaria_pr_number, agent: "Coder",
|
|
531
|
-
body: `Issues corregidos:\n${feedback}\n\nCommits:\n${commitList}`,
|
|
532
|
-
becariaConfig: config.becaria
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
}
|
|
564
|
+
await becariaIncrementalPush({ config, session, gitCtx, task, logger, repo, dispatchComment });
|
|
536
565
|
} else {
|
|
537
|
-
|
|
538
|
-
if (earlyPr) {
|
|
539
|
-
session.becaria_pr_number = earlyPr.prNumber;
|
|
540
|
-
session.becaria_pr_url = earlyPr.prUrl;
|
|
541
|
-
session.becaria_commits = earlyPr.commits;
|
|
542
|
-
await saveSession(session);
|
|
543
|
-
emitProgress(emitter, makeEvent("becaria:pr-created", { ...eventBase, stage: "becaria" }, {
|
|
544
|
-
message: `Early PR created: #${earlyPr.prNumber}`,
|
|
545
|
-
detail: { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl }
|
|
546
|
-
}));
|
|
547
|
-
|
|
548
|
-
if (repo) {
|
|
549
|
-
const commitList = earlyPr.commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
550
|
-
await dispatchComment({
|
|
551
|
-
repo, prNumber: earlyPr.prNumber, agent: "Coder",
|
|
552
|
-
body: `Iteración ${i} completada.\n\nCommits:\n${commitList}`,
|
|
553
|
-
becariaConfig: config.becaria
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
}
|
|
566
|
+
await becariaCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i, repo, dispatchComment });
|
|
557
567
|
}
|
|
558
568
|
} catch (err) {
|
|
559
569
|
logger.warn(`BecarIA early PR/push failed (non-blocking): ${err.message}`);
|
|
560
570
|
}
|
|
561
571
|
}
|
|
562
572
|
|
|
573
|
+
function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
|
|
574
|
+
for (const alert of alerts) {
|
|
575
|
+
emitProgress(emitter, makeEvent("solomon:alert", { ...eventBase, stage: "solomon" }, {
|
|
576
|
+
status: alert.severity === "critical" ? "fail" : "warn",
|
|
577
|
+
message: alert.message,
|
|
578
|
+
detail: alert.detail
|
|
579
|
+
}));
|
|
580
|
+
logger.warn(`Solomon alert [${alert.rule}]: ${alert.message}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
563
584
|
async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, becariaEnabled, blockingIssues }) {
|
|
564
585
|
if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
|
|
565
586
|
|
|
@@ -569,15 +590,7 @@ async function handleSolomonCheck({ config, session, emitter, eventBase, logger,
|
|
|
569
590
|
const rulesResult = evaluateRules(rulesContext, config.solomon?.rules);
|
|
570
591
|
|
|
571
592
|
if (rulesResult.alerts.length > 0) {
|
|
572
|
-
|
|
573
|
-
emitProgress(emitter, makeEvent("solomon:alert", { ...eventBase, stage: "solomon" }, {
|
|
574
|
-
status: alert.severity === "critical" ? "fail" : "warn",
|
|
575
|
-
message: alert.message,
|
|
576
|
-
detail: alert.detail
|
|
577
|
-
}));
|
|
578
|
-
logger.warn(`Solomon alert [${alert.rule}]: ${alert.message}`);
|
|
579
|
-
}
|
|
580
|
-
|
|
593
|
+
emitSolomonAlerts(rulesResult.alerts, emitter, eventBase, logger);
|
|
581
594
|
const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
|
|
582
595
|
if (pauseResult) return pauseResult;
|
|
583
596
|
}
|
|
@@ -621,6 +634,27 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
|
|
|
621
634
|
return null;
|
|
622
635
|
}
|
|
623
636
|
|
|
637
|
+
function formatBlockingIssues(issues) {
|
|
638
|
+
return issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function formatSuggestions(suggestions) {
|
|
642
|
+
return suggestions?.map((s) => {
|
|
643
|
+
const detail = typeof s === "string" ? s : `${s.id || ""} ${s.description || s}`;
|
|
644
|
+
return `- ${detail}`;
|
|
645
|
+
}).join("\n") || "";
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function buildReviewCommentBody(review, i) {
|
|
649
|
+
const status = review.approved ? "APPROVED" : "REQUEST_CHANGES";
|
|
650
|
+
const blocking = formatBlockingIssues(review.blocking_issues);
|
|
651
|
+
const suggestions = formatSuggestions(review.non_blocking_suggestions);
|
|
652
|
+
let body = `Review iteración ${i}: ${status}`;
|
|
653
|
+
if (blocking) body += `\n\n**Blocking:**\n${blocking}`;
|
|
654
|
+
if (suggestions) body += `\n\n**Suggestions:**\n${suggestions}`;
|
|
655
|
+
return body;
|
|
656
|
+
}
|
|
657
|
+
|
|
624
658
|
async function handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger }) {
|
|
625
659
|
if (!becariaEnabled || !session.becaria_pr_number) return;
|
|
626
660
|
|
|
@@ -631,33 +665,19 @@ async function handleBecariaReviewDispatch({ becariaEnabled, config, session, re
|
|
|
631
665
|
if (!repo) return;
|
|
632
666
|
|
|
633
667
|
const bc = config.becaria;
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
event: "REQUEST_CHANGES",
|
|
644
|
-
body: blocking || review.summary || "Changes requested",
|
|
645
|
-
agent: "Reviewer", becariaConfig: bc
|
|
646
|
-
});
|
|
647
|
-
}
|
|
668
|
+
const reviewEvent = review.approved ? "APPROVE" : "REQUEST_CHANGES";
|
|
669
|
+
const reviewBody = review.approved
|
|
670
|
+
? (review.summary || "Approved")
|
|
671
|
+
: (formatBlockingIssues(review.blocking_issues) || review.summary || "Changes requested");
|
|
672
|
+
|
|
673
|
+
await dispatchReview({
|
|
674
|
+
repo, prNumber: session.becaria_pr_number,
|
|
675
|
+
event: reviewEvent, body: reviewBody, agent: "Reviewer", becariaConfig: bc
|
|
676
|
+
});
|
|
648
677
|
|
|
649
|
-
const status = review.approved ? "APPROVED" : "REQUEST_CHANGES";
|
|
650
|
-
const blocking = review.blocking_issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
|
|
651
|
-
const suggestions = review.non_blocking_suggestions?.map((s) => {
|
|
652
|
-
const detail = typeof s === "string" ? s : `${s.id || ""} ${s.description || s}`;
|
|
653
|
-
return `- ${detail}`;
|
|
654
|
-
}).join("\n") || "";
|
|
655
|
-
let reviewBody = `Review iteración ${i}: ${status}`;
|
|
656
|
-
if (blocking) reviewBody += `\n\n**Blocking:**\n${blocking}`;
|
|
657
|
-
if (suggestions) reviewBody += `\n\n**Suggestions:**\n${suggestions}`;
|
|
658
678
|
await dispatchComment({
|
|
659
679
|
repo, prNumber: session.becaria_pr_number, agent: "Reviewer",
|
|
660
|
-
body:
|
|
680
|
+
body: buildReviewCommentBody(review, i), becariaConfig: bc
|
|
661
681
|
});
|
|
662
682
|
|
|
663
683
|
logger.info(`BecarIA: dispatched review for PR #${session.becaria_pr_number}`);
|
|
@@ -1093,6 +1113,24 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
|
|
|
1093
1113
|
return { approved: false, sessionId: session.id, reason: "max_iterations" };
|
|
1094
1114
|
}
|
|
1095
1115
|
|
|
1116
|
+
async function tryAutoStartBoard(config, logger, emitter, eventBase) {
|
|
1117
|
+
if (!config.hu_board?.enabled || !config.hu_board?.auto_start) return;
|
|
1118
|
+
|
|
1119
|
+
try {
|
|
1120
|
+
const { startBoard } = await import("./commands/board.js");
|
|
1121
|
+
const boardPort = config.hu_board.port || 4000;
|
|
1122
|
+
const boardResult = await startBoard(boardPort);
|
|
1123
|
+
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
1124
|
+
logger.info(`HU Board ${status} at ${boardResult.url}`);
|
|
1125
|
+
emitProgress(emitter, makeEvent("board:started", eventBase, {
|
|
1126
|
+
message: `HU Board running at ${boardResult.url}`,
|
|
1127
|
+
detail: { pid: boardResult.pid, port: boardPort }
|
|
1128
|
+
}));
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1096
1134
|
async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
|
|
1097
1135
|
const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
|
|
1098
1136
|
ctx.askQuestion = askQuestion;
|
|
@@ -1124,6 +1162,9 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1124
1162
|
}));
|
|
1125
1163
|
}
|
|
1126
1164
|
|
|
1165
|
+
// --- HU Board auto-start ---
|
|
1166
|
+
await tryAutoStartBoard(config, logger, emitter, ctx.eventBase);
|
|
1167
|
+
|
|
1127
1168
|
// --- Product Context ---
|
|
1128
1169
|
const ctxProjectDir = config.projectDir || process.cwd();
|
|
1129
1170
|
const { content: productContext, source: productContextSource } = await loadProductContext(ctxProjectDir);
|
package/src/utils/display.js
CHANGED
|
@@ -219,9 +219,14 @@ function printSessionGit(git) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
function isBudgetUnavailable(budget) {
|
|
223
|
+
return budget.usage_available === false ||
|
|
224
|
+
(budget.total_tokens === 0 && budget.total_cost_usd === 0 && Object.keys(budget.breakdown_by_role || {}).length > 0);
|
|
225
|
+
}
|
|
226
|
+
|
|
222
227
|
function printSessionBudget(budget) {
|
|
223
228
|
if (!budget) return;
|
|
224
|
-
if (
|
|
229
|
+
if (isBudgetUnavailable(budget)) {
|
|
225
230
|
console.log(` ${ANSI.dim}\ud83d\udcb0 Budget: N/A (provider does not report usage)${ANSI.reset}`);
|
|
226
231
|
return;
|
|
227
232
|
}
|
|
@@ -381,12 +386,13 @@ const EVENT_HANDLERS = {
|
|
|
381
386
|
},
|
|
382
387
|
|
|
383
388
|
"budget:update": (event, icon) => {
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
const
|
|
389
|
+
const d = event.detail || {};
|
|
390
|
+
const total = Number(d.total_cost_usd || 0);
|
|
391
|
+
const totalTokens = Number(d.total_tokens || 0);
|
|
392
|
+
const max = Number(d.max_budget_usd);
|
|
393
|
+
const pct = Number(d.pct_used ?? 0);
|
|
394
|
+
const warn = Number(d.warn_threshold_pct ?? 80);
|
|
395
|
+
const hasEntries = (d.entries?.length ?? 0) > 0 || Object.keys(d.breakdown_by_role || {}).length > 0;
|
|
390
396
|
if (hasEntries && totalTokens === 0 && total === 0) {
|
|
391
397
|
console.log(` \u251c\u2500 ${icon} Budget: ${ANSI.dim}N/A (provider does not report usage)${ANSI.reset}`);
|
|
392
398
|
return;
|
|
@@ -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
|