karajan-code 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
package/src/cli.js CHANGED
@@ -18,6 +18,10 @@ import { resumeCommand } from "./commands/resume.js";
18
18
  import { sonarCommand, sonarOpenCommand } from "./commands/sonar.js";
19
19
  import { rolesCommand } from "./commands/roles.js";
20
20
  import { agentsCommand } from "./commands/agents.js";
21
+ import { discoverCommand } from "./commands/discover.js";
22
+ import { triageCommand } from "./commands/triage.js";
23
+ import { researcherCommand } from "./commands/researcher.js";
24
+ import { architectCommand } from "./commands/architect.js";
21
25
 
22
26
  const PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
23
27
  const PKG_VERSION = JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
@@ -93,6 +97,8 @@ program
93
97
  .option("--methodology <name>")
94
98
  .option("--no-auto-rebase")
95
99
  .option("--no-sonar")
100
+ .option("--enable-sonarcloud", "Enable SonarCloud scan (complementary to SonarQube)")
101
+ .option("--no-sonarcloud")
96
102
  .option("--checkpoint-interval <n>", "Minutes between interactive checkpoints (default: 5)")
97
103
  .option("--pg-task <cardId>", "Planning Game card ID (e.g., KJC-TSK-0042)")
98
104
  .option("--pg-project <projectId>", "Planning Game project ID")
@@ -191,6 +197,59 @@ program
191
197
  });
192
198
  });
193
199
 
200
+ program
201
+ .command("discover")
202
+ .description("Analyze task for gaps, ambiguities and missing info")
203
+ .argument("<task>")
204
+ .option("--mode <name>", "Discovery mode: gaps|momtest|wendel|classify|jtbd", "gaps")
205
+ .option("--discover <name>", "Override discover agent")
206
+ .option("--discover-model <name>", "Override discover model")
207
+ .option("--json", "Output raw JSON")
208
+ .action(async (task, flags) => {
209
+ await withConfig("discover", flags, async ({ config, logger }) => {
210
+ await discoverCommand({ task, config, logger, mode: flags.mode, json: flags.json });
211
+ });
212
+ });
213
+
214
+ program
215
+ .command("triage")
216
+ .description("Classify task complexity and recommend pipeline roles")
217
+ .argument("<task>")
218
+ .option("--triage <name>", "Override triage agent")
219
+ .option("--triage-model <name>", "Override triage model")
220
+ .option("--json", "Output raw JSON")
221
+ .action(async (task, flags) => {
222
+ await withConfig("triage", flags, async ({ config, logger }) => {
223
+ await triageCommand({ task, config, logger, json: flags.json });
224
+ });
225
+ });
226
+
227
+ program
228
+ .command("researcher")
229
+ .description("Research codebase for a task (files, patterns, constraints)")
230
+ .argument("<task>")
231
+ .option("--researcher <name>", "Override researcher agent")
232
+ .option("--researcher-model <name>", "Override researcher model")
233
+ .action(async (task, flags) => {
234
+ await withConfig("researcher", flags, async ({ config, logger }) => {
235
+ await researcherCommand({ task, config, logger });
236
+ });
237
+ });
238
+
239
+ program
240
+ .command("architect")
241
+ .description("Design solution architecture (layers, patterns, contracts)")
242
+ .argument("<task>")
243
+ .option("--architect <name>", "Override architect agent")
244
+ .option("--architect-model <name>", "Override architect model")
245
+ .option("--context <text>", "Additional context (e.g. researcher output)")
246
+ .option("--json", "Output raw JSON")
247
+ .action(async (task, flags) => {
248
+ await withConfig("architect", flags, async ({ config, logger }) => {
249
+ await architectCommand({ task, config, logger, context: flags.context, json: flags.json });
250
+ });
251
+ });
252
+
194
253
  program
195
254
  .command("resume")
196
255
  .description("Resume a paused session")
@@ -0,0 +1,90 @@
1
+ import { createAgent } from "../agents/index.js";
2
+ import { assertAgentsAvailable } from "../agents/availability.js";
3
+ import { resolveRole } from "../config.js";
4
+ import { buildArchitectPrompt, parseArchitectOutput } from "../prompts/architect.js";
5
+
6
+ function formatArchitect(result) {
7
+ const lines = [];
8
+ lines.push(`## Architecture Design`);
9
+ lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
10
+
11
+ const arch = result.architecture;
12
+ if (arch) {
13
+ if (arch.type) lines.push(`**Type:** ${arch.type}`, "");
14
+
15
+ if (arch.layers?.length) {
16
+ lines.push("### Layers");
17
+ for (const l of arch.layers) {
18
+ if (typeof l === "string") {
19
+ lines.push(`- ${l}`);
20
+ } else {
21
+ lines.push(`- **${l.name}**: ${l.responsibility || ""}`);
22
+ }
23
+ }
24
+ lines.push("");
25
+ }
26
+
27
+ if (arch.patterns?.length) {
28
+ lines.push("### Patterns");
29
+ for (const p of arch.patterns) lines.push(`- ${p}`);
30
+ lines.push("");
31
+ }
32
+
33
+ if (arch.tradeoffs?.length) {
34
+ lines.push("### Tradeoffs");
35
+ for (const t of arch.tradeoffs) {
36
+ lines.push(`- **${t.decision}**: ${t.rationale || ""}`);
37
+ if (t.alternatives?.length) lines.push(` Alternatives: ${t.alternatives.join(", ")}`);
38
+ }
39
+ lines.push("");
40
+ }
41
+
42
+ if (arch.apiContracts?.length) {
43
+ lines.push("### API Contracts");
44
+ for (const c of arch.apiContracts) {
45
+ lines.push(`- \`${c.method || "GET"} ${c.endpoint}\``);
46
+ }
47
+ lines.push("");
48
+ }
49
+ }
50
+
51
+ if (result.questions?.length) {
52
+ lines.push("### Clarification Questions");
53
+ for (const q of result.questions) {
54
+ lines.push(`- ${q.question || q}`);
55
+ }
56
+ lines.push("");
57
+ }
58
+
59
+ if (result.summary) lines.push(`---\n${result.summary}`);
60
+ return lines.join("\n");
61
+ }
62
+
63
+ export async function architectCommand({ task, config, logger, context, json }) {
64
+ const architectRole = resolveRole(config, "architect");
65
+ await assertAgentsAvailable([architectRole.provider]);
66
+ logger.info(`Architect (${architectRole.provider}) starting...`);
67
+
68
+ const agent = createAgent(architectRole.provider, config, logger);
69
+ const prompt = buildArchitectPrompt({ task, researchContext: context });
70
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
71
+ const result = await agent.runTask({ prompt, onOutput, role: "architect" });
72
+
73
+ if (!result.ok) {
74
+ throw new Error(result.error || result.output || "Architect failed");
75
+ }
76
+
77
+ const parsed = parseArchitectOutput(result.output);
78
+
79
+ if (json) {
80
+ console.log(JSON.stringify(parsed || result.output, null, 2));
81
+ return;
82
+ }
83
+
84
+ if (parsed?.verdict) {
85
+ console.log(formatArchitect(parsed));
86
+ } else {
87
+ console.log(result.output);
88
+ }
89
+ logger.info("Architect completed.");
90
+ }
@@ -0,0 +1,87 @@
1
+ import { createAgent } from "../agents/index.js";
2
+ import { assertAgentsAvailable } from "../agents/availability.js";
3
+ import { resolveRole } from "../config.js";
4
+ import { buildDiscoverPrompt, parseDiscoverOutput } from "../prompts/discover.js";
5
+ import { parseMaybeJsonString } from "../review/parser.js";
6
+
7
+ function formatDiscover(result, mode) {
8
+ const lines = [];
9
+ lines.push(`## Discovery (${mode})`);
10
+ lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
11
+
12
+ if (result.gaps?.length) {
13
+ lines.push("## Gaps");
14
+ for (const g of result.gaps) {
15
+ const sev = g.severity ? ` [${g.severity}]` : "";
16
+ lines.push(`- ${g.description || g}${sev}`);
17
+ if (g.suggestedQuestion) lines.push(` → ${g.suggestedQuestion}`);
18
+ }
19
+ lines.push("");
20
+ }
21
+
22
+ if (result.momTestQuestions?.length) {
23
+ lines.push("## Mom Test Questions");
24
+ for (const q of result.momTestQuestions) {
25
+ lines.push(`- ${q.question || q}`);
26
+ if (q.rationale) lines.push(` _${q.rationale}_`);
27
+ }
28
+ lines.push("");
29
+ }
30
+
31
+ if (result.wendelChecklist?.length) {
32
+ lines.push("## Wendel Checklist");
33
+ for (const w of result.wendelChecklist) {
34
+ const icon = w.status === "pass" ? "✓" : w.status === "fail" ? "✗" : "?";
35
+ lines.push(`- [${icon}] ${w.condition}: ${w.justification || ""}`);
36
+ }
37
+ lines.push("");
38
+ }
39
+
40
+ if (result.classification) {
41
+ lines.push("## Classification");
42
+ lines.push(`- Type: ${result.classification.type}`);
43
+ if (result.classification.adoptionRisk) lines.push(`- Adoption risk: ${result.classification.adoptionRisk}`);
44
+ if (result.classification.frictionEstimate) lines.push(`- Friction: ${result.classification.frictionEstimate}`);
45
+ lines.push("");
46
+ }
47
+
48
+ if (result.jtbds?.length) {
49
+ lines.push("## Jobs-to-be-Done");
50
+ for (const j of result.jtbds) {
51
+ lines.push(`- **${j.id || ""}**: ${j.functional || j}`);
52
+ }
53
+ lines.push("");
54
+ }
55
+
56
+ if (result.summary) lines.push(`---\n${result.summary}`);
57
+ return lines.join("\n");
58
+ }
59
+
60
+ export async function discoverCommand({ task, config, logger, mode, json }) {
61
+ const discoverRole = resolveRole(config, "discover");
62
+ await assertAgentsAvailable([discoverRole.provider]);
63
+ logger.info(`Discover (${discoverRole.provider}) starting — mode: ${mode || "gaps"}...`);
64
+
65
+ const agent = createAgent(discoverRole.provider, config, logger);
66
+ const prompt = buildDiscoverPrompt({ task, mode: mode || "gaps" });
67
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
68
+ const result = await agent.runTask({ prompt, onOutput, role: "discover" });
69
+
70
+ if (!result.ok) {
71
+ throw new Error(result.error || result.output || "Discover failed");
72
+ }
73
+
74
+ const parsed = parseDiscoverOutput(result.output);
75
+
76
+ if (json) {
77
+ console.log(JSON.stringify(parsed || result.output, null, 2));
78
+ return;
79
+ }
80
+
81
+ if (parsed?.verdict) {
82
+ console.log(formatDiscover(parsed, mode || "gaps"));
83
+ } else {
84
+ console.log(result.output);
85
+ }
86
+ logger.info("Discover completed.");
87
+ }
@@ -0,0 +1,40 @@
1
+ import { createAgent } from "../agents/index.js";
2
+ import { assertAgentsAvailable } from "../agents/availability.js";
3
+ import { resolveRole } from "../config.js";
4
+
5
+ const SUBAGENT_PREAMBLE = [
6
+ "IMPORTANT: You are running as a Karajan sub-agent.",
7
+ "Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
8
+ "Do NOT use any MCP tools. Focus only on researching the codebase."
9
+ ].join(" ");
10
+
11
+ function buildResearchPrompt(task) {
12
+ return [
13
+ SUBAGENT_PREAMBLE,
14
+ "Investigate the codebase for the following task.",
15
+ "Identify affected files, patterns, constraints, prior decisions, risks, and test coverage.",
16
+ "Return a single valid JSON object with your findings and nothing else.",
17
+ '{"affected_files":[string],"patterns":[string],"constraints":[string],"prior_decisions":[string],"risks":[string],"test_coverage":string}',
18
+ `## Task\n${task}`
19
+ ].join("\n\n");
20
+ }
21
+
22
+ export async function researcherCommand({ task, config, logger }) {
23
+ const researcherRole = resolveRole(config, "researcher");
24
+ await assertAgentsAvailable([researcherRole.provider]);
25
+ logger.info(`Researcher (${researcherRole.provider}) starting...`);
26
+
27
+ const agent = createAgent(researcherRole.provider, config, logger);
28
+ const prompt = buildResearchPrompt(task);
29
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
30
+ const result = await agent.runTask({ prompt, onOutput, role: "researcher" });
31
+
32
+ if (!result.ok) {
33
+ throw new Error(result.error || result.output || "Researcher failed");
34
+ }
35
+
36
+ if (result.output) {
37
+ console.log(result.output);
38
+ }
39
+ logger.info("Researcher completed.");
40
+ }
@@ -0,0 +1,63 @@
1
+ import { createAgent } from "../agents/index.js";
2
+ import { assertAgentsAvailable } from "../agents/availability.js";
3
+ import { resolveRole } from "../config.js";
4
+ import { buildTriagePrompt } from "../prompts/triage.js";
5
+ import { parseMaybeJsonString } from "../review/parser.js";
6
+
7
+ function formatTriage(result) {
8
+ const lines = [];
9
+ lines.push(`## Triage Result`);
10
+ lines.push(`- **Level:** ${result.level || "unknown"}`);
11
+ if (result.taskType) lines.push(`- **Task type:** ${result.taskType}`);
12
+ if (result.reasoning) lines.push(`- **Reasoning:** ${result.reasoning}`);
13
+ lines.push("");
14
+
15
+ if (result.roles?.length) {
16
+ lines.push("### Recommended Roles");
17
+ for (const r of result.roles) {
18
+ lines.push(`- ${r}`);
19
+ }
20
+ lines.push("");
21
+ }
22
+
23
+ if (result.shouldDecompose) {
24
+ lines.push("### Decomposition Suggested");
25
+ if (result.subtasks?.length) {
26
+ for (const s of result.subtasks) {
27
+ lines.push(`- ${typeof s === "string" ? s : s.title || s}`);
28
+ }
29
+ }
30
+ lines.push("");
31
+ }
32
+
33
+ return lines.join("\n");
34
+ }
35
+
36
+ export async function triageCommand({ task, config, logger, json }) {
37
+ const triageRole = resolveRole(config, "triage");
38
+ await assertAgentsAvailable([triageRole.provider]);
39
+ logger.info(`Triage (${triageRole.provider}) starting...`);
40
+
41
+ const agent = createAgent(triageRole.provider, config, logger);
42
+ const prompt = buildTriagePrompt({ task });
43
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
44
+ const result = await agent.runTask({ prompt, onOutput, role: "triage" });
45
+
46
+ if (!result.ok) {
47
+ throw new Error(result.error || result.output || "Triage failed");
48
+ }
49
+
50
+ const parsed = parseMaybeJsonString(result.output);
51
+
52
+ if (json) {
53
+ console.log(JSON.stringify(parsed || result.output, null, 2));
54
+ return;
55
+ }
56
+
57
+ if (parsed?.level) {
58
+ console.log(formatTriage(parsed));
59
+ } else {
60
+ console.log(result.output);
61
+ }
62
+ logger.info("Triage completed.");
63
+ }
package/src/config.js CHANGED
@@ -101,6 +101,18 @@ const DEFAULTS = {
101
101
  disabled_rules: ["javascript:S1116", "javascript:S3776"]
102
102
  }
103
103
  },
104
+ sonarcloud: {
105
+ enabled: false,
106
+ organization: null,
107
+ token: null,
108
+ project_key: null,
109
+ host: "https://sonarcloud.io",
110
+ scanner: {
111
+ sources: "src,public,lib",
112
+ exclusions: "**/node_modules/**,**/dist/**,**/build/**,**/*.min.js",
113
+ test_inclusions: "**/*.test.js,**/*.spec.js,**/tests/**,**/__tests__/**"
114
+ }
115
+ },
104
116
  policies: {},
105
117
  serena: { enabled: false },
106
118
  planning_game: { enabled: false, project_id: null, codeveloper: null },
@@ -347,6 +359,9 @@ function applyBecariaOverride(out, flags) {
347
359
 
348
360
  function applyMiscOverrides(out, flags) {
349
361
  if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
362
+ out.sonarcloud = out.sonarcloud || {};
363
+ if (flags.enableSonarcloud === true) out.sonarcloud.enabled = true;
364
+ if (flags.noSonarcloud === true || flags.sonarcloud === false) out.sonarcloud.enabled = false;
350
365
 
351
366
  out.planning_game = out.planning_game || {};
352
367
  if (flags.pgTask) out.planning_game.enabled = true;
@@ -469,6 +469,138 @@ export async function handleDiscoverDirect(a, server, extra) {
469
469
  return { ok: true, ...result.result, summary: result.summary };
470
470
  }
471
471
 
472
+ export async function handleTriageDirect(a, server, extra) {
473
+ const config = await buildConfig(a, "triage");
474
+ const logger = createLogger(config.output.log_level, "mcp");
475
+
476
+ const triageRole = resolveRole(config, "triage");
477
+ await assertAgentsAvailable([triageRole.provider]);
478
+
479
+ const projectDir = await resolveProjectDir(server);
480
+ const runLog = createRunLog(projectDir);
481
+ runLog.logText(`[kj_triage] started`);
482
+ const emitter = buildDirectEmitter(server, runLog, extra);
483
+ const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
484
+ const onOutput = ({ stream, line }) => {
485
+ emitter.emit("progress", { type: "agent:output", stage: "triage", message: line, detail: { stream, agent: triageRole.provider } });
486
+ };
487
+ const stallDetector = createStallDetector({
488
+ onOutput, emitter, eventBase, stage: "triage", provider: triageRole.provider
489
+ });
490
+
491
+ const { TriageRole } = await import("../roles/triage-role.js");
492
+ const triage = new TriageRole({ config, logger, emitter });
493
+ await triage.init({ task: a.task });
494
+
495
+ sendTrackerLog(server, "triage", "running", triageRole.provider);
496
+ runLog.logText(`[triage] agent launched, waiting for response...`);
497
+ let result;
498
+ try {
499
+ result = await triage.run({ task: a.task, onOutput: stallDetector.onOutput });
500
+ } finally {
501
+ stallDetector.stop();
502
+ const stats = stallDetector.stats();
503
+ runLog.logText(`[triage] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
504
+ runLog.close();
505
+ }
506
+
507
+ if (!result.ok) {
508
+ sendTrackerLog(server, "triage", "failed");
509
+ throw new Error(result.result?.error || result.summary || "Triage failed");
510
+ }
511
+
512
+ sendTrackerLog(server, "triage", "done");
513
+ return { ok: true, ...result.result, summary: result.summary };
514
+ }
515
+
516
+ export async function handleResearcherDirect(a, server, extra) {
517
+ const config = await buildConfig(a, "researcher");
518
+ const logger = createLogger(config.output.log_level, "mcp");
519
+
520
+ const researcherRole = resolveRole(config, "researcher");
521
+ await assertAgentsAvailable([researcherRole.provider]);
522
+
523
+ const projectDir = await resolveProjectDir(server);
524
+ const runLog = createRunLog(projectDir);
525
+ runLog.logText(`[kj_researcher] started`);
526
+ const emitter = buildDirectEmitter(server, runLog, extra);
527
+ const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
528
+ const onOutput = ({ stream, line }) => {
529
+ emitter.emit("progress", { type: "agent:output", stage: "researcher", message: line, detail: { stream, agent: researcherRole.provider } });
530
+ };
531
+ const stallDetector = createStallDetector({
532
+ onOutput, emitter, eventBase, stage: "researcher", provider: researcherRole.provider
533
+ });
534
+
535
+ const { ResearcherRole } = await import("../roles/researcher-role.js");
536
+ const researcher = new ResearcherRole({ config, logger, emitter });
537
+ await researcher.init({ task: a.task });
538
+
539
+ sendTrackerLog(server, "researcher", "running", researcherRole.provider);
540
+ runLog.logText(`[researcher] agent launched, waiting for response...`);
541
+ let result;
542
+ try {
543
+ result = await researcher.run({ task: a.task, onOutput: stallDetector.onOutput });
544
+ } finally {
545
+ stallDetector.stop();
546
+ const stats = stallDetector.stats();
547
+ runLog.logText(`[researcher] 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, "researcher", "failed");
553
+ throw new Error(result.result?.error || result.summary || "Researcher failed");
554
+ }
555
+
556
+ sendTrackerLog(server, "researcher", "done");
557
+ return { ok: true, ...result.result, summary: result.summary };
558
+ }
559
+
560
+ export async function handleArchitectDirect(a, server, extra) {
561
+ const config = await buildConfig(a, "architect");
562
+ const logger = createLogger(config.output.log_level, "mcp");
563
+
564
+ const architectRole = resolveRole(config, "architect");
565
+ await assertAgentsAvailable([architectRole.provider]);
566
+
567
+ const projectDir = await resolveProjectDir(server);
568
+ const runLog = createRunLog(projectDir);
569
+ runLog.logText(`[kj_architect] started`);
570
+ const emitter = buildDirectEmitter(server, runLog, extra);
571
+ const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
572
+ const onOutput = ({ stream, line }) => {
573
+ emitter.emit("progress", { type: "agent:output", stage: "architect", message: line, detail: { stream, agent: architectRole.provider } });
574
+ };
575
+ const stallDetector = createStallDetector({
576
+ onOutput, emitter, eventBase, stage: "architect", provider: architectRole.provider
577
+ });
578
+
579
+ const { ArchitectRole } = await import("../roles/architect-role.js");
580
+ const architect = new ArchitectRole({ config, logger, emitter });
581
+ await architect.init({ task: a.task });
582
+
583
+ sendTrackerLog(server, "architect", "running", architectRole.provider);
584
+ runLog.logText(`[architect] agent launched, waiting for response...`);
585
+ let result;
586
+ try {
587
+ result = await architect.run({ task: a.task, researchContext: a.context || null, onOutput: stallDetector.onOutput });
588
+ } finally {
589
+ stallDetector.stop();
590
+ const stats = stallDetector.stats();
591
+ runLog.logText(`[architect] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
592
+ runLog.close();
593
+ }
594
+
595
+ if (!result.ok) {
596
+ sendTrackerLog(server, "architect", "failed");
597
+ throw new Error(result.result?.error || result.summary || "Architect failed");
598
+ }
599
+
600
+ sendTrackerLog(server, "architect", "done");
601
+ return { ok: true, ...result.result, summary: result.summary };
602
+ }
603
+
472
604
  /* ── Preflight helpers ─────────────────────────────────────────────── */
473
605
 
474
606
  const AGENT_ROLES = new Set(["coder", "reviewer", "tester", "security", "solomon"]);
@@ -662,6 +794,27 @@ async function handleDiscover(a, server, extra) {
662
794
  return handleDiscoverDirect(a, server, extra);
663
795
  }
664
796
 
797
+ async function handleTriage(a, server, extra) {
798
+ if (!a.task) {
799
+ return failPayload("Missing required field: task");
800
+ }
801
+ return handleTriageDirect(a, server, extra);
802
+ }
803
+
804
+ async function handleResearcher(a, server, extra) {
805
+ if (!a.task) {
806
+ return failPayload("Missing required field: task");
807
+ }
808
+ return handleResearcherDirect(a, server, extra);
809
+ }
810
+
811
+ async function handleArchitect(a, server, extra) {
812
+ if (!a.task) {
813
+ return failPayload("Missing required field: task");
814
+ }
815
+ return handleArchitectDirect(a, server, extra);
816
+ }
817
+
665
818
  /* ── Handler dispatch map ─────────────────────────────────────────── */
666
819
 
667
820
  const toolHandlers = {
@@ -679,7 +832,10 @@ const toolHandlers = {
679
832
  kj_code: (a, server, extra) => handleCode(a, server, extra),
680
833
  kj_review: (a, server, extra) => handleReview(a, server, extra),
681
834
  kj_plan: (a, server, extra) => handlePlan(a, server, extra),
682
- kj_discover: (a, server, extra) => handleDiscover(a, server, extra)
835
+ kj_discover: (a, server, extra) => handleDiscover(a, server, extra),
836
+ kj_triage: (a, server, extra) => handleTriage(a, server, extra),
837
+ kj_researcher: (a, server, extra) => handleResearcher(a, server, extra),
838
+ kj_architect: (a, server, extra) => handleArchitect(a, server, extra)
683
839
  };
684
840
 
685
841
  export async function handleToolCall(name, args, server, extra) {
package/src/mcp/tools.js CHANGED
@@ -93,6 +93,7 @@ export const tools = [
93
93
  checkpointInterval: { type: "number", description: "Minutes between interactive checkpoints (default: 5). Set 0 to disable." },
94
94
  taskType: { type: "string", enum: ["sw", "infra", "doc", "add-tests", "refactor"], description: "Explicit task type for policy resolution. Overrides triage classification." },
95
95
  noSonar: { type: "boolean" },
96
+ enableSonarcloud: { type: "boolean", description: "Enable SonarCloud scan (complementary to SonarQube)" },
96
97
  kjHome: { type: "string" },
97
98
  sonarToken: { type: "string" },
98
99
  timeoutMs: { type: "number" }
@@ -242,5 +243,42 @@ export const tools = [
242
243
  kjHome: { type: "string" }
243
244
  }
244
245
  }
246
+ },
247
+ {
248
+ name: "kj_triage",
249
+ description: "Classify task complexity and recommend which pipeline roles to activate. Returns level (trivial/simple/medium/complex), taskType, recommended roles, and optional decomposition.",
250
+ inputSchema: {
251
+ type: "object",
252
+ required: ["task"],
253
+ properties: {
254
+ task: { type: "string", description: "Task description to classify" },
255
+ kjHome: { type: "string" }
256
+ }
257
+ }
258
+ },
259
+ {
260
+ name: "kj_researcher",
261
+ description: "Research the codebase for a task. Identifies affected files, patterns, constraints, prior decisions, risks, and test coverage.",
262
+ inputSchema: {
263
+ type: "object",
264
+ required: ["task"],
265
+ properties: {
266
+ task: { type: "string", description: "Task description to research" },
267
+ kjHome: { type: "string" }
268
+ }
269
+ }
270
+ },
271
+ {
272
+ name: "kj_architect",
273
+ description: "Design solution architecture for a task. Returns layers, patterns, data model, API contracts, tradeoffs, and a verdict (ready/needs_clarification).",
274
+ inputSchema: {
275
+ type: "object",
276
+ required: ["task"],
277
+ properties: {
278
+ task: { type: "string", description: "Task description to architect" },
279
+ context: { type: "string", description: "Additional context (e.g., researcher output)" },
280
+ kjHome: { type: "string" }
281
+ }
282
+ }
245
283
  }
246
284
  ];
@@ -427,6 +427,47 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
427
427
  return { action: "ok", stageResult };
428
428
  }
429
429
 
430
+ export async function runSonarCloudStage({ config, logger, emitter, eventBase, session, trackBudget, iteration }) {
431
+ logger.setContext({ iteration, stage: "sonarcloud" });
432
+ emitProgress(
433
+ emitter,
434
+ makeEvent("sonarcloud:start", { ...eventBase, stage: "sonarcloud" }, {
435
+ message: "SonarCloud scanning"
436
+ })
437
+ );
438
+
439
+ const { runSonarCloudScan } = await import("../sonar/cloud-scanner.js");
440
+ const scanStart = Date.now();
441
+ const result = await runSonarCloudScan(config);
442
+ trackBudget({ role: "sonarcloud", provider: "sonarcloud", result: { ok: result.ok }, duration_ms: Date.now() - scanStart });
443
+
444
+ await addCheckpoint(session, {
445
+ stage: "sonarcloud",
446
+ iteration,
447
+ project_key: result.projectKey,
448
+ exitCode: result.exitCode,
449
+ provider: "sonarcloud",
450
+ model: null
451
+ });
452
+
453
+ const status = result.ok ? "ok" : "warn";
454
+ const message = result.ok
455
+ ? `SonarCloud scan passed (project: ${result.projectKey})`
456
+ : `SonarCloud scan issue: ${(result.stderr || "").slice(0, 200)}`;
457
+
458
+ emitProgress(
459
+ emitter,
460
+ makeEvent("sonarcloud:end", { ...eventBase, stage: "sonarcloud" }, {
461
+ status,
462
+ message,
463
+ detail: { projectKey: result.projectKey, exitCode: result.exitCode }
464
+ })
465
+ );
466
+
467
+ // SonarCloud is advisory — never blocks the pipeline
468
+ return { action: "ok", stageResult: { ok: result.ok, projectKey: result.projectKey, message } };
469
+ }
470
+
430
471
  async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary, repeatDetector }) {
431
472
  logger.warn(`Reviewer stalled (${repeatCounts.reviewer} repeats). Invoking Solomon mediation.`);
432
473
  emitProgress(
@@ -30,7 +30,7 @@ import { resolveReviewProfile } from "./review/profiles.js";
30
30
  import { CoderRole } from "./roles/coder-role.js";
31
31
  import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
32
32
  import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage } from "./orchestrator/pre-loop-stages.js";
33
- import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
33
+ import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
34
34
  import { runTesterStage, runSecurityStage } from "./orchestrator/post-loop-stages.js";
35
35
  import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js";
36
36
 
@@ -910,6 +910,15 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
910
910
  }
911
911
  }
912
912
 
913
+ if (config.sonarcloud?.enabled) {
914
+ const cloudResult = await runSonarCloudStage({
915
+ config, logger, emitter, eventBase, session, trackBudget, iteration: i
916
+ });
917
+ if (cloudResult.stageResult) {
918
+ stageResults.sonarcloud = cloudResult.stageResult;
919
+ }
920
+ }
921
+
913
922
  return { action: "ok" };
914
923
  }
915
924
 
@@ -0,0 +1,76 @@
1
+ import { runCommand } from "../utils/process.js";
2
+ import { resolveSonarProjectKey } from "./project-key.js";
3
+
4
+ function buildCloudScannerArgs(projectKey, config) {
5
+ const sc = config.sonarcloud || {};
6
+ const scanner = sc.scanner || {};
7
+ const host = sc.host || "https://sonarcloud.io";
8
+ const token = process.env.KJ_SONARCLOUD_TOKEN || sc.token;
9
+ const organization = process.env.KJ_SONARCLOUD_ORG || sc.organization;
10
+
11
+ const args = [
12
+ `-Dsonar.host.url=${host}`,
13
+ `-Dsonar.projectKey=${projectKey}`
14
+ ];
15
+
16
+ if (token) args.push(`-Dsonar.token=${token}`);
17
+ if (organization) args.push(`-Dsonar.organization=${organization}`);
18
+ if (scanner.sources) args.push(`-Dsonar.sources=${scanner.sources}`);
19
+ if (scanner.exclusions) args.push(`-Dsonar.exclusions=${scanner.exclusions}`);
20
+ if (scanner.test_inclusions) args.push(`-Dsonar.test.inclusions=${scanner.test_inclusions}`);
21
+
22
+ return args;
23
+ }
24
+
25
+ export async function runSonarCloudScan(config, projectKey = null) {
26
+ const sc = config.sonarcloud || {};
27
+ const token = process.env.KJ_SONARCLOUD_TOKEN || sc.token;
28
+ const organization = sc.organization || process.env.KJ_SONARCLOUD_ORG;
29
+
30
+ if (!token) {
31
+ return {
32
+ ok: false,
33
+ projectKey: null,
34
+ stdout: "",
35
+ stderr: "SonarCloud token not configured. Set sonarcloud.token in kj.config.yml or KJ_SONARCLOUD_TOKEN env var.",
36
+ exitCode: 1
37
+ };
38
+ }
39
+
40
+ if (!organization) {
41
+ return {
42
+ ok: false,
43
+ projectKey: null,
44
+ stdout: "",
45
+ stderr: "SonarCloud organization not configured. Set sonarcloud.organization in kj.config.yml or KJ_SONARCLOUD_ORG env var.",
46
+ exitCode: 1
47
+ };
48
+ }
49
+
50
+ let effectiveProjectKey;
51
+ try {
52
+ effectiveProjectKey = projectKey || sc.project_key || await resolveSonarProjectKey(config, { projectKey });
53
+ } catch (error) {
54
+ return {
55
+ ok: false,
56
+ projectKey: null,
57
+ stdout: "",
58
+ stderr: error?.message || String(error),
59
+ exitCode: 1
60
+ };
61
+ }
62
+
63
+ const scannerTimeout = 15 * 60 * 1000;
64
+ const args = buildCloudScannerArgs(effectiveProjectKey, config);
65
+
66
+ // Use npx @sonar/scan (no Docker needed)
67
+ const result = await runCommand("npx", ["@sonar/scan", ...args], { timeout: scannerTimeout });
68
+
69
+ return {
70
+ ok: result.exitCode === 0,
71
+ projectKey: effectiveProjectKey,
72
+ stdout: result.stdout,
73
+ stderr: result.stderr,
74
+ exitCode: result.exitCode
75
+ };
76
+ }