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.
- package/README.md +1 -1
- package/docs/README.es.md +1 -1
- package/package.json +1 -1
- package/src/mcp/handlers/direct-handlers.js +393 -0
- package/src/mcp/handlers/hu-handlers.js +93 -0
- package/src/mcp/handlers/management-handlers.js +182 -0
- package/src/mcp/handlers/run-handler.js +283 -0
- package/src/mcp/server-handlers.js +40 -870
- package/src/orchestrator/iteration-stages.js +9 -890
- package/src/orchestrator/pre-loop-stages.js +8 -906
- package/src/orchestrator/preflight-checks.js +3 -1
- package/src/orchestrator/solomon-escalation.js +34 -10
- package/src/orchestrator/stages/architect-stage.js +176 -0
- package/src/orchestrator/stages/coder-stage.js +307 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +467 -0
- package/src/orchestrator/stages/planner-stage.js +90 -0
- package/src/orchestrator/stages/research-stage.js +74 -0
- package/src/orchestrator/stages/reviewer-stage.js +350 -0
- package/src/orchestrator/stages/sonar-stage.js +267 -0
- package/src/orchestrator/stages/triage-stage.js +137 -0
- package/src/orchestrator.js +25 -9
- package/src/planning-game/decomposition.js +7 -5
- package/src/planning-game/pipeline-adapter.js +6 -2
- package/src/utils/messages.js +61 -0
|
@@ -1,38 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP server handler logic.
|
|
3
|
-
*
|
|
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 {
|
|
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 {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1055
|
-
kj_init:
|
|
1056
|
-
kj_doctor:
|
|
1057
|
-
kj_agents:
|
|
1058
|
-
kj_preflight:
|
|
1059
|
-
kj_config:
|
|
1060
|
-
kj_scan:
|
|
1061
|
-
kj_roles:
|
|
1062
|
-
kj_report:
|
|
1063
|
-
kj_resume:
|
|
1064
|
-
kj_run:
|
|
1065
|
-
kj_code:
|
|
1066
|
-
kj_review:
|
|
1067
|
-
kj_plan:
|
|
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),
|