karajan-code 1.48.0 → 1.50.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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/agents/resolve-bin.js +1 -1
  3. package/src/audit/basal-cost.js +26 -23
  4. package/src/bootstrap.js +2 -2
  5. package/src/commands/agents.js +1 -1
  6. package/src/commands/board.js +3 -3
  7. package/src/commands/code.js +1 -1
  8. package/src/commands/doctor.js +4 -4
  9. package/src/commands/init.js +2 -2
  10. package/src/commands/report.js +1 -1
  11. package/src/commands/review.js +1 -1
  12. package/src/commands/roles.js +1 -1
  13. package/src/hu/store.js +9 -5
  14. package/src/mcp/handlers/direct-handlers.js +393 -0
  15. package/src/mcp/handlers/hu-handlers.js +93 -0
  16. package/src/mcp/handlers/management-handlers.js +182 -0
  17. package/src/mcp/handlers/run-handler.js +283 -0
  18. package/src/mcp/orphan-guard.js +1 -1
  19. package/src/mcp/server-handlers.js +41 -871
  20. package/src/mcp/suggest-handler.js +2 -2
  21. package/src/orchestrator/hu-sub-pipeline.js +1 -1
  22. package/src/orchestrator/iteration-stages.js +9 -890
  23. package/src/orchestrator/pre-loop-stages.js +8 -906
  24. package/src/orchestrator/preflight-checks.js +13 -19
  25. package/src/orchestrator/stages/architect-stage.js +176 -0
  26. package/src/orchestrator/stages/coder-stage.js +307 -0
  27. package/src/orchestrator/stages/hu-reviewer-stage.js +467 -0
  28. package/src/orchestrator/stages/planner-stage.js +90 -0
  29. package/src/orchestrator/stages/research-stage.js +74 -0
  30. package/src/orchestrator/stages/reviewer-stage.js +350 -0
  31. package/src/orchestrator/stages/sonar-stage.js +267 -0
  32. package/src/orchestrator/stages/triage-stage.js +137 -0
  33. package/src/plugins/loader.js +1 -1
  34. package/src/prompts/architect.js +1 -1
  35. package/src/review/parser.js +3 -3
  36. package/src/roles/architect-role.js +1 -1
  37. package/src/roles/audit-role.js +3 -3
  38. package/src/roles/base-role.js +1 -1
  39. package/src/roles/discover-role.js +1 -1
  40. package/src/roles/hu-reviewer-role.js +1 -1
  41. package/src/roles/researcher-role.js +1 -1
  42. package/src/roles/triage-role.js +1 -1
  43. package/src/session-cleanup.js +1 -1
  44. package/src/session-store.js +2 -2
  45. package/src/skills/openskills-client.js +1 -1
  46. package/src/skills/skill-detector.js +2 -2
  47. package/src/sonar/api.js +4 -3
  48. package/src/sonar/config-resolver.js +69 -0
  49. package/src/sonar/credentials.js +1 -1
  50. package/src/sonar/project-key.js +1 -1
  51. package/src/sonar/scanner.js +13 -21
  52. package/src/utils/fs.js +1 -1
  53. package/src/utils/json-extract.js +1 -1
  54. package/src/utils/project-detect.js +1 -1
  55. package/src/utils/rtk-detect.js +1 -1
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Direct role handler logic (code, review, plan, audit, discover, triage, researcher, architect, impeccable).
3
+ * Extracted from server-handlers.js for maintainability.
4
+ */
5
+
6
+ import { EventEmitter } from "node:events";
7
+ import fs from "node:fs/promises";
8
+ import { resolveRole } from "../../config.js";
9
+ import { createLogger } from "../../utils/logger.js";
10
+ import { assertAgentsAvailable } from "../../agents/availability.js";
11
+ import { createAgent } from "../../agents/index.js";
12
+ import { buildPlannerPrompt } from "../../prompts/planner.js";
13
+ import { buildCoderPrompt } from "../../prompts/coder.js";
14
+ import { buildReviewerPrompt } from "../../prompts/reviewer.js";
15
+ import { parseMaybeJsonString } from "../../review/parser.js";
16
+ import { computeBaseRef, generateDiff } from "../../review/diff-generator.js";
17
+ import { resolveReviewProfile } from "../../review/profiles.js";
18
+ import { createRunLog } from "../../utils/run-log.js";
19
+ import { createStallDetector } from "../../utils/stall-detector.js";
20
+ import { sendTrackerLog } from "../progress.js";
21
+ import { runDirectRole } from "../direct-role-runner.js";
22
+ import { isPreflightAcked, ackPreflight, getSessionOverrides } from "../preflight.js";
23
+ import { ensureBootstrap } from "../../bootstrap.js";
24
+ import { loadConfig } from "../../config.js";
25
+ import {
26
+ resolveProjectDir,
27
+ failPayload,
28
+ assertNotOnBaseBranch,
29
+ buildConfig,
30
+ buildDirectEmitter,
31
+ } from "../server-handlers.js";
32
+
33
+ /**
34
+ * Run bootstrap gate: validate all environment prerequisites before execution.
35
+ */
36
+ async function runBootstrapGate(server, a) {
37
+ const projectDir = await resolveProjectDir(server, a.projectDir);
38
+ const { config } = await loadConfig(projectDir);
39
+ await ensureBootstrap(projectDir, config);
40
+ }
41
+
42
+ function applySessionOverrides(a, roleKeys) {
43
+ const sessionOvr = getSessionOverrides();
44
+ for (const key of roleKeys) {
45
+ if (sessionOvr[key] !== undefined) { a[key] = sessionOvr[key]; }
46
+ }
47
+ }
48
+
49
+ export async function handlePlanDirect(a, server, extra) {
50
+ const { normalizePlanArgs } = await import("../tool-arg-normalizers.js");
51
+ const options = normalizePlanArgs(a);
52
+ const config = await buildConfig(options, "plan");
53
+ const logger = createLogger(config.output.log_level, "mcp");
54
+
55
+ const plannerRole = resolveRole(config, "planner");
56
+ await assertAgentsAvailable([plannerRole.provider]);
57
+
58
+ const projectDir = await resolveProjectDir(server, a.projectDir);
59
+ const runLog = createRunLog(projectDir);
60
+ const silenceTimeoutMs = Number(config?.session?.max_agent_silence_minutes) > 0
61
+ ? Math.round(Number(config.session.max_agent_silence_minutes) * 60 * 1000)
62
+ : undefined;
63
+ const plannerTimeoutMs = Number(config?.session?.max_planner_minutes) > 0
64
+ ? Math.round(Number(config.session.max_planner_minutes) * 60 * 1000)
65
+ : undefined;
66
+ const silenceLabel = silenceTimeoutMs ? `${Math.round(silenceTimeoutMs / 1000)}s` : "disabled";
67
+ const runtimeLabel = plannerTimeoutMs ? `${Math.round(plannerTimeoutMs / 1000)}s` : "disabled";
68
+ runLog.logText(
69
+ `[kj_plan] started — provider=${plannerRole.provider}, max_silence=${silenceLabel}, max_runtime=${runtimeLabel}`
70
+ );
71
+ const emitter = buildDirectEmitter(server, runLog, extra);
72
+ const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
73
+ const onOutput = ({ stream, line }) => {
74
+ emitter.emit("progress", { type: "agent:output", stage: "planner", message: line, detail: { stream, agent: plannerRole.provider } });
75
+ };
76
+ const stallDetector = createStallDetector({
77
+ onOutput, emitter, eventBase, stage: "planner", provider: plannerRole.provider
78
+ });
79
+
80
+ const planner = createAgent(plannerRole.provider, config, logger);
81
+ const prompt = buildPlannerPrompt({ task: a.task, context: a.context });
82
+ sendTrackerLog(server, "planner", "running", plannerRole.provider);
83
+ runLog.logText(`[planner] agent launched, waiting for response...`);
84
+ let result;
85
+ let plannerStats = null;
86
+ try {
87
+ result = await planner.runTask({
88
+ prompt,
89
+ role: "planner",
90
+ onOutput: stallDetector.onOutput,
91
+ silenceTimeoutMs,
92
+ timeoutMs: plannerTimeoutMs
93
+ });
94
+ } finally {
95
+ stallDetector.stop();
96
+ plannerStats = stallDetector.stats();
97
+ runLog.logText(
98
+ `[planner] finished — lines=${plannerStats.lineCount}, bytes=${plannerStats.bytesReceived}, elapsed=${Math.round(plannerStats.elapsedMs / 1000)}s`
99
+ );
100
+ runLog.close();
101
+ }
102
+
103
+ if (!result.ok) {
104
+ sendTrackerLog(server, "planner", "failed");
105
+ const baseError = result.error || result.output || "Planner failed";
106
+ const statsSuffix = plannerStats
107
+ ? ` [lines=${plannerStats.lineCount}, bytes=${plannerStats.bytesReceived}, elapsed=${Math.round(plannerStats.elapsedMs / 1000)}s]`
108
+ : "";
109
+ throw new Error(`${baseError}${statsSuffix}`);
110
+ }
111
+
112
+ sendTrackerLog(server, "planner", "done");
113
+ const parsed = parseMaybeJsonString(result.output);
114
+ return { ok: true, plan: parsed || result.output, raw: result.output };
115
+ }
116
+
117
+ export async function handleCodeDirect(a, server, extra) {
118
+ const config = await buildConfig(a, "code");
119
+ await assertNotOnBaseBranch(config);
120
+ const logger = createLogger(config.output.log_level, "mcp");
121
+
122
+ const coderRole = resolveRole(config, "coder");
123
+ await assertAgentsAvailable([coderRole.provider]);
124
+
125
+ const projectDir = await resolveProjectDir(server, a.projectDir);
126
+ const runLog = createRunLog(projectDir);
127
+ runLog.logText(`[kj_code] started — provider=${coderRole.provider}`);
128
+ const emitter = buildDirectEmitter(server, runLog, extra);
129
+ const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
130
+ const onOutput = ({ stream, line }) => {
131
+ emitter.emit("progress", { type: "agent:output", stage: "coder", message: line, detail: { stream, agent: coderRole.provider } });
132
+ };
133
+ const stallDetector = createStallDetector({
134
+ onOutput, emitter, eventBase, stage: "coder", provider: coderRole.provider
135
+ });
136
+
137
+ const coder = createAgent(coderRole.provider, config, logger);
138
+ let coderRules = null;
139
+ if (config.coder_rules) {
140
+ try {
141
+ coderRules = await fs.readFile(config.coder_rules, "utf8");
142
+ } catch { /* configured coder_rules path not found */
143
+ try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
144
+ }
145
+ }
146
+ const prompt = await buildCoderPrompt({ task: a.task, coderRules, methodology: config.development?.methodology || "tdd" });
147
+ sendTrackerLog(server, "coder", "running", coderRole.provider);
148
+ runLog.logText(`[coder] agent launched, waiting for response...`);
149
+ let result;
150
+ try {
151
+ result = await coder.runTask({ prompt, role: "coder", onOutput: stallDetector.onOutput });
152
+ } finally {
153
+ stallDetector.stop();
154
+ const stats = stallDetector.stats();
155
+ runLog.logText(`[coder] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
156
+ runLog.close();
157
+ }
158
+
159
+ if (!result.ok) {
160
+ sendTrackerLog(server, "coder", "failed");
161
+ throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
162
+ }
163
+
164
+ sendTrackerLog(server, "coder", "done");
165
+ return { ok: true, output: result.output, exitCode: result.exitCode };
166
+ }
167
+
168
+ export async function handleReviewDirect(a, server, extra) {
169
+ const config = await buildConfig(a, "review");
170
+ await assertNotOnBaseBranch(config);
171
+ const logger = createLogger(config.output.log_level, "mcp");
172
+
173
+ const reviewerRole = resolveRole(config, "reviewer");
174
+ await assertAgentsAvailable([reviewerRole.provider, config.reviewer_options?.fallback_reviewer]);
175
+
176
+ const projectDir = await resolveProjectDir(server, a.projectDir);
177
+ const runLog = createRunLog(projectDir);
178
+ runLog.logText(`[kj_review] started — provider=${reviewerRole.provider}`);
179
+ const emitter = buildDirectEmitter(server, runLog, extra);
180
+ const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
181
+ const onOutput = ({ stream, line }) => {
182
+ emitter.emit("progress", { type: "agent:output", stage: "reviewer", message: line, detail: { stream, agent: reviewerRole.provider } });
183
+ };
184
+ const stallDetector = createStallDetector({
185
+ onOutput, emitter, eventBase, stage: "reviewer", provider: reviewerRole.provider
186
+ });
187
+
188
+ const reviewer = createAgent(reviewerRole.provider, config, logger);
189
+ const resolvedBase = await computeBaseRef({ baseBranch: config.base_branch, baseRef: a.baseRef });
190
+ const diff = await generateDiff({ baseRef: resolvedBase });
191
+ const { rules } = await resolveReviewProfile({ mode: config.review_mode, projectDir: process.cwd() });
192
+
193
+ const prompt = await buildReviewerPrompt({ task: a.task, diff, reviewRules: rules, mode: config.review_mode });
194
+ sendTrackerLog(server, "reviewer", "running", reviewerRole.provider);
195
+ runLog.logText(`[reviewer] agent launched, waiting for response...`);
196
+ let result;
197
+ try {
198
+ result = await reviewer.reviewTask({ prompt, role: "reviewer", onOutput: stallDetector.onOutput });
199
+ } finally {
200
+ stallDetector.stop();
201
+ const stats = stallDetector.stats();
202
+ runLog.logText(`[reviewer] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
203
+ runLog.close();
204
+ }
205
+
206
+ if (!result.ok) {
207
+ sendTrackerLog(server, "reviewer", "failed");
208
+ throw new Error(result.error || result.output || `Reviewer failed (exit ${result.exitCode})`);
209
+ }
210
+
211
+ sendTrackerLog(server, "reviewer", "done");
212
+ const parsed = parseMaybeJsonString(result.output);
213
+ return { ok: true, review: parsed || result.output, raw: result.output };
214
+ }
215
+
216
+ export async function handleDiscoverDirect(a, server, extra) {
217
+ // Build context from pgTask if provided
218
+ let context = a.context || null;
219
+ if (a.pgTask && a.pgProject) {
220
+ try {
221
+ const pgContext = `Planning Game card: ${a.pgTask} (project: ${a.pgProject})`;
222
+ context = context ? `${context}\n\n${pgContext}` : pgContext;
223
+ } catch { /* PG not available — proceed without */ }
224
+ }
225
+
226
+ return runDirectRole({
227
+ roleName: "discover",
228
+ importRole: async () => {
229
+ const { DiscoverRole } = await import("../../roles/discover-role.js");
230
+ return { RoleClass: DiscoverRole };
231
+ },
232
+ initContext: { task: a.task },
233
+ runInput: { task: a.task, mode: a.mode || "gaps", context },
234
+ logStartMsg: `[kj_discover] started — mode=${a.mode || "gaps"}`,
235
+ args: a, server, extra,
236
+ resolveProjectDir, buildConfig, buildDirectEmitter
237
+ });
238
+ }
239
+
240
+ export async function handleTriageDirect(a, server, extra) {
241
+ return runDirectRole({
242
+ roleName: "triage",
243
+ importRole: async () => {
244
+ const { TriageRole } = await import("../../roles/triage-role.js");
245
+ return { RoleClass: TriageRole };
246
+ },
247
+ initContext: { task: a.task },
248
+ runInput: { task: a.task },
249
+ args: a, server, extra,
250
+ resolveProjectDir, buildConfig, buildDirectEmitter
251
+ });
252
+ }
253
+
254
+ export async function handleResearcherDirect(a, server, extra) {
255
+ return runDirectRole({
256
+ roleName: "researcher",
257
+ importRole: async () => {
258
+ const { ResearcherRole } = await import("../../roles/researcher-role.js");
259
+ return { RoleClass: ResearcherRole };
260
+ },
261
+ initContext: { task: a.task },
262
+ runInput: { task: a.task },
263
+ args: a, server, extra,
264
+ resolveProjectDir, buildConfig, buildDirectEmitter
265
+ });
266
+ }
267
+
268
+ export async function handleAuditDirect(a, server, extra) {
269
+ const task = a.task || "Analyze the full codebase";
270
+ return runDirectRole({
271
+ roleName: "audit",
272
+ importRole: async () => {
273
+ const { AuditRole } = await import("../../roles/audit-role.js");
274
+ return { RoleClass: AuditRole };
275
+ },
276
+ initContext: { task },
277
+ runInput: { task, dimensions: a.dimensions || null },
278
+ logStartMsg: `[kj_audit] started — dimensions=${a.dimensions || "all"}`,
279
+ args: a, server, extra,
280
+ resolveProjectDir, buildConfig, buildDirectEmitter
281
+ });
282
+ }
283
+
284
+ export async function handleArchitectDirect(a, server, extra) {
285
+ return runDirectRole({
286
+ roleName: "architect",
287
+ importRole: async () => {
288
+ const { ArchitectRole } = await import("../../roles/architect-role.js");
289
+ return { RoleClass: ArchitectRole };
290
+ },
291
+ initContext: { task: a.task },
292
+ runInput: { task: a.task, researchContext: a.context || null },
293
+ args: a, server, extra,
294
+ resolveProjectDir, buildConfig, buildDirectEmitter
295
+ });
296
+ }
297
+
298
+ export async function handleCode(a, server, extra) {
299
+ if (!a.task) {
300
+ return failPayload("Missing required field: task");
301
+ }
302
+ await runBootstrapGate(server, a);
303
+ if (!isPreflightAcked()) {
304
+ // Auto-acknowledge with defaults for autonomous operation
305
+ ackPreflight({});
306
+ const logger = createLogger("info", "mcp");
307
+ logger.info("Preflight auto-acknowledged with default agent config");
308
+ }
309
+ applySessionOverrides(a, ["coder"]);
310
+ return handleCodeDirect(a, server, extra);
311
+ }
312
+
313
+ export async function handleReview(a, server, extra) {
314
+ if (!a.task) {
315
+ return failPayload("Missing required field: task");
316
+ }
317
+ await runBootstrapGate(server, a);
318
+ return handleReviewDirect(a, server, extra);
319
+ }
320
+
321
+ export async function handlePlan(a, server, extra) {
322
+ if (!a.task) {
323
+ return failPayload("Missing required field: task");
324
+ }
325
+ await runBootstrapGate(server, a);
326
+ return handlePlanDirect(a, server, extra);
327
+ }
328
+
329
+ export async function handleDiscover(a, server, extra) {
330
+ if (!a.task) {
331
+ return failPayload("Missing required field: task");
332
+ }
333
+ const validModes = new Set(["gaps", "momtest", "wendel", "classify", "jtbd"]);
334
+ if (a.mode && !validModes.has(a.mode)) {
335
+ return failPayload(`Invalid mode "${a.mode}". Valid values: ${[...validModes].join(", ")}`);
336
+ }
337
+ await runBootstrapGate(server, a);
338
+ return handleDiscoverDirect(a, server, extra);
339
+ }
340
+
341
+ export async function handleTriage(a, server, extra) {
342
+ if (!a.task) {
343
+ return failPayload("Missing required field: task");
344
+ }
345
+ await runBootstrapGate(server, a);
346
+ return handleTriageDirect(a, server, extra);
347
+ }
348
+
349
+ export async function handleResearcher(a, server, extra) {
350
+ if (!a.task) {
351
+ return failPayload("Missing required field: task");
352
+ }
353
+ await runBootstrapGate(server, a);
354
+ return handleResearcherDirect(a, server, extra);
355
+ }
356
+
357
+ export async function handleArchitect(a, server, extra) {
358
+ if (!a.task) {
359
+ return failPayload("Missing required field: task");
360
+ }
361
+ await runBootstrapGate(server, a);
362
+ return handleArchitectDirect(a, server, extra);
363
+ }
364
+
365
+ export async function handleAudit(a, server, extra) {
366
+ await runBootstrapGate(server, a);
367
+ const result = await handleAuditDirect(a, server, extra);
368
+
369
+ // Return compact summary for MCP (full details in session log)
370
+ if (result?.ok && result?.summary) {
371
+ const compact = {
372
+ ok: true,
373
+ overallHealth: result.summary?.overallHealth || result.summary,
374
+ totalFindings: result.summary?.totalFindings,
375
+ critical: result.summary?.critical,
376
+ high: result.summary?.high,
377
+ topRecommendations: (result.topRecommendations || []).slice(0, 5).map(r => ({
378
+ priority: r.priority,
379
+ dimension: r.dimension,
380
+ action: r.action,
381
+ impact: r.impact
382
+ })),
383
+ textSummary: result.textSummary || result.summary,
384
+ basalCost: result.basalCost ? {
385
+ totalLines: result.basalCost.totalLines,
386
+ totalFiles: result.basalCost.totalFiles,
387
+ dependencies: result.basalCost.dependencies
388
+ } : undefined
389
+ };
390
+ return { content: [{ type: "text", text: JSON.stringify(compact, null, 2) }] };
391
+ }
392
+ return result;
393
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * HU, Skills, and Suggest handler logic.
3
+ * Extracted from server-handlers.js for maintainability.
4
+ */
5
+
6
+ import { handleSuggestion } from "../suggest-handler.js";
7
+ import {
8
+ resolveProjectDir,
9
+ failPayload,
10
+ } from "../server-handlers.js";
11
+
12
+ export async function handleHu(a, server) {
13
+ const action = a.action;
14
+ if (!action) return failPayload("Missing required field: action");
15
+
16
+ const projectDir = await resolveProjectDir(server, a.projectDir);
17
+ const { createManualHu, listHus, getHu, updateHuStatus } = await import("../../hu/store.js");
18
+
19
+ switch (action) {
20
+ case "create": {
21
+ if (!a.title) return failPayload("Missing required field: title (required for create)");
22
+ const hu = await createManualHu(projectDir, {
23
+ title: a.title,
24
+ description: a.description,
25
+ status: a.status,
26
+ acceptanceCriteria: a.acceptanceCriteria
27
+ });
28
+ return { ok: true, hu };
29
+ }
30
+ case "list": {
31
+ const hus = await listHus(projectDir);
32
+ return { ok: true, hus };
33
+ }
34
+ case "get": {
35
+ if (!a.huId) return failPayload("Missing required field: huId (required for get)");
36
+ const hu = await getHu(projectDir, a.huId);
37
+ return { ok: true, hu };
38
+ }
39
+ case "update": {
40
+ if (!a.huId) return failPayload("Missing required field: huId (required for update)");
41
+ if (!a.status) return failPayload("Missing required field: status (required for update)");
42
+ const hu = await updateHuStatus(projectDir, a.huId, a.status);
43
+ return { ok: true, hu };
44
+ }
45
+ default:
46
+ return failPayload(`Unknown hu action: ${action}`);
47
+ }
48
+ }
49
+
50
+ export async function handleSkills(a) {
51
+ const action = a.action;
52
+ if (!action) return failPayload("Missing required field: action");
53
+
54
+ const { isOpenSkillsAvailable, installSkill, removeSkill, listSkills, readSkill } =
55
+ await import("../../skills/openskills-client.js");
56
+
57
+ const opts = { projectDir: a.projectDir || null };
58
+
59
+ switch (action) {
60
+ case "install": {
61
+ if (!a.source) return failPayload("Missing required field: source (required for install)");
62
+ const available = await isOpenSkillsAvailable();
63
+ if (!available) {
64
+ return failPayload("OpenSkills CLI is not available. Install it with: npm install -g openskills");
65
+ }
66
+ return installSkill(a.source, { ...opts, global: a.global || false });
67
+ }
68
+ case "remove": {
69
+ if (!a.name) return failPayload("Missing required field: name (required for remove)");
70
+ return removeSkill(a.name, opts);
71
+ }
72
+ case "list": {
73
+ return listSkills(opts);
74
+ }
75
+ case "read": {
76
+ if (!a.name) return failPayload("Missing required field: name (required for read)");
77
+ return readSkill(a.name, opts);
78
+ }
79
+ default:
80
+ return failPayload(`Unknown skills action: ${action}`);
81
+ }
82
+ }
83
+
84
+ export async function handleSuggest(a) {
85
+ if (!a.suggestion) {
86
+ return failPayload("Missing required field: suggestion");
87
+ }
88
+ return handleSuggestion({
89
+ suggestion: a.suggestion,
90
+ context: a.context || null,
91
+ projectDir: a.projectDir || null
92
+ });
93
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Management handler logic (init, doctor, config, scan, report, roles, agents, preflight, status, board).
3
+ * Extracted from server-handlers.js for maintainability.
4
+ */
5
+
6
+ import { runKjCommand } from "../run-kj.js";
7
+ import { loadConfig } from "../../config.js";
8
+ import { readRunLog } from "../../utils/run-log.js";
9
+ import { isPreflightAcked, ackPreflight, getSessionOverrides } from "../preflight.js";
10
+ import { ensureBootstrap } from "../../bootstrap.js";
11
+ import {
12
+ resolveProjectDir,
13
+ failPayload,
14
+ responseText,
15
+ buildConfig,
16
+ } from "../server-handlers.js";
17
+
18
+ const AGENT_ROLES = new Set(["coder", "reviewer", "tester", "security", "solomon"]);
19
+
20
+ /**
21
+ * Run bootstrap gate: validate all environment prerequisites before execution.
22
+ */
23
+ async function runBootstrapGate(server, a) {
24
+ const projectDir = await resolveProjectDir(server, a.projectDir);
25
+ const { config } = await loadConfig(projectDir);
26
+ await ensureBootstrap(projectDir, config);
27
+ }
28
+
29
+ export async function handleStatus(a, server) {
30
+ const maxLines = a.lines || 50;
31
+ const projectDir = await resolveProjectDir(server, a.projectDir);
32
+ return readRunLog(projectDir, maxLines);
33
+ }
34
+
35
+ export async function handleAgents(a) {
36
+ const action = a.action || "list";
37
+ if (action === "set") {
38
+ if (!a.role || !a.provider) {
39
+ return failPayload("Missing required fields: role and provider");
40
+ }
41
+ const { setAgent } = await import("../../commands/agents.js");
42
+ const result = await setAgent(a.role, a.provider, { global: false });
43
+ return { ok: true, ...result, message: `${result.role} now uses ${result.provider} (scope: ${result.scope})` };
44
+ }
45
+ const config = await buildConfig(a);
46
+ const { listAgents } = await import("../../commands/agents.js");
47
+ const sessionOvr = getSessionOverrides();
48
+ return { ok: true, agents: listAgents(config, sessionOvr) };
49
+ }
50
+
51
+ function parseHumanResponseOverrides(humanResponse, overrides) {
52
+ for (const role of AGENT_ROLES) {
53
+ const patterns = [
54
+ new RegExp(String.raw`use\s+(\w+)\s+(?:as|for)\s+${role}`, "i"),
55
+ new RegExp(String.raw`${role}\s*[:=]\s*(\w+)`, "i"),
56
+ new RegExp(String.raw`set\s+${role}\s+(?:to|=)\s*(\w+)`, "i")
57
+ ];
58
+ for (const pat of patterns) {
59
+ const m = pat.exec(humanResponse);
60
+ if (m && !overrides[role]) {
61
+ overrides[role] = m[1];
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ function buildPreflightOverrides(a) {
69
+ const overrides = {};
70
+ for (const role of AGENT_ROLES) {
71
+ if (a[role]) overrides[role] = a[role];
72
+ }
73
+ if (a.enableTester !== undefined) overrides.enableTester = a.enableTester;
74
+ if (a.enableSecurity !== undefined) overrides.enableSecurity = a.enableSecurity;
75
+
76
+ const resp = (a.humanResponse || "").toLowerCase();
77
+ if (resp !== "ok") {
78
+ parseHumanResponseOverrides(a.humanResponse || "", overrides);
79
+ }
80
+ return overrides;
81
+ }
82
+
83
+ function formatPreflightConfig(agents, overrides) {
84
+ const lines = agents
85
+ .filter(ag => ag.provider !== "-")
86
+ .map(ag => {
87
+ const ovr = overrides[ag.role] ? ` -> ${overrides[ag.role]} (session override)` : "";
88
+ const modelSuffix = ag.model === "-" ? "" : ` (${ag.model})`;
89
+ return ` ${ag.role}: ${ag.provider}${modelSuffix}${ovr}`;
90
+ });
91
+ const overrideLines = Object.entries(overrides)
92
+ .filter(([k]) => !AGENT_ROLES.has(k))
93
+ .map(([k, v]) => ` ${k}: ${v}`);
94
+ return [...lines, ...overrideLines].join("\n");
95
+ }
96
+
97
+ export async function handlePreflight(a) {
98
+ const overrides = buildPreflightOverrides(a);
99
+ ackPreflight(overrides);
100
+
101
+ const config = await buildConfig(a);
102
+ const { listAgents } = await import("../../commands/agents.js");
103
+ const agents = listAgents(config);
104
+
105
+ return {
106
+ ok: true,
107
+ message: `Preflight acknowledged. Agent configuration confirmed.`,
108
+ config: formatPreflightConfig(agents, overrides),
109
+ overrides
110
+ };
111
+ }
112
+
113
+ export function handleRoles(a) {
114
+ const action = a.action || "list";
115
+ const commandArgs = [action];
116
+ if (action === "show" && a.roleName) commandArgs.push(a.roleName);
117
+ return runKjCommand({ command: "roles", commandArgs, options: a });
118
+ }
119
+
120
+ function buildReportArgs(a) {
121
+ const commandArgs = [];
122
+ if (a.list) commandArgs.push("--list");
123
+ if (a.sessionId) commandArgs.push("--session-id", String(a.sessionId));
124
+ if (a.format) commandArgs.push("--format", String(a.format));
125
+ if (a.trace) commandArgs.push("--trace");
126
+ if (a.currency) commandArgs.push("--currency", String(a.currency));
127
+ if (a.pgTask) commandArgs.push("--pg-task", String(a.pgTask));
128
+ return commandArgs;
129
+ }
130
+
131
+ export function handleReport(a) {
132
+ return runKjCommand({ command: "report", commandArgs: buildReportArgs(a), options: a });
133
+ }
134
+
135
+ export function handleInit(a) {
136
+ return runKjCommand({ command: "init", options: a });
137
+ }
138
+
139
+ export function handleDoctor(a) {
140
+ return runKjCommand({ command: "doctor", options: a });
141
+ }
142
+
143
+ export function handleConfig(a) {
144
+ return runKjCommand({ command: "config", commandArgs: a.json ? ["--json"] : [], options: a });
145
+ }
146
+
147
+ export async function handleScan(a, server) {
148
+ await runBootstrapGate(server, a);
149
+ return runKjCommand({ command: "scan", options: a });
150
+ }
151
+
152
+ export async function handleBoard(a) {
153
+ const action = a.action || "status";
154
+ const { loadConfig: lc } = await import("../../config.js");
155
+ const { config } = await lc();
156
+ const port = a.port || config.hu_board?.port || 4000;
157
+ const { startBoard, stopBoard, boardStatus } = await import("../../commands/board.js");
158
+ switch (action) {
159
+ case "start": return startBoard(port);
160
+ case "stop": return stopBoard();
161
+ case "status": return boardStatus(port);
162
+ default: return failPayload(`Unknown board action: ${action}`);
163
+ }
164
+ }
165
+
166
+ export async function buildPreflightRequiredResponse(toolName) {
167
+ const { config } = await loadConfig();
168
+ const { listAgents } = await import("../../commands/agents.js");
169
+ const agents = listAgents(config);
170
+ const agentSummary = agents
171
+ .filter(ag => ag.provider !== "-")
172
+ .map(ag => {
173
+ const modelSuffix = ag.model === "-" ? "" : ` (${ag.model})`;
174
+ return ` ${ag.role}: ${ag.provider}${modelSuffix}`;
175
+ })
176
+ .join("\n");
177
+ return responseText({
178
+ ok: false,
179
+ preflightRequired: true,
180
+ message: `PREFLIGHT REQUIRED\n\nCurrent agent configuration:\n${agentSummary}\n\nAsk the human to confirm or adjust this configuration, then call kj_preflight with their response.\n\nDo NOT pass coder/reviewer parameters to ${toolName} — use kj_preflight to set them.`
181
+ });
182
+ }