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 +1 -1
- package/src/cli.js +2 -0
- package/src/commands/architect.js +39 -42
- package/src/commands/discover.js +45 -41
- package/src/config.js +16 -0
- package/src/mcp/server-handlers.js +226 -1
- package/src/mcp/tools.js +38 -0
- package/src/orchestrator/iteration-stages.js +41 -0
- package/src/orchestrator.js +10 -1
- package/src/session-store.js +18 -0
- package/src/sonar/cloud-scanner.js +76 -0
- package/src/utils/run-log.js +4 -2
package/package.json
CHANGED
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
package/src/commands/discover.js
CHANGED
|
@@ -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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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:
|
|
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(
|
package/src/orchestrator.js
CHANGED
|
@@ -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
|
|
package/src/session-store.js
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/run-log.js
CHANGED
|
@@ -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:
|
|
192
|
-
summary:
|
|
193
|
+
lines: truncated,
|
|
194
|
+
summary: truncated.join("\n")
|
|
193
195
|
};
|
|
194
196
|
} catch (err) {
|
|
195
197
|
return {
|