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
package/README.md
CHANGED
|
@@ -243,7 +243,7 @@ Not nostalgia, not stubbornness. I've been using JavaScript since 1997, when Bre
|
|
|
243
243
|
git clone https://github.com/manufosela/karajan-code.git
|
|
244
244
|
cd karajan-code
|
|
245
245
|
npm install
|
|
246
|
-
npm test # Run
|
|
246
|
+
npm test # Run ~2473 tests with Vitest
|
|
247
247
|
npm run validate # Lint + test
|
|
248
248
|
```
|
|
249
249
|
|
package/docs/README.es.md
CHANGED
|
@@ -213,7 +213,7 @@ No es nostalgia ni cabezoneria. Es que llevo usando JavaScript desde 1997, cuand
|
|
|
213
213
|
git clone https://github.com/manufosela/karajan-code.git
|
|
214
214
|
cd karajan-code
|
|
215
215
|
npm install
|
|
216
|
-
npm test # Ejecutar
|
|
216
|
+
npm test # Ejecutar ~2473 tests con Vitest
|
|
217
217
|
npm run validate # Lint + test
|
|
218
218
|
```
|
|
219
219
|
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|