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 +1 -1
- package/src/cli.js +59 -0
- package/src/commands/architect.js +90 -0
- package/src/commands/discover.js +87 -0
- package/src/commands/researcher.js +40 -0
- package/src/commands/triage.js +63 -0
- package/src/config.js +15 -0
- package/src/mcp/server-handlers.js +157 -1
- package/src/mcp/tools.js +38 -0
- package/src/orchestrator/iteration-stages.js +41 -0
- package/src/orchestrator.js +10 -1
- package/src/sonar/cloud-scanner.js +76 -0
package/package.json
CHANGED
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:
|
|
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(
|
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
|
|
|
@@ -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
|
+
}
|