karajan-code 1.49.0 → 1.50.1

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.
@@ -1,38 +1,28 @@
1
1
  /**
2
2
  * MCP server handler logic.
3
- * Extracted from server.js for testability.
3
+ * Shared helpers remain here; handler implementations are in ./handlers/*.
4
+ * Re-exports everything so existing imports continue to work.
4
5
  */
5
6
 
6
7
  import { EventEmitter } from "node:events";
7
8
  import fs from "node:fs/promises";
8
- import { runKjCommand } from "./run-kj.js";
9
- import { normalizePlanArgs } from "./tool-arg-normalizers.js";
10
- import { buildProgressHandler, buildProgressNotifier, buildPipelineTracker, sendTrackerLog } from "./progress.js";
11
- import { createStallDetector } from "../utils/stall-detector.js";
12
- import { runDirectRole } from "./direct-role-runner.js";
13
- import { runFlow, resumeFlow } from "../orchestrator.js";
14
- import { loadConfig, applyRunOverrides, validateConfig, resolveRole } from "../config.js";
15
- import { createLogger } from "../utils/logger.js";
16
- import { assertAgentsAvailable } from "../agents/availability.js";
17
- import { createAgent } from "../agents/index.js";
18
- import { buildPlannerPrompt } from "../prompts/planner.js";
19
- import { buildCoderPrompt } from "../prompts/coder.js";
20
- import { buildReviewerPrompt } from "../prompts/reviewer.js";
21
- import { parseMaybeJsonString } from "../review/parser.js";
22
- import { computeBaseRef, generateDiff } from "../review/diff-generator.js";
23
- import { resolveReviewProfile } from "../review/profiles.js";
24
- import { createRunLog, readRunLog } from "../utils/run-log.js";
9
+ import { loadConfig, applyRunOverrides, validateConfig } from "../config.js";
25
10
  import { currentBranch } from "../utils/git.js";
26
- import { isPreflightAcked, ackPreflight, getSessionOverrides } from "./preflight.js";
27
- import { ensureBootstrap } from "../bootstrap.js";
28
- import { validateSovereignty } from "./sovereignty-guard.js";
29
- import { handleSuggestion } from "./suggest-handler.js";
11
+ import { buildProgressNotifier } from "./progress.js";
12
+
13
+ // ── Sub-module re-exports ────────────────────────────────────────────
14
+ export { handleRunDirect, handleResumeDirect, validateResumeAnswer, handleRun, handleResume } from "./handlers/run-handler.js";
15
+ export { handlePlanDirect, handleCodeDirect, handleReviewDirect, handleDiscoverDirect, handleTriageDirect, handleResearcherDirect, handleAuditDirect, handleArchitectDirect, handleCode, handleReview, handlePlan, handleDiscover, handleTriage, handleResearcher, handleArchitect, handleAudit } from "./handlers/direct-handlers.js";
16
+ export { handleStatus, handleAgents, handlePreflight, handleRoles, handleReport, handleInit, handleDoctor, handleConfig, handleScan, handleBoard, buildPreflightRequiredResponse } from "./handlers/management-handlers.js";
17
+ export { handleHu, handleSkills, handleSuggest } from "./handlers/hu-handlers.js";
18
+
19
+ // ── Shared helpers (used by sub-modules and external consumers) ──────
30
20
 
31
21
  /**
32
22
  * Resolve the user's project directory.
33
23
  * Priority: 1) explicit projectDir param, 2) MCP roots, 3) error with instructions.
34
24
  */
35
- async function resolveProjectDir(server, explicitProjectDir) {
25
+ export async function resolveProjectDir(server, explicitProjectDir) {
36
26
  // 1. Explicit projectDir from tool parameter — always wins
37
27
  if (explicitProjectDir) return explicitProjectDir;
38
28
 
@@ -157,16 +147,6 @@ export async function assertNotOnBaseBranch(config) {
157
147
  }
158
148
  }
159
149
 
160
- /**
161
- * Run bootstrap gate: validate all environment prerequisites before execution.
162
- * Throws if any prerequisite fails.
163
- */
164
- async function runBootstrapGate(server, a) {
165
- const projectDir = await resolveProjectDir(server, a.projectDir);
166
- const { config } = await loadConfig(projectDir);
167
- await ensureBootstrap(projectDir, config);
168
- }
169
-
170
150
  export function enrichedFailPayload(error, toolName) {
171
151
  const msg = error?.message || String(error);
172
152
  const { category, suggestion } = classifyError(error);
@@ -207,178 +187,12 @@ export function buildAskQuestion(server) {
207
187
  };
208
188
  }
209
189
 
210
- const MAX_AUTO_RESUMES = 2;
211
- const NON_RECOVERABLE_CATEGORIES = new Set([
212
- "config_error", "auth_error", "agent_missing", "branch_error", "git_error", "bootstrap_error"
213
- ]);
214
-
215
- async function attemptAutoResume({ err, config, logger, emitter, askQuestion, runLog }) {
216
- const { category } = classifyError(err);
217
- if (NON_RECOVERABLE_CATEGORIES.has(category)) return null;
218
-
219
- // Find session ID from most recent session file
220
- const { loadMostRecentSession } = await import("../session-store.js");
221
- let session;
222
- try {
223
- session = await loadMostRecentSession();
224
- } catch { /* no session store or read error */
225
- return null;
226
- }
227
- if (!session || !["failed", "stopped"].includes(session.status)) return null;
228
-
229
- const maxRetries = config.session?.max_auto_resumes ?? MAX_AUTO_RESUMES;
230
- const autoResumeCount = session.auto_resume_count || 0;
231
- if (autoResumeCount >= maxRetries) {
232
- runLog.logText(`[resilient] auto-resume limit reached (${maxRetries}), giving up`);
233
- return null;
234
- }
235
-
236
- runLog.logText(`[resilient] run failed (${category}), auto-resuming (${autoResumeCount + 1}/${maxRetries})...`);
237
- emitter.emit("progress", {
238
- type: "resilient:auto_resume",
239
- attempt: autoResumeCount + 1,
240
- maxRetries,
241
- errorCategory: category,
242
- sessionId: session.id
243
- });
244
-
245
- // Increment counter and save before resuming
246
- const { saveSession } = await import("../session-store.js");
247
- session.auto_resume_count = autoResumeCount + 1;
248
- await saveSession(session);
249
-
250
- try {
251
- const result = await resumeFlow({
252
- sessionId: session.id,
253
- config,
254
- logger,
255
- flags: {},
256
- emitter,
257
- askQuestion
258
- });
259
- const ok = !result.paused && (result.approved !== false);
260
- runLog.logText(`[resilient] auto-resume ${ok ? "succeeded" : "finished"} — ok=${ok}`);
261
- return { ok, ...result, autoResumed: true, autoResumeAttempt: autoResumeCount + 1 };
262
- } catch (error) {
263
- // Recursive: try again if still within limits
264
- const nestedResult = await attemptAutoResume({
265
- err: error, config, logger, emitter, askQuestion, runLog
266
- });
267
- if (nestedResult) return nestedResult;
268
- runLog.logText(`[resilient] auto-resume failed: ${error.message}`);
269
- return null;
270
- }
271
- }
272
-
273
- const PIPELINE_PROVIDER_ROLES = [
274
- ["triage", true],
275
- ["planner", true],
276
- ["refactorer", true],
277
- ["researcher", true],
278
- ["tester", true],
279
- ["security", true]
280
- ];
281
-
282
- function collectRequiredProviders(config) {
283
- const providers = [
284
- resolveRole(config, "coder").provider,
285
- config.reviewer_options?.fallback_reviewer
286
- ];
287
- if (config.pipeline?.reviewer?.enabled !== false) {
288
- providers.push(resolveRole(config, "reviewer").provider);
289
- }
290
- for (const [role, requireEnabled] of PIPELINE_PROVIDER_ROLES) {
291
- if (requireEnabled && config.pipeline?.[role]?.enabled) {
292
- providers.push(resolveRole(config, role).provider);
293
- }
294
- }
295
- return providers;
296
- }
297
-
298
- export async function handleRunDirect(a, server, extra) {
299
- const config = await buildConfig(a);
300
- await assertNotOnBaseBranch(config);
301
- const logger = createLogger(config.output.log_level, "mcp");
302
-
303
- // Best-effort session cleanup before starting
304
- try {
305
- const { cleanupExpiredSessions } = await import("../session-cleanup.js");
306
- await cleanupExpiredSessions({ logger });
307
- } catch { /* non-blocking */ }
308
-
309
- await assertAgentsAvailable(collectRequiredProviders(config));
310
-
311
- const projectDir = await resolveProjectDir(server, a.projectDir);
312
- const runLog = createRunLog(projectDir);
313
- runLog.logText(`[kj_run] started — task="${a.task.slice(0, 80)}..."`);
314
-
315
- const emitter = new EventEmitter();
316
- emitter.on("progress", buildProgressHandler(server));
317
- emitter.on("progress", (event) => runLog.logEvent(event));
318
- const progressNotifier = buildProgressNotifier(extra);
319
- if (progressNotifier) emitter.on("progress", progressNotifier);
320
- buildPipelineTracker(config, emitter);
321
-
322
- const askQuestion = buildAskQuestion(server);
323
- const pgTaskId = a.pgTask || null;
324
- const pgProject = a.pgProject || config.planning_game?.project_id || null;
325
- try {
326
- const result = await runFlow({ task: a.task, config, logger, flags: a, emitter, askQuestion, pgTaskId, pgProject });
327
- runLog.logText(`[kj_run] finished — ok=${!result.paused && (result.approved !== false)}`);
328
- return { ok: !result.paused && (result.approved !== false), ...result };
329
- } catch (err) {
330
- const autoResumeResult = await attemptAutoResume({
331
- err, config, logger, emitter, askQuestion, runLog, progressNotifier, extra
332
- });
333
- if (autoResumeResult) return autoResumeResult;
334
- throw err;
335
- } finally {
336
- runLog.close();
337
- }
338
- }
339
-
340
- export async function handleResumeDirect(a, server, extra) {
341
- const config = await buildConfig(a);
342
- const logger = createLogger(config.output.log_level, "mcp");
343
-
344
- const projectDir = await resolveProjectDir(server, a.projectDir);
345
- const runLog = createRunLog(projectDir);
346
- runLog.logText(`[kj_resume] started — session="${a.sessionId}"`);
347
-
348
- const emitter = new EventEmitter();
349
- emitter.on("progress", buildProgressHandler(server));
350
- emitter.on("progress", (event) => runLog.logEvent(event));
351
- const progressNotifier = buildProgressNotifier(extra);
352
- if (progressNotifier) emitter.on("progress", progressNotifier);
353
-
354
- const askQuestion = buildAskQuestion(server);
355
- try {
356
- const result = await resumeFlow({
357
- sessionId: a.sessionId,
358
- answer: a.answer || null,
359
- config,
360
- logger,
361
- flags: a,
362
- emitter,
363
- askQuestion
364
- });
365
- const ok = !result.paused && (result.approved !== false);
366
- runLog.logText(`[kj_resume] finished — ok=${ok}`);
367
- return { ok, ...result };
368
- } catch (err) {
369
- runLog.logText(`[kj_resume] failed: ${err.message}`);
370
- throw err;
371
- } finally {
372
- runLog.close();
373
- }
374
- }
375
-
376
190
  const EVENT_LOG_LEVELS = {
377
191
  "agent:stall": "warning",
378
192
  "agent:heartbeat": "info"
379
193
  };
380
194
 
381
- function buildDirectEmitter(server, runLog, extra) {
195
+ export function buildDirectEmitter(server, runLog, extra) {
382
196
  const emitter = new EventEmitter();
383
197
  emitter.on("progress", (event) => {
384
198
  try {
@@ -392,679 +206,35 @@ function buildDirectEmitter(server, runLog, extra) {
392
206
  return emitter;
393
207
  }
394
208
 
395
- export async function handlePlanDirect(a, server, extra) {
396
- const options = normalizePlanArgs(a);
397
- const config = await buildConfig(options, "plan");
398
- const logger = createLogger(config.output.log_level, "mcp");
399
-
400
- const plannerRole = resolveRole(config, "planner");
401
- await assertAgentsAvailable([plannerRole.provider]);
402
-
403
- const projectDir = await resolveProjectDir(server, a.projectDir);
404
- const runLog = createRunLog(projectDir);
405
- const silenceTimeoutMs = Number(config?.session?.max_agent_silence_minutes) > 0
406
- ? Math.round(Number(config.session.max_agent_silence_minutes) * 60 * 1000)
407
- : undefined;
408
- const plannerTimeoutMs = Number(config?.session?.max_planner_minutes) > 0
409
- ? Math.round(Number(config.session.max_planner_minutes) * 60 * 1000)
410
- : undefined;
411
- const silenceLabel = silenceTimeoutMs ? `${Math.round(silenceTimeoutMs / 1000)}s` : "disabled";
412
- const runtimeLabel = plannerTimeoutMs ? `${Math.round(plannerTimeoutMs / 1000)}s` : "disabled";
413
- runLog.logText(
414
- `[kj_plan] started — provider=${plannerRole.provider}, max_silence=${silenceLabel}, max_runtime=${runtimeLabel}`
415
- );
416
- const emitter = buildDirectEmitter(server, runLog, extra);
417
- const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
418
- const onOutput = ({ stream, line }) => {
419
- emitter.emit("progress", { type: "agent:output", stage: "planner", message: line, detail: { stream, agent: plannerRole.provider } });
420
- };
421
- const stallDetector = createStallDetector({
422
- onOutput, emitter, eventBase, stage: "planner", provider: plannerRole.provider
423
- });
424
-
425
- const planner = createAgent(plannerRole.provider, config, logger);
426
- const prompt = buildPlannerPrompt({ task: a.task, context: a.context });
427
- sendTrackerLog(server, "planner", "running", plannerRole.provider);
428
- runLog.logText(`[planner] agent launched, waiting for response...`);
429
- let result;
430
- let plannerStats = null;
431
- try {
432
- result = await planner.runTask({
433
- prompt,
434
- role: "planner",
435
- onOutput: stallDetector.onOutput,
436
- silenceTimeoutMs,
437
- timeoutMs: plannerTimeoutMs
438
- });
439
- } finally {
440
- stallDetector.stop();
441
- plannerStats = stallDetector.stats();
442
- runLog.logText(
443
- `[planner] finished — lines=${plannerStats.lineCount}, bytes=${plannerStats.bytesReceived}, elapsed=${Math.round(plannerStats.elapsedMs / 1000)}s`
444
- );
445
- runLog.close();
446
- }
447
-
448
- if (!result.ok) {
449
- sendTrackerLog(server, "planner", "failed");
450
- const baseError = result.error || result.output || "Planner failed";
451
- const statsSuffix = plannerStats
452
- ? ` [lines=${plannerStats.lineCount}, bytes=${plannerStats.bytesReceived}, elapsed=${Math.round(plannerStats.elapsedMs / 1000)}s]`
453
- : "";
454
- throw new Error(`${baseError}${statsSuffix}`);
455
- }
456
-
457
- sendTrackerLog(server, "planner", "done");
458
- const parsed = parseMaybeJsonString(result.output);
459
- return { ok: true, plan: parsed || result.output, raw: result.output };
460
- }
461
-
462
- export async function handleCodeDirect(a, server, extra) {
463
- const config = await buildConfig(a, "code");
464
- await assertNotOnBaseBranch(config);
465
- const logger = createLogger(config.output.log_level, "mcp");
466
-
467
- const coderRole = resolveRole(config, "coder");
468
- await assertAgentsAvailable([coderRole.provider]);
469
-
470
- const projectDir = await resolveProjectDir(server, a.projectDir);
471
- const runLog = createRunLog(projectDir);
472
- runLog.logText(`[kj_code] started — provider=${coderRole.provider}`);
473
- const emitter = buildDirectEmitter(server, runLog, extra);
474
- const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
475
- const onOutput = ({ stream, line }) => {
476
- emitter.emit("progress", { type: "agent:output", stage: "coder", message: line, detail: { stream, agent: coderRole.provider } });
477
- };
478
- const stallDetector = createStallDetector({
479
- onOutput, emitter, eventBase, stage: "coder", provider: coderRole.provider
480
- });
481
-
482
- const coder = createAgent(coderRole.provider, config, logger);
483
- let coderRules = null;
484
- if (config.coder_rules) {
485
- try {
486
- coderRules = await fs.readFile(config.coder_rules, "utf8");
487
- } catch { /* configured coder_rules path not found */
488
- try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
489
- }
490
- }
491
- const prompt = await buildCoderPrompt({ task: a.task, coderRules, methodology: config.development?.methodology || "tdd" });
492
- sendTrackerLog(server, "coder", "running", coderRole.provider);
493
- runLog.logText(`[coder] agent launched, waiting for response...`);
494
- let result;
495
- try {
496
- result = await coder.runTask({ prompt, role: "coder", onOutput: stallDetector.onOutput });
497
- } finally {
498
- stallDetector.stop();
499
- const stats = stallDetector.stats();
500
- runLog.logText(`[coder] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
501
- runLog.close();
502
- }
503
-
504
- if (!result.ok) {
505
- sendTrackerLog(server, "coder", "failed");
506
- throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
507
- }
508
-
509
- sendTrackerLog(server, "coder", "done");
510
- return { ok: true, output: result.output, exitCode: result.exitCode };
511
- }
512
-
513
- export async function handleReviewDirect(a, server, extra) {
514
- const config = await buildConfig(a, "review");
515
- await assertNotOnBaseBranch(config);
516
- const logger = createLogger(config.output.log_level, "mcp");
517
-
518
- const reviewerRole = resolveRole(config, "reviewer");
519
- await assertAgentsAvailable([reviewerRole.provider, config.reviewer_options?.fallback_reviewer]);
520
-
521
- const projectDir = await resolveProjectDir(server, a.projectDir);
522
- const runLog = createRunLog(projectDir);
523
- runLog.logText(`[kj_review] started — provider=${reviewerRole.provider}`);
524
- const emitter = buildDirectEmitter(server, runLog, extra);
525
- const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
526
- const onOutput = ({ stream, line }) => {
527
- emitter.emit("progress", { type: "agent:output", stage: "reviewer", message: line, detail: { stream, agent: reviewerRole.provider } });
528
- };
529
- const stallDetector = createStallDetector({
530
- onOutput, emitter, eventBase, stage: "reviewer", provider: reviewerRole.provider
531
- });
532
-
533
- const reviewer = createAgent(reviewerRole.provider, config, logger);
534
- const resolvedBase = await computeBaseRef({ baseBranch: config.base_branch, baseRef: a.baseRef });
535
- const diff = await generateDiff({ baseRef: resolvedBase });
536
- const { rules } = await resolveReviewProfile({ mode: config.review_mode, projectDir: process.cwd() });
537
-
538
- const prompt = await buildReviewerPrompt({ task: a.task, diff, reviewRules: rules, mode: config.review_mode });
539
- sendTrackerLog(server, "reviewer", "running", reviewerRole.provider);
540
- runLog.logText(`[reviewer] agent launched, waiting for response...`);
541
- let result;
542
- try {
543
- result = await reviewer.reviewTask({ prompt, role: "reviewer", onOutput: stallDetector.onOutput });
544
- } finally {
545
- stallDetector.stop();
546
- const stats = stallDetector.stats();
547
- runLog.logText(`[reviewer] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
548
- runLog.close();
549
- }
550
-
551
- if (!result.ok) {
552
- sendTrackerLog(server, "reviewer", "failed");
553
- throw new Error(result.error || result.output || `Reviewer failed (exit ${result.exitCode})`);
554
- }
555
-
556
- sendTrackerLog(server, "reviewer", "done");
557
- const parsed = parseMaybeJsonString(result.output);
558
- return { ok: true, review: parsed || result.output, raw: result.output };
559
- }
560
-
561
- export async function handleDiscoverDirect(a, server, extra) {
562
- // Build context from pgTask if provided
563
- let context = a.context || null;
564
- if (a.pgTask && a.pgProject) {
565
- try {
566
- const pgContext = `Planning Game card: ${a.pgTask} (project: ${a.pgProject})`;
567
- context = context ? `${context}\n\n${pgContext}` : pgContext;
568
- } catch { /* PG not available — proceed without */ }
569
- }
570
-
571
- return runDirectRole({
572
- roleName: "discover",
573
- importRole: async () => {
574
- const { DiscoverRole } = await import("../roles/discover-role.js");
575
- return { RoleClass: DiscoverRole };
576
- },
577
- initContext: { task: a.task },
578
- runInput: { task: a.task, mode: a.mode || "gaps", context },
579
- logStartMsg: `[kj_discover] started — mode=${a.mode || "gaps"}`,
580
- args: a, server, extra,
581
- resolveProjectDir, buildConfig, buildDirectEmitter
582
- });
583
- }
584
-
585
- export async function handleTriageDirect(a, server, extra) {
586
- return runDirectRole({
587
- roleName: "triage",
588
- importRole: async () => {
589
- const { TriageRole } = await import("../roles/triage-role.js");
590
- return { RoleClass: TriageRole };
591
- },
592
- initContext: { task: a.task },
593
- runInput: { task: a.task },
594
- args: a, server, extra,
595
- resolveProjectDir, buildConfig, buildDirectEmitter
596
- });
597
- }
598
-
599
- export async function handleResearcherDirect(a, server, extra) {
600
- return runDirectRole({
601
- roleName: "researcher",
602
- importRole: async () => {
603
- const { ResearcherRole } = await import("../roles/researcher-role.js");
604
- return { RoleClass: ResearcherRole };
605
- },
606
- initContext: { task: a.task },
607
- runInput: { task: a.task },
608
- args: a, server, extra,
609
- resolveProjectDir, buildConfig, buildDirectEmitter
610
- });
611
- }
612
-
613
- export async function handleAuditDirect(a, server, extra) {
614
- const task = a.task || "Analyze the full codebase";
615
- return runDirectRole({
616
- roleName: "audit",
617
- importRole: async () => {
618
- const { AuditRole } = await import("../roles/audit-role.js");
619
- return { RoleClass: AuditRole };
620
- },
621
- initContext: { task },
622
- runInput: { task, dimensions: a.dimensions || null },
623
- logStartMsg: `[kj_audit] started — dimensions=${a.dimensions || "all"}`,
624
- args: a, server, extra,
625
- resolveProjectDir, buildConfig, buildDirectEmitter
626
- });
627
- }
628
-
629
- export async function handleArchitectDirect(a, server, extra) {
630
- return runDirectRole({
631
- roleName: "architect",
632
- importRole: async () => {
633
- const { ArchitectRole } = await import("../roles/architect-role.js");
634
- return { RoleClass: ArchitectRole };
635
- },
636
- initContext: { task: a.task },
637
- runInput: { task: a.task, researchContext: a.context || null },
638
- args: a, server, extra,
639
- resolveProjectDir, buildConfig, buildDirectEmitter
640
- });
641
- }
642
-
643
- /* ── Preflight helpers ─────────────────────────────────────────────── */
644
-
645
- const AGENT_ROLES = new Set(["coder", "reviewer", "tester", "security", "solomon"]);
646
-
647
- async function buildPreflightRequiredResponse(toolName) {
648
- const { config } = await loadConfig();
649
- const { listAgents } = await import("../commands/agents.js");
650
- const agents = listAgents(config);
651
- const agentSummary = agents
652
- .filter(ag => ag.provider !== "-")
653
- .map(ag => {
654
- const modelSuffix = ag.model === "-" ? "" : ` (${ag.model})`;
655
- return ` ${ag.role}: ${ag.provider}${modelSuffix}`;
656
- })
657
- .join("\n");
658
- return responseText({
659
- ok: false,
660
- preflightRequired: true,
661
- 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.`
662
- });
663
- }
664
-
665
- function applySessionOverrides(a, roleKeys) {
666
- const sessionOvr = getSessionOverrides();
667
- for (const key of roleKeys) {
668
- if (sessionOvr[key] !== undefined) { a[key] = sessionOvr[key]; }
669
- }
670
- }
671
-
672
- function parseHumanResponseOverrides(humanResponse, overrides) {
673
- for (const role of AGENT_ROLES) {
674
- const patterns = [
675
- new RegExp(String.raw`use\s+(\w+)\s+(?:as|for)\s+${role}`, "i"),
676
- new RegExp(String.raw`${role}\s*[:=]\s*(\w+)`, "i"),
677
- new RegExp(String.raw`set\s+${role}\s+(?:to|=)\s*(\w+)`, "i")
678
- ];
679
- for (const pat of patterns) {
680
- const m = pat.exec(humanResponse);
681
- if (m && !overrides[role]) {
682
- overrides[role] = m[1];
683
- break;
684
- }
685
- }
686
- }
687
- }
688
-
689
- function buildPreflightOverrides(a) {
690
- const overrides = {};
691
- for (const role of AGENT_ROLES) {
692
- if (a[role]) overrides[role] = a[role];
693
- }
694
- if (a.enableTester !== undefined) overrides.enableTester = a.enableTester;
695
- if (a.enableSecurity !== undefined) overrides.enableSecurity = a.enableSecurity;
696
-
697
- const resp = (a.humanResponse || "").toLowerCase();
698
- if (resp !== "ok") {
699
- parseHumanResponseOverrides(a.humanResponse || "", overrides);
700
- }
701
- return overrides;
702
- }
703
-
704
- function formatPreflightConfig(agents, overrides) {
705
- const lines = agents
706
- .filter(ag => ag.provider !== "-")
707
- .map(ag => {
708
- const ovr = overrides[ag.role] ? ` -> ${overrides[ag.role]} (session override)` : "";
709
- const modelSuffix = ag.model === "-" ? "" : ` (${ag.model})`;
710
- return ` ${ag.role}: ${ag.provider}${modelSuffix}${ovr}`;
711
- });
712
- const overrideLines = Object.entries(overrides)
713
- .filter(([k]) => !AGENT_ROLES.has(k))
714
- .map(([k, v]) => ` ${k}: ${v}`);
715
- return [...lines, ...overrideLines].join("\n");
716
- }
717
-
718
- function buildReportArgs(a) {
719
- const commandArgs = [];
720
- if (a.list) commandArgs.push("--list");
721
- if (a.sessionId) commandArgs.push("--session-id", String(a.sessionId));
722
- if (a.format) commandArgs.push("--format", String(a.format));
723
- if (a.trace) commandArgs.push("--trace");
724
- if (a.currency) commandArgs.push("--currency", String(a.currency));
725
- if (a.pgTask) commandArgs.push("--pg-task", String(a.pgTask));
726
- return commandArgs;
727
- }
728
-
729
- /* ── Individual tool handlers ─────────────────────────────────────── */
730
-
731
- async function handleStatus(a, server) {
732
- const maxLines = a.lines || 50;
733
- const projectDir = await resolveProjectDir(server, a.projectDir);
734
- return readRunLog(projectDir, maxLines);
735
- }
736
-
737
- async function handleAgents(a) {
738
- const action = a.action || "list";
739
- if (action === "set") {
740
- if (!a.role || !a.provider) {
741
- return failPayload("Missing required fields: role and provider");
742
- }
743
- const { setAgent } = await import("../commands/agents.js");
744
- const result = await setAgent(a.role, a.provider, { global: false });
745
- return { ok: true, ...result, message: `${result.role} now uses ${result.provider} (scope: ${result.scope})` };
746
- }
747
- const config = await buildConfig(a);
748
- const { listAgents } = await import("../commands/agents.js");
749
- const sessionOvr = getSessionOverrides();
750
- return { ok: true, agents: listAgents(config, sessionOvr) };
751
- }
752
-
753
- async function handlePreflight(a) {
754
- const overrides = buildPreflightOverrides(a);
755
- ackPreflight(overrides);
756
-
757
- const config = await buildConfig(a);
758
- const { listAgents } = await import("../commands/agents.js");
759
- const agents = listAgents(config);
760
-
761
- return {
762
- ok: true,
763
- message: `Preflight acknowledged. Agent configuration confirmed.`,
764
- config: formatPreflightConfig(agents, overrides),
765
- overrides
766
- };
767
- }
768
-
769
- function handleRoles(a) {
770
- const action = a.action || "list";
771
- const commandArgs = [action];
772
- if (action === "show" && a.roleName) commandArgs.push(a.roleName);
773
- return runKjCommand({ command: "roles", commandArgs, options: a });
774
- }
775
-
776
- const RESUME_MAX_ANSWER_LENGTH = 500;
777
- const RESUME_INJECTION_PATTERNS = [
778
- /ignore\s+(previous|all|above)\s+(instructions|rules|prompts)/i,
779
- /you\s+are\s+now/i,
780
- /new\s+instructions?:/i,
781
- /override\s+(all|security|guardrails|rules)/i,
782
- /skip\s+(all\s+)?(review|test|sonar|security|solomon|guard)s?\b/i,
783
- /disable\s+(tdd|review|test|sonar|security)\b/i,
784
- /set\s+(status|approved|verdict)\s*(=|to|:)/i,
785
- /force\s+(approve|merge|push|commit)\b/i,
786
- ];
787
-
788
- export function validateResumeAnswer(answer) {
789
- if (answer == null || answer === "") return { valid: true, sanitized: answer ?? null };
790
- if (typeof answer !== "string") return { valid: true, sanitized: String(answer) };
791
- if (answer.length > RESUME_MAX_ANSWER_LENGTH) {
792
- return { valid: false, reason: `Answer too long (${answer.length} chars, max ${RESUME_MAX_ANSWER_LENGTH})` };
793
- }
794
- for (const pattern of RESUME_INJECTION_PATTERNS) {
795
- if (pattern.test(answer)) {
796
- return { valid: false, reason: "Answer rejected: matches guardrail bypass pattern" };
797
- }
798
- }
799
- return { valid: true, sanitized: answer.trim() };
800
- }
801
-
802
- async function handleResume(a, server, extra) {
803
- if (!a.sessionId) {
804
- return failPayload("Missing required field: sessionId");
805
- }
806
- await runBootstrapGate(server, a);
807
- if (a.answer) {
808
- const validation = validateResumeAnswer(a.answer);
809
- if (!validation.valid) {
810
- return failPayload(`Resume answer rejected: ${validation.reason}`);
811
- }
812
- a.answer = validation.sanitized;
813
- }
814
- applySessionOverrides(a, ["coder", "reviewer", "tester", "security", "solomon", "enableTester", "enableSecurity", "enableImpeccable"]);
815
- return handleResumeDirect(a, server, extra);
816
- }
817
-
818
- async function handleRun(a, server, extra) {
819
- if (!a.task) {
820
- return failPayload("Missing required field: task");
821
- }
822
- if (a.taskType) {
823
- const validTypes = new Set(["sw", "infra", "doc", "add-tests", "refactor"]);
824
- if (!validTypes.has(a.taskType)) {
825
- return failPayload(`Invalid taskType "${a.taskType}". Valid values: ${[...validTypes].join(", ")}`);
826
- }
827
- }
828
-
829
- // Sovereignty guard: sanitise params and check for active sessions
830
- const projectDir = await resolveProjectDir(server, a.projectDir);
831
- const sovereignty = validateSovereignty(a, { projectDir });
832
- if (sovereignty.error) {
833
- return failPayload(sovereignty.error);
834
- }
835
- if (sovereignty.warnings.length > 0) {
836
- const logger = createLogger("info", "mcp");
837
- for (const w of sovereignty.warnings) logger.warn(`[sovereignty] ${w}`);
838
- }
839
- Object.assign(a, sovereignty.params);
840
-
841
- await runBootstrapGate(server, a);
842
- if (!isPreflightAcked()) {
843
- // Auto-acknowledge with defaults for autonomous operation
844
- ackPreflight({});
845
- const logger = createLogger("info", "mcp");
846
- logger.info("Preflight auto-acknowledged with default agent config");
847
- }
848
- applySessionOverrides(a, ["coder", "reviewer", "tester", "security", "solomon", "enableTester", "enableSecurity", "enableImpeccable"]);
849
- return handleRunDirect(a, server, extra);
850
- }
851
-
852
- async function handleCode(a, server, extra) {
853
- if (!a.task) {
854
- return failPayload("Missing required field: task");
855
- }
856
- await runBootstrapGate(server, a);
857
- if (!isPreflightAcked()) {
858
- // Auto-acknowledge with defaults for autonomous operation
859
- ackPreflight({});
860
- const logger = createLogger("info", "mcp");
861
- logger.info("Preflight auto-acknowledged with default agent config");
862
- }
863
- applySessionOverrides(a, ["coder"]);
864
- return handleCodeDirect(a, server, extra);
865
- }
866
-
867
- async function handleReview(a, server, extra) {
868
- if (!a.task) {
869
- return failPayload("Missing required field: task");
870
- }
871
- await runBootstrapGate(server, a);
872
- return handleReviewDirect(a, server, extra);
873
- }
874
-
875
- async function handlePlan(a, server, extra) {
876
- if (!a.task) {
877
- return failPayload("Missing required field: task");
878
- }
879
- await runBootstrapGate(server, a);
880
- return handlePlanDirect(a, server, extra);
881
- }
882
-
883
- async function handleDiscover(a, server, extra) {
884
- if (!a.task) {
885
- return failPayload("Missing required field: task");
886
- }
887
- const validModes = new Set(["gaps", "momtest", "wendel", "classify", "jtbd"]);
888
- if (a.mode && !validModes.has(a.mode)) {
889
- return failPayload(`Invalid mode "${a.mode}". Valid values: ${[...validModes].join(", ")}`);
890
- }
891
- await runBootstrapGate(server, a);
892
- return handleDiscoverDirect(a, server, extra);
893
- }
894
-
895
- async function handleTriage(a, server, extra) {
896
- if (!a.task) {
897
- return failPayload("Missing required field: task");
898
- }
899
- await runBootstrapGate(server, a);
900
- return handleTriageDirect(a, server, extra);
901
- }
902
-
903
- async function handleResearcher(a, server, extra) {
904
- if (!a.task) {
905
- return failPayload("Missing required field: task");
906
- }
907
- await runBootstrapGate(server, a);
908
- return handleResearcherDirect(a, server, extra);
909
- }
910
-
911
- async function handleArchitect(a, server, extra) {
912
- if (!a.task) {
913
- return failPayload("Missing required field: task");
914
- }
915
- await runBootstrapGate(server, a);
916
- return handleArchitectDirect(a, server, extra);
917
- }
918
-
919
- async function handleAudit(a, server, extra) {
920
- await runBootstrapGate(server, a);
921
- const result = await handleAuditDirect(a, server, extra);
922
-
923
- // Return compact summary for MCP (full details in session log)
924
- if (result?.ok && result?.summary) {
925
- const compact = {
926
- ok: true,
927
- overallHealth: result.summary?.overallHealth || result.summary,
928
- totalFindings: result.summary?.totalFindings,
929
- critical: result.summary?.critical,
930
- high: result.summary?.high,
931
- topRecommendations: (result.topRecommendations || []).slice(0, 5).map(r => ({
932
- priority: r.priority,
933
- dimension: r.dimension,
934
- action: r.action,
935
- impact: r.impact
936
- })),
937
- textSummary: result.textSummary || result.summary,
938
- basalCost: result.basalCost ? {
939
- totalLines: result.basalCost.totalLines,
940
- totalFiles: result.basalCost.totalFiles,
941
- dependencies: result.basalCost.dependencies
942
- } : undefined
943
- };
944
- return { content: [{ type: "text", text: JSON.stringify(compact, null, 2) }] };
945
- }
946
- return result;
947
- }
948
-
949
- async function handleHu(a, server) {
950
- const action = a.action;
951
- if (!action) return failPayload("Missing required field: action");
952
-
953
- const projectDir = await resolveProjectDir(server, a.projectDir);
954
- const { createManualHu, listHus, getHu, updateHuStatus } = await import("../hu/store.js");
955
-
956
- switch (action) {
957
- case "create": {
958
- if (!a.title) return failPayload("Missing required field: title (required for create)");
959
- const hu = await createManualHu(projectDir, {
960
- title: a.title,
961
- description: a.description,
962
- status: a.status,
963
- acceptanceCriteria: a.acceptanceCriteria
964
- });
965
- return { ok: true, hu };
966
- }
967
- case "list": {
968
- const hus = await listHus(projectDir);
969
- return { ok: true, hus };
970
- }
971
- case "get": {
972
- if (!a.huId) return failPayload("Missing required field: huId (required for get)");
973
- const hu = await getHu(projectDir, a.huId);
974
- return { ok: true, hu };
975
- }
976
- case "update": {
977
- if (!a.huId) return failPayload("Missing required field: huId (required for update)");
978
- if (!a.status) return failPayload("Missing required field: status (required for update)");
979
- const hu = await updateHuStatus(projectDir, a.huId, a.status);
980
- return { ok: true, hu };
981
- }
982
- default:
983
- return failPayload(`Unknown hu action: ${action}`);
984
- }
985
- }
986
-
987
- async function handleBoard(a) {
988
- const action = a.action || "status";
989
- const { loadConfig: lc } = await import("../config.js");
990
- const { config } = await lc();
991
- const port = a.port || config.hu_board?.port || 4000;
992
- const { startBoard, stopBoard, boardStatus } = await import("../commands/board.js");
993
- switch (action) {
994
- case "start": return startBoard(port);
995
- case "stop": return stopBoard();
996
- case "status": return boardStatus(port);
997
- default: return failPayload(`Unknown board action: ${action}`);
998
- }
999
- }
1000
-
1001
- async function handleScan(a, server) {
1002
- await runBootstrapGate(server, a);
1003
- return runKjCommand({ command: "scan", options: a });
1004
- }
1005
-
1006
- async function handleSuggest(a) {
1007
- if (!a.suggestion) {
1008
- return failPayload("Missing required field: suggestion");
1009
- }
1010
- return handleSuggestion({
1011
- suggestion: a.suggestion,
1012
- context: a.context || null,
1013
- projectDir: a.projectDir || null
1014
- });
1015
- }
1016
-
1017
- async function handleSkills(a) {
1018
- const action = a.action;
1019
- if (!action) return failPayload("Missing required field: action");
1020
-
1021
- const { isOpenSkillsAvailable, installSkill, removeSkill, listSkills, readSkill } =
1022
- await import("../skills/openskills-client.js");
1023
-
1024
- const opts = { projectDir: a.projectDir || null };
1025
-
1026
- switch (action) {
1027
- case "install": {
1028
- if (!a.source) return failPayload("Missing required field: source (required for install)");
1029
- const available = await isOpenSkillsAvailable();
1030
- if (!available) {
1031
- return failPayload("OpenSkills CLI is not available. Install it with: npm install -g openskills");
1032
- }
1033
- return installSkill(a.source, { ...opts, global: a.global || false });
1034
- }
1035
- case "remove": {
1036
- if (!a.name) return failPayload("Missing required field: name (required for remove)");
1037
- return removeSkill(a.name, opts);
1038
- }
1039
- case "list": {
1040
- return listSkills(opts);
1041
- }
1042
- case "read": {
1043
- if (!a.name) return failPayload("Missing required field: name (required for read)");
1044
- return readSkill(a.name, opts);
1045
- }
1046
- default:
1047
- return failPayload(`Unknown skills action: ${action}`);
1048
- }
1049
- }
209
+ // ── Handler dispatch map (imports from sub-modules) ──────────────────
1050
210
 
1051
- /* ── Handler dispatch map ─────────────────────────────────────────── */
211
+ import { handleRun, handleResume } from "./handlers/run-handler.js";
212
+ import {
213
+ handleCode, handleReview, handlePlan, handleDiscover,
214
+ handleTriage, handleResearcher, handleArchitect, handleAudit
215
+ } from "./handlers/direct-handlers.js";
216
+ import {
217
+ handleStatus, handleAgents, handlePreflight, handleRoles,
218
+ handleReport, handleInit, handleDoctor, handleConfig,
219
+ handleScan, handleBoard
220
+ } from "./handlers/management-handlers.js";
221
+ import { handleHu, handleSkills, handleSuggest } from "./handlers/hu-handlers.js";
1052
222
 
1053
223
  const toolHandlers = {
1054
- kj_status: (a, server) => handleStatus(a, server),
1055
- kj_init: (a) => runKjCommand({ command: "init", options: a }),
1056
- kj_doctor: (a) => runKjCommand({ command: "doctor", options: a }),
1057
- kj_agents: (a) => handleAgents(a),
1058
- kj_preflight: (a) => handlePreflight(a),
1059
- kj_config: (a) => runKjCommand({ command: "config", commandArgs: a.json ? ["--json"] : [], options: a }),
1060
- kj_scan: (a, server) => handleScan(a, server),
1061
- kj_roles: (a) => handleRoles(a),
1062
- kj_report: (a) => runKjCommand({ command: "report", commandArgs: buildReportArgs(a), options: a }),
1063
- kj_resume: (a, server, extra) => handleResume(a, server, extra),
1064
- kj_run: (a, server, extra) => handleRun(a, server, extra),
1065
- kj_code: (a, server, extra) => handleCode(a, server, extra),
1066
- kj_review: (a, server, extra) => handleReview(a, server, extra),
1067
- kj_plan: (a, server, extra) => handlePlan(a, server, extra),
224
+ kj_status: (a, server) => handleStatus(a, server),
225
+ kj_init: (a) => handleInit(a),
226
+ kj_doctor: (a) => handleDoctor(a),
227
+ kj_agents: (a) => handleAgents(a),
228
+ kj_preflight: (a) => handlePreflight(a),
229
+ kj_config: (a) => handleConfig(a),
230
+ kj_scan: (a, server) => handleScan(a, server),
231
+ kj_roles: (a) => handleRoles(a),
232
+ kj_report: (a) => handleReport(a),
233
+ kj_resume: (a, server, extra) => handleResume(a, server, extra),
234
+ kj_run: (a, server, extra) => handleRun(a, server, extra),
235
+ kj_code: (a, server, extra) => handleCode(a, server, extra),
236
+ kj_review: (a, server, extra) => handleReview(a, server, extra),
237
+ kj_plan: (a, server, extra) => handlePlan(a, server, extra),
1068
238
  kj_discover: (a, server, extra) => handleDiscover(a, server, extra),
1069
239
  kj_triage: (a, server, extra) => handleTriage(a, server, extra),
1070
240
  kj_researcher: (a, server, extra) => handleResearcher(a, server, extra),