karajan-code 1.2.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/LICENSE +21 -0
- package/README.md +441 -0
- package/docs/karajan-code-logo-small.png +0 -0
- package/package.json +60 -0
- package/scripts/install.js +898 -0
- package/scripts/install.sh +7 -0
- package/scripts/postinstall.js +117 -0
- package/scripts/setup-multi-instance.sh +150 -0
- package/src/activity-log.js +59 -0
- package/src/agents/aider-agent.js +25 -0
- package/src/agents/availability.js +32 -0
- package/src/agents/base-agent.js +27 -0
- package/src/agents/claude-agent.js +24 -0
- package/src/agents/codex-agent.js +27 -0
- package/src/agents/gemini-agent.js +25 -0
- package/src/agents/index.js +19 -0
- package/src/agents/resolve-bin.js +60 -0
- package/src/cli.js +200 -0
- package/src/commands/code.js +32 -0
- package/src/commands/config.js +74 -0
- package/src/commands/doctor.js +155 -0
- package/src/commands/init.js +181 -0
- package/src/commands/plan.js +67 -0
- package/src/commands/report.js +340 -0
- package/src/commands/resume.js +39 -0
- package/src/commands/review.js +26 -0
- package/src/commands/roles.js +117 -0
- package/src/commands/run.js +91 -0
- package/src/commands/scan.js +18 -0
- package/src/commands/sonar.js +53 -0
- package/src/config.js +322 -0
- package/src/git/automation.js +100 -0
- package/src/mcp/progress.js +69 -0
- package/src/mcp/run-kj.js +87 -0
- package/src/mcp/server-handlers.js +259 -0
- package/src/mcp/server.js +37 -0
- package/src/mcp/tool-arg-normalizers.js +16 -0
- package/src/mcp/tools.js +184 -0
- package/src/orchestrator.js +1277 -0
- package/src/planning-game/adapter.js +105 -0
- package/src/planning-game/client.js +81 -0
- package/src/prompts/coder.js +60 -0
- package/src/prompts/planner.js +26 -0
- package/src/prompts/reviewer.js +45 -0
- package/src/repeat-detector.js +77 -0
- package/src/review/diff-generator.js +22 -0
- package/src/review/parser.js +93 -0
- package/src/review/profiles.js +66 -0
- package/src/review/schema.js +31 -0
- package/src/review/tdd-policy.js +57 -0
- package/src/roles/base-role.js +127 -0
- package/src/roles/coder-role.js +60 -0
- package/src/roles/commiter-role.js +94 -0
- package/src/roles/index.js +12 -0
- package/src/roles/planner-role.js +81 -0
- package/src/roles/refactorer-role.js +66 -0
- package/src/roles/researcher-role.js +134 -0
- package/src/roles/reviewer-role.js +132 -0
- package/src/roles/security-role.js +128 -0
- package/src/roles/solomon-role.js +199 -0
- package/src/roles/sonar-role.js +65 -0
- package/src/roles/tester-role.js +114 -0
- package/src/roles/triage-role.js +128 -0
- package/src/session-store.js +80 -0
- package/src/sonar/api.js +78 -0
- package/src/sonar/enforcer.js +19 -0
- package/src/sonar/manager.js +163 -0
- package/src/sonar/project-key.js +83 -0
- package/src/sonar/scanner.js +267 -0
- package/src/utils/agent-detect.js +32 -0
- package/src/utils/budget.js +123 -0
- package/src/utils/display.js +346 -0
- package/src/utils/events.js +23 -0
- package/src/utils/fs.js +19 -0
- package/src/utils/git.js +101 -0
- package/src/utils/logger.js +86 -0
- package/src/utils/paths.js +18 -0
- package/src/utils/pricing.js +28 -0
- package/src/utils/process.js +67 -0
- package/src/utils/wizard.js +41 -0
- package/templates/coder-rules.md +24 -0
- package/templates/docker-compose.sonar.yml +60 -0
- package/templates/kj.config.yml +82 -0
- package/templates/review-rules.md +11 -0
- package/templates/roles/coder.md +42 -0
- package/templates/roles/commiter.md +44 -0
- package/templates/roles/planner.md +45 -0
- package/templates/roles/refactorer.md +39 -0
- package/templates/roles/researcher.md +37 -0
- package/templates/roles/reviewer-paranoid.md +38 -0
- package/templates/roles/reviewer-relaxed.md +34 -0
- package/templates/roles/reviewer-strict.md +37 -0
- package/templates/roles/reviewer.md +55 -0
- package/templates/roles/security.md +54 -0
- package/templates/roles/solomon.md +106 -0
- package/templates/roles/sonar.md +49 -0
- package/templates/roles/tester.md +41 -0
- package/templates/roles/triage.md +25 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
|
|
4
|
+
const SUBAGENT_PREAMBLE = [
|
|
5
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
6
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
7
|
+
"Do NOT use any MCP tools. Focus only on auditing code for security vulnerabilities."
|
|
8
|
+
].join(" ");
|
|
9
|
+
|
|
10
|
+
function resolveProvider(config) {
|
|
11
|
+
return (
|
|
12
|
+
config?.roles?.security?.provider ||
|
|
13
|
+
config?.roles?.coder?.provider ||
|
|
14
|
+
"claude"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildPrompt({ task, diff, instructions }) {
|
|
19
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
20
|
+
|
|
21
|
+
if (instructions) {
|
|
22
|
+
sections.push(instructions);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
sections.push(
|
|
26
|
+
"You are a security auditor. Analyze the code changes for vulnerabilities.",
|
|
27
|
+
"Check for: OWASP top 10, exposed secrets/API keys, hardcoded credentials, command injection, XSS, SQL injection, path traversal, prototype pollution, insecure dependencies.",
|
|
28
|
+
"Return a single valid JSON object with your findings and nothing else.",
|
|
29
|
+
'JSON schema: {"vulnerabilities":[{"severity":"critical|high|medium|low","category":string,"file":string,"line":number,"description":string,"fix_suggestion":string}],"verdict":"pass"|"fail"}'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
sections.push(`## Task\n${task}`);
|
|
33
|
+
|
|
34
|
+
if (diff) {
|
|
35
|
+
sections.push(`## Git diff to audit\n${diff}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return sections.join("\n\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseSecurityOutput(raw) {
|
|
42
|
+
const text = raw?.trim() || "";
|
|
43
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
44
|
+
if (!jsonMatch) return null;
|
|
45
|
+
return JSON.parse(jsonMatch[0]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildSummary(parsed) {
|
|
49
|
+
const vulns = parsed.vulnerabilities || [];
|
|
50
|
+
if (vulns.length === 0) {
|
|
51
|
+
return `Verdict: ${parsed.verdict || "pass"}; No vulnerabilities found`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const bySeverity = {};
|
|
55
|
+
for (const v of vulns) {
|
|
56
|
+
const sev = v.severity || "unknown";
|
|
57
|
+
bySeverity[sev] = (bySeverity[sev] || 0) + 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parts = Object.entries(bySeverity)
|
|
61
|
+
.sort(([a], [b]) => {
|
|
62
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
63
|
+
return (order[a] ?? 4) - (order[b] ?? 4);
|
|
64
|
+
})
|
|
65
|
+
.map(([sev, count]) => `${count} ${sev}`);
|
|
66
|
+
|
|
67
|
+
return `Verdict: ${parsed.verdict || "fail"}; ${parts.join(", ")}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class SecurityRole extends BaseRole {
|
|
71
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
72
|
+
super({ name: "security", config, logger, emitter });
|
|
73
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async execute(input) {
|
|
77
|
+
const { task, diff } = typeof input === "string"
|
|
78
|
+
? { task: input, diff: null }
|
|
79
|
+
: { task: input?.task || this.context?.task || "", diff: input?.diff || null };
|
|
80
|
+
|
|
81
|
+
const provider = resolveProvider(this.config);
|
|
82
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
83
|
+
|
|
84
|
+
const prompt = buildPrompt({ task, diff, instructions: this.instructions });
|
|
85
|
+
const result = await agent.runTask({ prompt, role: "security" });
|
|
86
|
+
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
result: {
|
|
91
|
+
error: result.error || result.output || "Security audit failed",
|
|
92
|
+
provider
|
|
93
|
+
},
|
|
94
|
+
summary: `Security audit failed: ${result.error || "unknown error"}`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const parsed = parseSecurityOutput(result.output);
|
|
100
|
+
if (!parsed) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
result: { error: "Failed to parse security output: no JSON found", provider },
|
|
104
|
+
summary: "Security output parse error: no JSON found"
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const verdict = parsed.verdict || (parsed.vulnerabilities?.length ? "fail" : "pass");
|
|
109
|
+
const ok = verdict === "pass";
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
ok,
|
|
113
|
+
result: {
|
|
114
|
+
vulnerabilities: parsed.vulnerabilities || [],
|
|
115
|
+
verdict,
|
|
116
|
+
provider
|
|
117
|
+
},
|
|
118
|
+
summary: buildSummary({ ...parsed, verdict })
|
|
119
|
+
};
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
result: { error: `Failed to parse security output: ${err.message}`, provider },
|
|
124
|
+
summary: `Security output parse error: ${err.message}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
|
|
4
|
+
const SUBAGENT_PREAMBLE = [
|
|
5
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
6
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
7
|
+
"Do NOT use any MCP tools. Focus only on resolving the conflict between agents."
|
|
8
|
+
].join(" ");
|
|
9
|
+
|
|
10
|
+
function resolveProvider(config) {
|
|
11
|
+
return (
|
|
12
|
+
config?.roles?.solomon?.provider ||
|
|
13
|
+
config?.roles?.coder?.provider ||
|
|
14
|
+
"claude"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatHistory(history) {
|
|
19
|
+
if (!history || history.length === 0) return "No previous interactions recorded.";
|
|
20
|
+
|
|
21
|
+
return history
|
|
22
|
+
.map((entry, i) => {
|
|
23
|
+
const agent = entry.agent || "unknown";
|
|
24
|
+
const feedback = entry.feedback || entry.message || "(no feedback)";
|
|
25
|
+
return `### Interaction ${i + 1} [${agent}]\n${feedback}`;
|
|
26
|
+
})
|
|
27
|
+
.join("\n\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildPrompt({ conflict, task, instructions }) {
|
|
31
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
32
|
+
|
|
33
|
+
if (instructions) {
|
|
34
|
+
sections.push(instructions);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
sections.push(
|
|
38
|
+
"You are Solomon, the conflict resolver in a multi-role AI pipeline.",
|
|
39
|
+
"You are activated when agents cannot reach agreement after their iteration limit."
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
sections.push(
|
|
43
|
+
"## Decision hierarchy",
|
|
44
|
+
"Security > Correctness > Tests > Architecture > Maintainability > Style",
|
|
45
|
+
"- Green tests are sacred. Never dismiss a failing test.",
|
|
46
|
+
"- Style preferences NEVER block approval.",
|
|
47
|
+
"- Hardcoded values that will come from DB later are acceptable (contextual false positive).",
|
|
48
|
+
"- Sonar INFO/MINOR issues are always dismissable.",
|
|
49
|
+
"- Sonar BLOCKER/CRITICAL must be fixed unless they are proven false positives."
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
sections.push(
|
|
53
|
+
"## Classification rules",
|
|
54
|
+
"For each issue, classify as:",
|
|
55
|
+
"1. **critical** (security, correctness, tests broken) — action: must_fix",
|
|
56
|
+
"2. **important** (architecture, maintainability) — action: should_fix",
|
|
57
|
+
"3. **style** (naming, formatting, preferences, false positives) — action: dismiss"
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
sections.push(
|
|
61
|
+
"## Your decision options",
|
|
62
|
+
'1. **approve** — All pending issues are style/false positives. Pipeline continues.',
|
|
63
|
+
'2. **approve_with_conditions** — Important issues exist but are fixable. Give exact instructions to Coder for one more attempt.',
|
|
64
|
+
'3. **escalate_human** — Critical issues that cannot be resolved, ambiguous requirements, architecture decisions, or business logic decisions.',
|
|
65
|
+
'4. **create_subtask** — A prerequisite task must be completed first to resolve the conflict. The current task will pause, the subtask runs, then the current task resumes.'
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
sections.push(
|
|
69
|
+
"Return a single valid JSON object with your ruling and nothing else.",
|
|
70
|
+
'JSON schema: {"ruling":"approve"|"approve_with_conditions"|"escalate_human"|"create_subtask","classification":[{"issue":string,"category":"critical"|"important"|"style","action":"must_fix"|"should_fix"|"dismiss"}],"conditions":[string],"dismissed":[string],"escalate":boolean,"escalate_reason":string|null,"subtask":{"title":string,"description":string,"reason":string}|null}'
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const stage = conflict?.stage || "unknown";
|
|
74
|
+
const iterationCount = conflict?.iterationCount ?? "?";
|
|
75
|
+
const maxIterations = conflict?.maxIterations ?? "?";
|
|
76
|
+
|
|
77
|
+
sections.push(
|
|
78
|
+
`## Conflict context`,
|
|
79
|
+
`Stage: ${stage}`,
|
|
80
|
+
`Iterations exhausted: ${iterationCount}/${maxIterations}`
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (task) {
|
|
84
|
+
sections.push(`## Original task\n${task}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (conflict?.diff) {
|
|
88
|
+
sections.push(`## Current diff\n${conflict.diff}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
sections.push(`## Agent interaction history\n${formatHistory(conflict?.history)}`);
|
|
92
|
+
|
|
93
|
+
return sections.join("\n\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseSolomonOutput(raw) {
|
|
97
|
+
const text = raw?.trim() || "";
|
|
98
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
99
|
+
if (!jsonMatch) return null;
|
|
100
|
+
return JSON.parse(jsonMatch[0]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildSummary(parsed) {
|
|
104
|
+
const ruling = parsed.ruling || "unknown";
|
|
105
|
+
const conditions = parsed.conditions || [];
|
|
106
|
+
const dismissed = parsed.dismissed || [];
|
|
107
|
+
const subtask = parsed.subtask || null;
|
|
108
|
+
|
|
109
|
+
if (ruling === "approve") {
|
|
110
|
+
const parts = ["Approved"];
|
|
111
|
+
if (dismissed.length > 0) parts.push(`${dismissed.length} dismissed`);
|
|
112
|
+
return parts.join("; ");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (ruling === "approve_with_conditions") {
|
|
116
|
+
const parts = [`Approved with ${conditions.length} condition${conditions.length !== 1 ? "s" : ""}`];
|
|
117
|
+
if (dismissed.length > 0) parts.push(`${dismissed.length} dismissed`);
|
|
118
|
+
return parts.join("; ");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (ruling === "escalate_human") {
|
|
122
|
+
return `Escalated to human: ${parsed.escalate_reason || "ambiguous conflict"}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (ruling === "create_subtask") {
|
|
126
|
+
return `Subtask created: ${subtask?.title || "unnamed subtask"}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return `Solomon ruling: ${ruling}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export class SolomonRole extends BaseRole {
|
|
133
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
134
|
+
super({ name: "solomon", config, logger, emitter });
|
|
135
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async execute(input) {
|
|
139
|
+
const conflict = input?.conflict || {};
|
|
140
|
+
|
|
141
|
+
const provider = resolveProvider(this.config);
|
|
142
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
143
|
+
|
|
144
|
+
const prompt = buildPrompt({
|
|
145
|
+
conflict,
|
|
146
|
+
task: this.context?.task || "",
|
|
147
|
+
instructions: this.instructions
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = await agent.runTask({ prompt, role: "solomon" });
|
|
151
|
+
|
|
152
|
+
if (!result.ok) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
result: {
|
|
156
|
+
error: result.error || result.output || "Solomon arbitration failed",
|
|
157
|
+
provider
|
|
158
|
+
},
|
|
159
|
+
summary: `Solomon failed: ${result.error || "unknown error"}`
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const parsed = parseSolomonOutput(result.output);
|
|
165
|
+
if (!parsed) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
result: { error: "Failed to parse Solomon output: no JSON found", provider },
|
|
169
|
+
summary: "Solomon output parse error: no JSON found"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const ruling = parsed.ruling || "approve";
|
|
174
|
+
const escalate = Boolean(parsed.escalate);
|
|
175
|
+
const ok = ruling !== "escalate_human";
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
ok,
|
|
179
|
+
result: {
|
|
180
|
+
ruling,
|
|
181
|
+
classification: parsed.classification || [],
|
|
182
|
+
conditions: parsed.conditions || [],
|
|
183
|
+
dismissed: parsed.dismissed || [],
|
|
184
|
+
escalate,
|
|
185
|
+
escalate_reason: parsed.escalate_reason || null,
|
|
186
|
+
subtask: parsed.subtask || null,
|
|
187
|
+
provider
|
|
188
|
+
},
|
|
189
|
+
summary: buildSummary(parsed)
|
|
190
|
+
};
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
result: { error: `Failed to parse Solomon output: ${err.message}`, provider },
|
|
195
|
+
summary: `Solomon output parse error: ${err.message}`
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { runSonarScan } from "../sonar/scanner.js";
|
|
3
|
+
import { getQualityGateStatus, getOpenIssues } from "../sonar/api.js";
|
|
4
|
+
import { shouldBlockByProfile, summarizeIssues } from "../sonar/enforcer.js";
|
|
5
|
+
|
|
6
|
+
function normalizeIssues(rawIssues) {
|
|
7
|
+
return rawIssues.map((issue) => ({
|
|
8
|
+
severity: issue.severity || "UNKNOWN",
|
|
9
|
+
type: issue.type || "UNKNOWN",
|
|
10
|
+
file: issue.component || "",
|
|
11
|
+
line: issue.line || 0,
|
|
12
|
+
rule: issue.rule || "",
|
|
13
|
+
message: issue.message || ""
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class SonarRole extends BaseRole {
|
|
18
|
+
constructor({ config, logger, emitter = null }) {
|
|
19
|
+
super({ name: "sonar", config, logger, emitter });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async execute(_input) {
|
|
23
|
+
const scan = await runSonarScan(this.config);
|
|
24
|
+
|
|
25
|
+
if (!scan.ok) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
result: {
|
|
29
|
+
projectKey: scan.projectKey || null,
|
|
30
|
+
gateStatus: null,
|
|
31
|
+
issues: [],
|
|
32
|
+
openIssuesTotal: 0,
|
|
33
|
+
issuesSummary: "",
|
|
34
|
+
blocking: false,
|
|
35
|
+
error: scan.stderr || scan.stdout || "Scan failed"
|
|
36
|
+
},
|
|
37
|
+
summary: `Sonar scan failed: ${scan.stderr || scan.stdout || "unknown error"}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const gate = await getQualityGateStatus(this.config, scan.projectKey);
|
|
42
|
+
const openIssues = await getOpenIssues(this.config, scan.projectKey);
|
|
43
|
+
const issues = normalizeIssues(openIssues.issues || []);
|
|
44
|
+
const issuesSummary = summarizeIssues(openIssues.issues || []);
|
|
45
|
+
|
|
46
|
+
const profile = this.config.sonarqube?.enforcement_profile || "pragmatic";
|
|
47
|
+
const blocking = shouldBlockByProfile({
|
|
48
|
+
gateStatus: gate.status,
|
|
49
|
+
profile
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
ok: !blocking,
|
|
54
|
+
result: {
|
|
55
|
+
projectKey: scan.projectKey,
|
|
56
|
+
gateStatus: gate.status,
|
|
57
|
+
issues,
|
|
58
|
+
openIssuesTotal: openIssues.total || 0,
|
|
59
|
+
issuesSummary,
|
|
60
|
+
blocking
|
|
61
|
+
},
|
|
62
|
+
summary: `Quality gate: ${gate.status}; Issues: ${openIssues.total || 0}${issuesSummary ? ` (${issuesSummary})` : ""}${blocking ? " [BLOCKING]" : ""}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
|
|
4
|
+
const SUBAGENT_PREAMBLE = [
|
|
5
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
6
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
7
|
+
"Do NOT use any MCP tools. Focus only on evaluating test quality."
|
|
8
|
+
].join(" ");
|
|
9
|
+
|
|
10
|
+
function resolveProvider(config) {
|
|
11
|
+
return (
|
|
12
|
+
config?.roles?.tester?.provider ||
|
|
13
|
+
config?.roles?.coder?.provider ||
|
|
14
|
+
"claude"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildPrompt({ task, diff, sonarIssues, instructions }) {
|
|
19
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
20
|
+
|
|
21
|
+
if (instructions) {
|
|
22
|
+
sections.push(instructions);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
sections.push(
|
|
26
|
+
"You are a test quality gate. You do NOT write tests — you evaluate them.",
|
|
27
|
+
"Run the test suite, check coverage, identify missing scenarios, and evaluate assertion quality.",
|
|
28
|
+
"Return a single valid JSON object with your findings and nothing else.",
|
|
29
|
+
'JSON schema: {"tests_pass":boolean,"coverage":{"overall":number,"services":number,"utilities":number},"missing_scenarios":[string],"quality_issues":[string],"verdict":"pass"|"fail"}'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
sections.push(`## Task\n${task}`);
|
|
33
|
+
|
|
34
|
+
if (diff) {
|
|
35
|
+
sections.push(`## Git diff\n${diff}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (sonarIssues) {
|
|
39
|
+
sections.push(`## Sonar test issues\n${sonarIssues}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return sections.join("\n\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseTesterOutput(raw) {
|
|
46
|
+
const text = raw?.trim() || "";
|
|
47
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
48
|
+
if (!jsonMatch) return null;
|
|
49
|
+
return JSON.parse(jsonMatch[0]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class TesterRole extends BaseRole {
|
|
53
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
54
|
+
super({ name: "tester", config, logger, emitter });
|
|
55
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async execute(input) {
|
|
59
|
+
const { task, diff, sonarIssues } = typeof input === "string"
|
|
60
|
+
? { task: input, diff: null, sonarIssues: null }
|
|
61
|
+
: { task: input?.task || this.context?.task || "", diff: input?.diff || null, sonarIssues: input?.sonarIssues || null };
|
|
62
|
+
|
|
63
|
+
const provider = resolveProvider(this.config);
|
|
64
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
65
|
+
|
|
66
|
+
const prompt = buildPrompt({ task, diff, sonarIssues, instructions: this.instructions });
|
|
67
|
+
const result = await agent.runTask({ prompt, role: "tester" });
|
|
68
|
+
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
result: {
|
|
73
|
+
error: result.error || result.output || "Tester failed",
|
|
74
|
+
provider
|
|
75
|
+
},
|
|
76
|
+
summary: `Tester failed: ${result.error || "unknown error"}`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const parsed = parseTesterOutput(result.output);
|
|
82
|
+
if (!parsed) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
result: { error: "Failed to parse tester output: no JSON found", provider },
|
|
86
|
+
summary: "Tester output parse error: no JSON found"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const verdict = parsed.verdict || (parsed.tests_pass ? "pass" : "fail");
|
|
91
|
+
const ok = verdict === "pass";
|
|
92
|
+
const coverage = parsed.coverage || {};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
ok,
|
|
96
|
+
result: {
|
|
97
|
+
tests_pass: Boolean(parsed.tests_pass),
|
|
98
|
+
coverage,
|
|
99
|
+
missing_scenarios: parsed.missing_scenarios || [],
|
|
100
|
+
quality_issues: parsed.quality_issues || [],
|
|
101
|
+
verdict,
|
|
102
|
+
provider
|
|
103
|
+
},
|
|
104
|
+
summary: `Verdict: ${verdict}; Coverage: ${coverage.overall ?? "?"}%${parsed.missing_scenarios?.length ? `; ${parsed.missing_scenarios.length} missing scenario(s)` : ""}${parsed.quality_issues?.length ? `; ${parsed.quality_issues.length} quality issue(s)` : ""}`
|
|
105
|
+
};
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
result: { error: `Failed to parse tester output: ${err.message}`, provider },
|
|
110
|
+
summary: `Tester output parse error: ${err.message}`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
|
|
4
|
+
const SUBAGENT_PREAMBLE = [
|
|
5
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
6
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
7
|
+
"Do NOT use any MCP tools. Focus only on task complexity triage."
|
|
8
|
+
].join(" ");
|
|
9
|
+
|
|
10
|
+
const VALID_LEVELS = new Set(["trivial", "simple", "medium", "complex"]);
|
|
11
|
+
const VALID_ROLES = new Set(["planner", "researcher", "refactorer", "reviewer", "tester", "security"]);
|
|
12
|
+
|
|
13
|
+
function resolveProvider(config) {
|
|
14
|
+
return (
|
|
15
|
+
config?.roles?.triage?.provider ||
|
|
16
|
+
config?.roles?.coder?.provider ||
|
|
17
|
+
"claude"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildPrompt({ task, instructions }) {
|
|
22
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
23
|
+
|
|
24
|
+
if (instructions) {
|
|
25
|
+
sections.push(instructions);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
sections.push(
|
|
29
|
+
"Classify the task complexity and recommend only the necessary pipeline roles.",
|
|
30
|
+
"Keep the reasoning short and practical.",
|
|
31
|
+
"Return a single valid JSON object and nothing else.",
|
|
32
|
+
'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security"],"reasoning":string}'
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
sections.push(`## Task\n${task}`);
|
|
36
|
+
|
|
37
|
+
return sections.join("\n\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseTriageOutput(raw) {
|
|
41
|
+
const text = raw?.trim() || "";
|
|
42
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
43
|
+
if (!jsonMatch) return null;
|
|
44
|
+
return JSON.parse(jsonMatch[0]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeRoles(roles) {
|
|
48
|
+
if (!Array.isArray(roles)) return [];
|
|
49
|
+
return Array.from(new Set(roles.filter((role) => VALID_ROLES.has(role))));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class TriageRole extends BaseRole {
|
|
53
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
54
|
+
super({ name: "triage", config, logger, emitter });
|
|
55
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async execute(input) {
|
|
59
|
+
const task = typeof input === "string"
|
|
60
|
+
? input
|
|
61
|
+
: input?.task || this.context?.task || "";
|
|
62
|
+
|
|
63
|
+
const provider = resolveProvider(this.config);
|
|
64
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
65
|
+
|
|
66
|
+
const prompt = buildPrompt({ task, instructions: this.instructions });
|
|
67
|
+
const result = await agent.runTask({ prompt, role: "triage" });
|
|
68
|
+
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
result: {
|
|
73
|
+
error: result.error || result.output || "Triage failed",
|
|
74
|
+
provider
|
|
75
|
+
},
|
|
76
|
+
summary: `Triage failed: ${result.error || "unknown error"}`,
|
|
77
|
+
usage: result.usage
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const parsed = parseTriageOutput(result.output);
|
|
83
|
+
if (!parsed) {
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
result: {
|
|
87
|
+
level: "medium",
|
|
88
|
+
roles: ["reviewer"],
|
|
89
|
+
reasoning: "Unstructured output, using safe defaults.",
|
|
90
|
+
provider,
|
|
91
|
+
raw: result.output
|
|
92
|
+
},
|
|
93
|
+
summary: "Triage complete (fallback defaults)",
|
|
94
|
+
usage: result.usage
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
|
|
99
|
+
const roles = normalizeRoles(parsed.roles);
|
|
100
|
+
const reasoning = String(parsed.reasoning || "").trim() || "No reasoning provided.";
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
result: {
|
|
105
|
+
level,
|
|
106
|
+
roles,
|
|
107
|
+
reasoning,
|
|
108
|
+
provider
|
|
109
|
+
},
|
|
110
|
+
summary: `Triage: ${level} (${roles.length} role${roles.length === 1 ? "" : "s"})`,
|
|
111
|
+
usage: result.usage
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
result: {
|
|
117
|
+
level: "medium",
|
|
118
|
+
roles: ["reviewer"],
|
|
119
|
+
reasoning: "Failed to parse triage output, using safe defaults.",
|
|
120
|
+
provider,
|
|
121
|
+
raw: result.output
|
|
122
|
+
},
|
|
123
|
+
summary: "Triage complete (fallback defaults)",
|
|
124
|
+
usage: result.usage
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|