karajan-code 1.20.0 → 1.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.20.0",
3
+ "version": "1.21.1",
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
@@ -97,6 +97,8 @@ program
97
97
  .option("--methodology <name>")
98
98
  .option("--no-auto-rebase")
99
99
  .option("--no-sonar")
100
+ .option("--enable-sonarcloud", "Enable SonarCloud scan (complementary to SonarQube)")
101
+ .option("--no-sonarcloud")
100
102
  .option("--checkpoint-interval <n>", "Minutes between interactive checkpoints (default: 5)")
101
103
  .option("--pg-task <cardId>", "Planning Game card ID (e.g., KJC-TSK-0042)")
102
104
  .option("--pg-project <projectId>", "Planning Game project ID")
@@ -3,56 +3,53 @@ import { assertAgentsAvailable } from "../agents/availability.js";
3
3
  import { resolveRole } from "../config.js";
4
4
  import { buildArchitectPrompt, parseArchitectOutput } from "../prompts/architect.js";
5
5
 
6
- function formatArchitect(result) {
7
- const lines = [];
8
- lines.push(`## Architecture Design`);
9
- lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
6
+ function formatLayers(layers, lines) {
7
+ lines.push("### Layers");
8
+ for (const l of layers) {
9
+ lines.push(typeof l === "string" ? `- ${l}` : `- **${l.name}**: ${l.responsibility || ""}`);
10
+ }
11
+ lines.push("");
12
+ }
10
13
 
11
- const arch = result.architecture;
12
- if (arch) {
13
- if (arch.type) lines.push(`**Type:** ${arch.type}`, "");
14
+ function formatTradeoffs(tradeoffs, lines) {
15
+ lines.push("### Tradeoffs");
16
+ for (const t of tradeoffs) {
17
+ lines.push(`- **${t.decision}**: ${t.rationale || ""}`);
18
+ if (t.alternatives?.length) lines.push(` Alternatives: ${t.alternatives.join(", ")}`);
19
+ }
20
+ lines.push("");
21
+ }
14
22
 
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
- }
23
+ function formatApiContracts(contracts, lines) {
24
+ lines.push("### API Contracts");
25
+ for (const c of contracts) {
26
+ lines.push(`- \`${c.method || "GET"} ${c.endpoint}\``);
27
+ }
28
+ lines.push("");
29
+ }
26
30
 
27
- if (arch.patterns?.length) {
28
- lines.push("### Patterns");
29
- for (const p of arch.patterns) lines.push(`- ${p}`);
30
- lines.push("");
31
- }
31
+ function formatArchitecture(arch, lines) {
32
+ if (arch.type) lines.push(`**Type:** ${arch.type}`, "");
33
+ if (arch.layers?.length) formatLayers(arch.layers, lines);
34
+ if (arch.patterns?.length) {
35
+ lines.push("### Patterns");
36
+ for (const p of arch.patterns) lines.push(`- ${p}`);
37
+ lines.push("");
38
+ }
39
+ if (arch.tradeoffs?.length) formatTradeoffs(arch.tradeoffs, lines);
40
+ if (arch.apiContracts?.length) formatApiContracts(arch.apiContracts, lines);
41
+ }
32
42
 
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
- }
43
+ function formatArchitect(result) {
44
+ const lines = [];
45
+ lines.push(`## Architecture Design`);
46
+ lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
41
47
 
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
- }
48
+ if (result.architecture) formatArchitecture(result.architecture, lines);
50
49
 
51
50
  if (result.questions?.length) {
52
51
  lines.push("### Clarification Questions");
53
- for (const q of result.questions) {
54
- lines.push(`- ${q.question || q}`);
55
- }
52
+ for (const q of result.questions) lines.push(`- ${q.question || q}`);
56
53
  lines.push("");
57
54
  }
58
55
 
@@ -2,57 +2,61 @@ import { createAgent } from "../agents/index.js";
2
2
  import { assertAgentsAvailable } from "../agents/availability.js";
3
3
  import { resolveRole } from "../config.js";
4
4
  import { buildDiscoverPrompt, parseDiscoverOutput } from "../prompts/discover.js";
5
- import { parseMaybeJsonString } from "../review/parser.js";
6
5
 
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("");
6
+ function formatGaps(gaps, lines) {
7
+ lines.push("## Gaps");
8
+ for (const g of gaps) {
9
+ const sev = g.severity ? ` [${g.severity}]` : "";
10
+ lines.push(`- ${g.description || g}${sev}`);
11
+ if (g.suggestedQuestion) lines.push(` → ${g.suggestedQuestion}`);
20
12
  }
13
+ lines.push("");
14
+ }
21
15
 
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("");
16
+ function formatMomTest(questions, lines) {
17
+ lines.push("## Mom Test Questions");
18
+ for (const q of questions) {
19
+ lines.push(`- ${q.question || q}`);
20
+ if (q.rationale) lines.push(` _${q.rationale}_`);
29
21
  }
22
+ lines.push("");
23
+ }
30
24
 
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("");
25
+ function formatWendel(checklist, lines) {
26
+ lines.push("## Wendel Checklist");
27
+ for (const w of checklist) {
28
+ const icon = w.status === "pass" ? "✓" : w.status === "fail" ? "✗" : "?";
29
+ lines.push(`- [${icon}] ${w.condition}: ${w.justification || ""}`);
38
30
  }
31
+ lines.push("");
32
+ }
39
33
 
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
- }
34
+ function formatClassification(classification, lines) {
35
+ lines.push("## Classification");
36
+ lines.push(`- Type: ${classification.type}`);
37
+ if (classification.adoptionRisk) lines.push(`- Adoption risk: ${classification.adoptionRisk}`);
38
+ if (classification.frictionEstimate) lines.push(`- Friction: ${classification.frictionEstimate}`);
39
+ lines.push("");
40
+ }
47
41
 
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("");
42
+ function formatJtbds(jtbds, lines) {
43
+ lines.push("## Jobs-to-be-Done");
44
+ for (const j of jtbds) {
45
+ lines.push(`- **${j.id || ""}**: ${j.functional || j}`);
54
46
  }
47
+ lines.push("");
48
+ }
49
+
50
+ function formatDiscover(result, mode) {
51
+ const lines = [];
52
+ lines.push(`## Discovery (${mode})`);
53
+ lines.push(`**Verdict:** ${result.verdict || "unknown"}`, "");
55
54
 
55
+ if (result.gaps?.length) formatGaps(result.gaps, lines);
56
+ if (result.momTestQuestions?.length) formatMomTest(result.momTestQuestions, lines);
57
+ if (result.wendelChecklist?.length) formatWendel(result.wendelChecklist, lines);
58
+ if (result.classification) formatClassification(result.classification, lines);
59
+ if (result.jtbds?.length) formatJtbds(result.jtbds, lines);
56
60
  if (result.summary) lines.push(`---\n${result.summary}`);
57
61
  return lines.join("\n");
58
62
  }
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 },
@@ -129,6 +141,7 @@ const DEFAULTS = {
129
141
  max_reviewer_retries: 3,
130
142
  max_tester_retries: 1,
131
143
  max_security_retries: 1,
144
+ max_auto_resumes: 2,
132
145
  expiry_days: 30
133
146
  },
134
147
  failFast: {
@@ -347,6 +360,9 @@ function applyBecariaOverride(out, flags) {
347
360
 
348
361
  function applyMiscOverrides(out, flags) {
349
362
  if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
363
+ out.sonarcloud = out.sonarcloud || {};
364
+ if (flags.enableSonarcloud === true) out.sonarcloud.enabled = true;
365
+ if (flags.noSonarcloud === true || flags.sonarcloud === false) out.sonarcloud.enabled = false;
350
366
 
351
367
  out.planning_game = out.planning_game || {};
352
368
  if (flags.pgTask) out.planning_game.enabled = true;
@@ -171,6 +171,69 @@ export function buildAskQuestion(server) {
171
171
  };
172
172
  }
173
173
 
174
+ const MAX_AUTO_RESUMES = 2;
175
+ const NON_RECOVERABLE_CATEGORIES = new Set([
176
+ "config_error", "auth_error", "agent_missing", "branch_error", "git_error"
177
+ ]);
178
+
179
+ async function attemptAutoResume({ err, config, logger, emitter, askQuestion, runLog }) {
180
+ const { category } = classifyError(err);
181
+ if (NON_RECOVERABLE_CATEGORIES.has(category)) return null;
182
+
183
+ // Find session ID from most recent session file
184
+ const { loadMostRecentSession } = await import("../session-store.js");
185
+ let session;
186
+ try {
187
+ session = await loadMostRecentSession();
188
+ } catch {
189
+ return null;
190
+ }
191
+ if (!session || !["failed", "stopped"].includes(session.status)) return null;
192
+
193
+ const maxRetries = config.session?.max_auto_resumes ?? MAX_AUTO_RESUMES;
194
+ const autoResumeCount = session.auto_resume_count || 0;
195
+ if (autoResumeCount >= maxRetries) {
196
+ runLog.logText(`[resilient] auto-resume limit reached (${maxRetries}), giving up`);
197
+ return null;
198
+ }
199
+
200
+ runLog.logText(`[resilient] run failed (${category}), auto-resuming (${autoResumeCount + 1}/${maxRetries})...`);
201
+ emitter.emit("progress", {
202
+ type: "resilient:auto_resume",
203
+ attempt: autoResumeCount + 1,
204
+ maxRetries,
205
+ errorCategory: category,
206
+ sessionId: session.id
207
+ });
208
+
209
+ // Increment counter and save before resuming
210
+ const { saveSession } = await import("../session-store.js");
211
+ session.auto_resume_count = autoResumeCount + 1;
212
+ await saveSession(session);
213
+
214
+ try {
215
+ const result = await resumeFlow({
216
+ sessionId: session.id,
217
+ config,
218
+ logger,
219
+ flags: {},
220
+ emitter,
221
+ askQuestion
222
+ });
223
+ const ok = !result.paused && (result.approved !== false);
224
+ runLog.logText(`[resilient] auto-resume ${ok ? "succeeded" : "finished"} — ok=${ok}`);
225
+ return { ok, ...result, autoResumed: true, autoResumeAttempt: autoResumeCount + 1 };
226
+ } catch (error) {
227
+ // Recursive: try again if still within limits
228
+ const nestedResult = await attemptAutoResume({
229
+ err: error, config, logger, emitter, askQuestion, runLog
230
+ });
231
+ if (nestedResult) return nestedResult;
232
+ runLog.logText(`[resilient] auto-resume failed: ${error.message}`);
233
+ return null;
234
+ }
235
+ }
236
+
174
237
  export async function handleRunDirect(a, server, extra) {
175
238
  const config = await buildConfig(a);
176
239
  await assertNotOnBaseBranch(config);
@@ -209,6 +272,12 @@ export async function handleRunDirect(a, server, extra) {
209
272
  const result = await runFlow({ task: a.task, config, logger, flags: a, emitter, askQuestion, pgTaskId, pgProject });
210
273
  runLog.logText(`[kj_run] finished — ok=${!result.paused && (result.approved !== false)}`);
211
274
  return { ok: !result.paused && (result.approved !== false), ...result };
275
+ } catch (err) {
276
+ const autoResumeResult = await attemptAutoResume({
277
+ err, config, logger, emitter, askQuestion, runLog, progressNotifier, extra
278
+ });
279
+ if (autoResumeResult) return autoResumeResult;
280
+ throw err;
212
281
  } finally {
213
282
  runLog.close();
214
283
  }
@@ -469,6 +538,138 @@ export async function handleDiscoverDirect(a, server, extra) {
469
538
  return { ok: true, ...result.result, summary: result.summary };
470
539
  }
471
540
 
541
+ export async function handleTriageDirect(a, server, extra) {
542
+ const config = await buildConfig(a, "triage");
543
+ const logger = createLogger(config.output.log_level, "mcp");
544
+
545
+ const triageRole = resolveRole(config, "triage");
546
+ await assertAgentsAvailable([triageRole.provider]);
547
+
548
+ const projectDir = await resolveProjectDir(server);
549
+ const runLog = createRunLog(projectDir);
550
+ runLog.logText(`[kj_triage] started`);
551
+ const emitter = buildDirectEmitter(server, runLog, extra);
552
+ const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
553
+ const onOutput = ({ stream, line }) => {
554
+ emitter.emit("progress", { type: "agent:output", stage: "triage", message: line, detail: { stream, agent: triageRole.provider } });
555
+ };
556
+ const stallDetector = createStallDetector({
557
+ onOutput, emitter, eventBase, stage: "triage", provider: triageRole.provider
558
+ });
559
+
560
+ const { TriageRole } = await import("../roles/triage-role.js");
561
+ const triage = new TriageRole({ config, logger, emitter });
562
+ await triage.init({ task: a.task });
563
+
564
+ sendTrackerLog(server, "triage", "running", triageRole.provider);
565
+ runLog.logText(`[triage] agent launched, waiting for response...`);
566
+ let result;
567
+ try {
568
+ result = await triage.run({ task: a.task, onOutput: stallDetector.onOutput });
569
+ } finally {
570
+ stallDetector.stop();
571
+ const stats = stallDetector.stats();
572
+ runLog.logText(`[triage] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
573
+ runLog.close();
574
+ }
575
+
576
+ if (!result.ok) {
577
+ sendTrackerLog(server, "triage", "failed");
578
+ throw new Error(result.result?.error || result.summary || "Triage failed");
579
+ }
580
+
581
+ sendTrackerLog(server, "triage", "done");
582
+ return { ok: true, ...result.result, summary: result.summary };
583
+ }
584
+
585
+ export async function handleResearcherDirect(a, server, extra) {
586
+ const config = await buildConfig(a, "researcher");
587
+ const logger = createLogger(config.output.log_level, "mcp");
588
+
589
+ const researcherRole = resolveRole(config, "researcher");
590
+ await assertAgentsAvailable([researcherRole.provider]);
591
+
592
+ const projectDir = await resolveProjectDir(server);
593
+ const runLog = createRunLog(projectDir);
594
+ runLog.logText(`[kj_researcher] started`);
595
+ const emitter = buildDirectEmitter(server, runLog, extra);
596
+ const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
597
+ const onOutput = ({ stream, line }) => {
598
+ emitter.emit("progress", { type: "agent:output", stage: "researcher", message: line, detail: { stream, agent: researcherRole.provider } });
599
+ };
600
+ const stallDetector = createStallDetector({
601
+ onOutput, emitter, eventBase, stage: "researcher", provider: researcherRole.provider
602
+ });
603
+
604
+ const { ResearcherRole } = await import("../roles/researcher-role.js");
605
+ const researcher = new ResearcherRole({ config, logger, emitter });
606
+ await researcher.init({ task: a.task });
607
+
608
+ sendTrackerLog(server, "researcher", "running", researcherRole.provider);
609
+ runLog.logText(`[researcher] agent launched, waiting for response...`);
610
+ let result;
611
+ try {
612
+ result = await researcher.run({ task: a.task, onOutput: stallDetector.onOutput });
613
+ } finally {
614
+ stallDetector.stop();
615
+ const stats = stallDetector.stats();
616
+ runLog.logText(`[researcher] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
617
+ runLog.close();
618
+ }
619
+
620
+ if (!result.ok) {
621
+ sendTrackerLog(server, "researcher", "failed");
622
+ throw new Error(result.result?.error || result.summary || "Researcher failed");
623
+ }
624
+
625
+ sendTrackerLog(server, "researcher", "done");
626
+ return { ok: true, ...result.result, summary: result.summary };
627
+ }
628
+
629
+ export async function handleArchitectDirect(a, server, extra) {
630
+ const config = await buildConfig(a, "architect");
631
+ const logger = createLogger(config.output.log_level, "mcp");
632
+
633
+ const architectRole = resolveRole(config, "architect");
634
+ await assertAgentsAvailable([architectRole.provider]);
635
+
636
+ const projectDir = await resolveProjectDir(server);
637
+ const runLog = createRunLog(projectDir);
638
+ runLog.logText(`[kj_architect] started`);
639
+ const emitter = buildDirectEmitter(server, runLog, extra);
640
+ const eventBase = { sessionId: null, iteration: 0, startedAt: Date.now() };
641
+ const onOutput = ({ stream, line }) => {
642
+ emitter.emit("progress", { type: "agent:output", stage: "architect", message: line, detail: { stream, agent: architectRole.provider } });
643
+ };
644
+ const stallDetector = createStallDetector({
645
+ onOutput, emitter, eventBase, stage: "architect", provider: architectRole.provider
646
+ });
647
+
648
+ const { ArchitectRole } = await import("../roles/architect-role.js");
649
+ const architect = new ArchitectRole({ config, logger, emitter });
650
+ await architect.init({ task: a.task });
651
+
652
+ sendTrackerLog(server, "architect", "running", architectRole.provider);
653
+ runLog.logText(`[architect] agent launched, waiting for response...`);
654
+ let result;
655
+ try {
656
+ result = await architect.run({ task: a.task, researchContext: a.context || null, onOutput: stallDetector.onOutput });
657
+ } finally {
658
+ stallDetector.stop();
659
+ const stats = stallDetector.stats();
660
+ runLog.logText(`[architect] finished — lines=${stats.lineCount}, bytes=${stats.bytesReceived}, elapsed=${Math.round(stats.elapsedMs / 1000)}s`);
661
+ runLog.close();
662
+ }
663
+
664
+ if (!result.ok) {
665
+ sendTrackerLog(server, "architect", "failed");
666
+ throw new Error(result.result?.error || result.summary || "Architect failed");
667
+ }
668
+
669
+ sendTrackerLog(server, "architect", "done");
670
+ return { ok: true, ...result.result, summary: result.summary };
671
+ }
672
+
472
673
  /* ── Preflight helpers ─────────────────────────────────────────────── */
473
674
 
474
675
  const AGENT_ROLES = new Set(["coder", "reviewer", "tester", "security", "solomon"]);
@@ -662,6 +863,27 @@ async function handleDiscover(a, server, extra) {
662
863
  return handleDiscoverDirect(a, server, extra);
663
864
  }
664
865
 
866
+ async function handleTriage(a, server, extra) {
867
+ if (!a.task) {
868
+ return failPayload("Missing required field: task");
869
+ }
870
+ return handleTriageDirect(a, server, extra);
871
+ }
872
+
873
+ async function handleResearcher(a, server, extra) {
874
+ if (!a.task) {
875
+ return failPayload("Missing required field: task");
876
+ }
877
+ return handleResearcherDirect(a, server, extra);
878
+ }
879
+
880
+ async function handleArchitect(a, server, extra) {
881
+ if (!a.task) {
882
+ return failPayload("Missing required field: task");
883
+ }
884
+ return handleArchitectDirect(a, server, extra);
885
+ }
886
+
665
887
  /* ── Handler dispatch map ─────────────────────────────────────────── */
666
888
 
667
889
  const toolHandlers = {
@@ -679,7 +901,10 @@ const toolHandlers = {
679
901
  kj_code: (a, server, extra) => handleCode(a, server, extra),
680
902
  kj_review: (a, server, extra) => handleReview(a, server, extra),
681
903
  kj_plan: (a, server, extra) => handlePlan(a, server, extra),
682
- kj_discover: (a, server, extra) => handleDiscover(a, server, extra)
904
+ kj_discover: (a, server, extra) => handleDiscover(a, server, extra),
905
+ kj_triage: (a, server, extra) => handleTriage(a, server, extra),
906
+ kj_researcher: (a, server, extra) => handleResearcher(a, server, extra),
907
+ kj_architect: (a, server, extra) => handleArchitect(a, server, extra)
683
908
  };
684
909
 
685
910
  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
 
@@ -63,6 +63,24 @@ export async function pauseSession(session, { question, context: pauseContext })
63
63
  await saveSession(session);
64
64
  }
65
65
 
66
+ export async function loadMostRecentSession() {
67
+ let entries;
68
+ try {
69
+ entries = await fs.readdir(SESSION_ROOT, { withFileTypes: true });
70
+ } catch {
71
+ return null;
72
+ }
73
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
74
+ for (let i = dirs.length - 1; i >= 0; i--) {
75
+ try {
76
+ return await loadSession(dirs[i]);
77
+ } catch {
78
+ continue;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
66
84
  export async function resumeSessionWithAnswer(sessionId, answer) {
67
85
  const session = await loadSession(sessionId);
68
86
  if (session.status !== "paused") {
@@ -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
+ }
@@ -183,13 +183,15 @@ export function readRunLog(projectDir, maxLines = 50) {
183
183
  const total = lines.length;
184
184
  const shown = lines.slice(-maxLines);
185
185
  const status = parseRunStatus(lines);
186
+ const MAX_LINE_CHARS = 2000;
187
+ const truncated = shown.map(l => l.length > MAX_LINE_CHARS ? l.slice(0, MAX_LINE_CHARS) + "… [truncated]" : l);
186
188
  return {
187
189
  ok: true,
188
190
  path: logPath,
189
191
  totalLines: total,
190
192
  status,
191
- lines: shown,
192
- summary: shown.join("\n")
193
+ lines: truncated,
194
+ summary: truncated.join("\n")
193
195
  };
194
196
  } catch (err) {
195
197
  return {