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,67 @@
|
|
|
1
|
+
import { createAgent } from "../agents/index.js";
|
|
2
|
+
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
3
|
+
import { resolveRole } from "../config.js";
|
|
4
|
+
import { buildPlannerPrompt } from "../prompts/planner.js";
|
|
5
|
+
import { parseMaybeJsonString } from "../review/parser.js";
|
|
6
|
+
|
|
7
|
+
function formatPlan(plan) {
|
|
8
|
+
const lines = [];
|
|
9
|
+
|
|
10
|
+
if (plan.approach) {
|
|
11
|
+
lines.push("## Approach", plan.approach, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (plan.steps?.length) {
|
|
15
|
+
lines.push("## Steps");
|
|
16
|
+
for (let i = 0; i < plan.steps.length; i++) {
|
|
17
|
+
const step = plan.steps[i];
|
|
18
|
+
const commit = step.commit ? ` → \`${step.commit}\`` : "";
|
|
19
|
+
lines.push(`${i + 1}. ${step.description}${commit}`);
|
|
20
|
+
}
|
|
21
|
+
lines.push("");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (plan.risks?.length) {
|
|
25
|
+
lines.push("## Risks");
|
|
26
|
+
for (const risk of plan.risks) {
|
|
27
|
+
lines.push(`- ${risk}`);
|
|
28
|
+
}
|
|
29
|
+
lines.push("");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (plan.outOfScope?.length) {
|
|
33
|
+
lines.push("## Out of scope");
|
|
34
|
+
for (const item of plan.outOfScope) {
|
|
35
|
+
lines.push(`- ${item}`);
|
|
36
|
+
}
|
|
37
|
+
lines.push("");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function planCommand({ task, config, logger, json, context }) {
|
|
44
|
+
const plannerRole = resolveRole(config, "planner");
|
|
45
|
+
await assertAgentsAvailable([plannerRole.provider]);
|
|
46
|
+
|
|
47
|
+
const planner = createAgent(plannerRole.provider, config, logger);
|
|
48
|
+
const prompt = buildPlannerPrompt({ task, context });
|
|
49
|
+
const result = await planner.runTask({ prompt, role: "planner" });
|
|
50
|
+
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
throw new Error(result.error || result.output || "Planner failed");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parsed = parseMaybeJsonString(result.output);
|
|
56
|
+
|
|
57
|
+
if (json) {
|
|
58
|
+
console.log(JSON.stringify(parsed || result.output, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (parsed && parsed.approach) {
|
|
63
|
+
console.log(formatPlan(parsed));
|
|
64
|
+
} else {
|
|
65
|
+
console.log(result.output);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { exists } from "../utils/fs.js";
|
|
4
|
+
import { getSessionRoot } from "../utils/paths.js";
|
|
5
|
+
import { loadConfig } from "../config.js";
|
|
6
|
+
|
|
7
|
+
function parseBudgetFromActivityLog(logText) {
|
|
8
|
+
if (!logText) {
|
|
9
|
+
return { consumed_usd: null, limit_usd: null };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const regex = /Budget:\s*\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*\$([0-9]+(?:\.[0-9]+)?)/g;
|
|
13
|
+
let match;
|
|
14
|
+
let last = null;
|
|
15
|
+
while ((match = regex.exec(logText)) !== null) {
|
|
16
|
+
last = match;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!last) {
|
|
20
|
+
return { consumed_usd: null, limit_usd: null };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
consumed_usd: Number(last[1]),
|
|
25
|
+
limit_usd: Number(last[2])
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function summarizeIterations(checkpoints = []) {
|
|
30
|
+
const byIteration = new Map();
|
|
31
|
+
for (const checkpoint of checkpoints) {
|
|
32
|
+
const iteration = Number(checkpoint?.iteration);
|
|
33
|
+
if (!Number.isFinite(iteration) || iteration <= 0) continue;
|
|
34
|
+
|
|
35
|
+
if (!byIteration.has(iteration)) {
|
|
36
|
+
byIteration.set(iteration, {
|
|
37
|
+
iteration,
|
|
38
|
+
coder_runs: 0,
|
|
39
|
+
reviewer_attempts: 0,
|
|
40
|
+
reviewer_approved: null
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const item = byIteration.get(iteration);
|
|
45
|
+
if (checkpoint.stage === "coder") {
|
|
46
|
+
item.coder_runs += 1;
|
|
47
|
+
} else if (checkpoint.stage === "reviewer-attempt") {
|
|
48
|
+
item.reviewer_attempts += 1;
|
|
49
|
+
} else if (checkpoint.stage === "reviewer") {
|
|
50
|
+
item.reviewer_approved = Boolean(checkpoint.approved);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return [...byIteration.values()].sort((a, b) => a.iteration - b.iteration);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function summarizePlan(checkpoints = []) {
|
|
58
|
+
const stages = checkpoints
|
|
59
|
+
.map((checkpoint) => checkpoint.stage)
|
|
60
|
+
.filter((stage) => typeof stage === "string" && stage.length > 0);
|
|
61
|
+
|
|
62
|
+
const uniqueOrdered = [];
|
|
63
|
+
for (const stage of stages) {
|
|
64
|
+
if (uniqueOrdered[uniqueOrdered.length - 1] !== stage) {
|
|
65
|
+
uniqueOrdered.push(stage);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return uniqueOrdered;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function summarizeSonar(checkpoints = []) {
|
|
73
|
+
const sonarPoints = checkpoints
|
|
74
|
+
.filter((checkpoint) => checkpoint.stage === "sonar" && typeof checkpoint.open_issues === "number")
|
|
75
|
+
.map((checkpoint) => checkpoint.open_issues);
|
|
76
|
+
|
|
77
|
+
if (sonarPoints.length === 0) {
|
|
78
|
+
return { initial: null, final: null, resolved: 0 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const initial = sonarPoints[0];
|
|
82
|
+
const final = sonarPoints[sonarPoints.length - 1];
|
|
83
|
+
return {
|
|
84
|
+
initial,
|
|
85
|
+
final,
|
|
86
|
+
resolved: Math.max(initial - final, 0)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function summarizeCommits(session, checkpoints = []) {
|
|
91
|
+
const idsFromSession = Array.isArray(session?.git?.commits)
|
|
92
|
+
? session.git.commits.filter((item) => typeof item === "string" && item.length > 0)
|
|
93
|
+
: [];
|
|
94
|
+
|
|
95
|
+
const idsFromCheckpoints = checkpoints
|
|
96
|
+
.filter((checkpoint) => checkpoint.stage === "git-commit" && checkpoint.committed && checkpoint.commit)
|
|
97
|
+
.map((checkpoint) => checkpoint.commit)
|
|
98
|
+
.filter((item) => typeof item === "string" && item.length > 0);
|
|
99
|
+
|
|
100
|
+
const ids = [...new Set([...idsFromSession, ...idsFromCheckpoints])];
|
|
101
|
+
if (ids.length > 0) {
|
|
102
|
+
return { count: ids.length, ids };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const committedCount = checkpoints.filter(
|
|
106
|
+
(checkpoint) => checkpoint.stage === "git-commit" && checkpoint.committed
|
|
107
|
+
).length;
|
|
108
|
+
|
|
109
|
+
return { count: committedCount, ids: [] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function readActivityLog(sessionDir) {
|
|
113
|
+
const file = path.join(sessionDir, "activity.log");
|
|
114
|
+
if (!(await exists(file))) {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return fs.readFile(file, "utf8");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function buildReport(dir, sessionId) {
|
|
122
|
+
const sessionDir = path.join(dir, sessionId);
|
|
123
|
+
const sessionFile = path.join(sessionDir, "session.json");
|
|
124
|
+
if (!(await exists(sessionFile))) {
|
|
125
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
126
|
+
}
|
|
127
|
+
const content = await fs.readFile(sessionFile, "utf8");
|
|
128
|
+
const session = JSON.parse(content);
|
|
129
|
+
const checkpoints = Array.isArray(session.checkpoints) ? session.checkpoints : [];
|
|
130
|
+
const activityLog = await readActivityLog(sessionDir);
|
|
131
|
+
|
|
132
|
+
const sonar = summarizeSonar(checkpoints);
|
|
133
|
+
const budget = parseBudgetFromActivityLog(activityLog);
|
|
134
|
+
const commits = summarizeCommits(session, checkpoints);
|
|
135
|
+
|
|
136
|
+
const budgetTrace = Array.isArray(session.budget?.trace) ? session.budget.trace : [];
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
session_id: session.id,
|
|
140
|
+
task_description: session.task || "",
|
|
141
|
+
plan_executed: summarizePlan(checkpoints),
|
|
142
|
+
iterations: summarizeIterations(checkpoints),
|
|
143
|
+
sonar_issues_resolved: {
|
|
144
|
+
initial_open_issues: sonar.initial,
|
|
145
|
+
final_open_issues: sonar.final,
|
|
146
|
+
resolved: sonar.resolved
|
|
147
|
+
},
|
|
148
|
+
budget_consumed: budget,
|
|
149
|
+
budget_trace: budgetTrace,
|
|
150
|
+
commits_generated: commits,
|
|
151
|
+
status: session.status || "unknown"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function printTextReport(report) {
|
|
156
|
+
const budgetText =
|
|
157
|
+
typeof report.budget_consumed?.consumed_usd === "number"
|
|
158
|
+
? `$${report.budget_consumed.consumed_usd.toFixed(2)}${
|
|
159
|
+
typeof report.budget_consumed?.limit_usd === "number"
|
|
160
|
+
? ` / $${report.budget_consumed.limit_usd.toFixed(2)}`
|
|
161
|
+
: ""
|
|
162
|
+
}`
|
|
163
|
+
: "N/A";
|
|
164
|
+
|
|
165
|
+
const planText = report.plan_executed.length > 0 ? report.plan_executed.join(" -> ") : "N/A";
|
|
166
|
+
const iterationText =
|
|
167
|
+
report.iterations.length > 0
|
|
168
|
+
? report.iterations
|
|
169
|
+
.map(
|
|
170
|
+
(item) =>
|
|
171
|
+
`#${item.iteration} coder=${item.coder_runs} reviewer_attempts=${item.reviewer_attempts} approved=${item.reviewer_approved}`
|
|
172
|
+
)
|
|
173
|
+
.join("\n")
|
|
174
|
+
: "N/A";
|
|
175
|
+
const commitsText =
|
|
176
|
+
report.commits_generated.ids.length > 0
|
|
177
|
+
? `${report.commits_generated.count} (${report.commits_generated.ids.join(", ")})`
|
|
178
|
+
: String(report.commits_generated.count);
|
|
179
|
+
|
|
180
|
+
console.log(`Session: ${report.session_id}`);
|
|
181
|
+
console.log(`Status: ${report.status}`);
|
|
182
|
+
console.log("Task Description:");
|
|
183
|
+
console.log(report.task_description || "N/A");
|
|
184
|
+
console.log("Plan Executed:");
|
|
185
|
+
console.log(planText);
|
|
186
|
+
console.log("Iterations (Coder/Reviewer):");
|
|
187
|
+
console.log(iterationText);
|
|
188
|
+
console.log("Sonar Issues Resolved:");
|
|
189
|
+
console.log(
|
|
190
|
+
`initial=${report.sonar_issues_resolved.initial_open_issues ?? "N/A"} final=${report.sonar_issues_resolved.final_open_issues ?? "N/A"} resolved=${report.sonar_issues_resolved.resolved}`
|
|
191
|
+
);
|
|
192
|
+
console.log("Budget Consumed:");
|
|
193
|
+
console.log(budgetText);
|
|
194
|
+
console.log("Commits Generated:");
|
|
195
|
+
console.log(commitsText);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatDuration(ms) {
|
|
199
|
+
if (ms === null || ms === undefined) return "-";
|
|
200
|
+
if (ms < 1000) return `${ms}ms`;
|
|
201
|
+
const seconds = ms / 1000;
|
|
202
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
203
|
+
const minutes = Math.floor(seconds / 60);
|
|
204
|
+
const remainSeconds = (seconds % 60).toFixed(0);
|
|
205
|
+
return `${minutes}m${remainSeconds}s`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function padRight(str, len) {
|
|
209
|
+
const s = String(str);
|
|
210
|
+
return s.length >= len ? s : s + " ".repeat(len - s.length);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function padLeft(str, len) {
|
|
214
|
+
const s = String(str);
|
|
215
|
+
return s.length >= len ? s : " ".repeat(len - s.length) + s;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function convertCost(costUsd, currency, exchangeRate) {
|
|
219
|
+
if (currency === "eur") return costUsd * exchangeRate;
|
|
220
|
+
return costUsd;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function formatCost(cost, currency) {
|
|
224
|
+
const symbol = currency === "eur" ? "\u20AC" : "$";
|
|
225
|
+
return `${symbol}${cost.toFixed(4)}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function printTraceTable(trace, { currency = "usd", exchangeRate = 0.92 } = {}) {
|
|
229
|
+
if (!trace || trace.length === 0) {
|
|
230
|
+
console.log("No trace data available.");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const currencyLabel = currency.toUpperCase();
|
|
235
|
+
|
|
236
|
+
const headers = ["#", "Stage", "Provider", "Duration", "Tokens In", "Tokens Out", `Cost ${currencyLabel}`];
|
|
237
|
+
const rows = trace.map((entry) => {
|
|
238
|
+
const cost = convertCost(entry.cost_usd, currency, exchangeRate);
|
|
239
|
+
return [
|
|
240
|
+
String(entry.index ?? "-"),
|
|
241
|
+
entry.role,
|
|
242
|
+
entry.provider || "-",
|
|
243
|
+
formatDuration(entry.duration_ms),
|
|
244
|
+
String(entry.tokens_in),
|
|
245
|
+
String(entry.tokens_out),
|
|
246
|
+
formatCost(cost, currency)
|
|
247
|
+
];
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const totals = trace.reduce(
|
|
251
|
+
(acc, entry) => {
|
|
252
|
+
acc.tokens_in += entry.tokens_in;
|
|
253
|
+
acc.tokens_out += entry.tokens_out;
|
|
254
|
+
acc.cost += entry.cost_usd;
|
|
255
|
+
acc.duration += entry.duration_ms ?? 0;
|
|
256
|
+
return acc;
|
|
257
|
+
},
|
|
258
|
+
{ tokens_in: 0, tokens_out: 0, cost: 0, duration: 0 }
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const totalRow = [
|
|
262
|
+
"",
|
|
263
|
+
"TOTAL",
|
|
264
|
+
"",
|
|
265
|
+
formatDuration(totals.duration),
|
|
266
|
+
String(totals.tokens_in),
|
|
267
|
+
String(totals.tokens_out),
|
|
268
|
+
formatCost(convertCost(totals.cost, currency, exchangeRate), currency)
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const allRows = [headers, ...rows, totalRow];
|
|
272
|
+
const colWidths = headers.map((_, colIdx) =>
|
|
273
|
+
Math.max(...allRows.map((row) => String(row[colIdx]).length))
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const rightAligned = new Set([0, 3, 4, 5, 6]);
|
|
277
|
+
function formatRow(row) {
|
|
278
|
+
return row
|
|
279
|
+
.map((cell, idx) =>
|
|
280
|
+
rightAligned.has(idx)
|
|
281
|
+
? padLeft(cell, colWidths[idx])
|
|
282
|
+
: padRight(cell, colWidths[idx])
|
|
283
|
+
)
|
|
284
|
+
.join(" ");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log(formatRow(headers));
|
|
288
|
+
console.log(colWidths.map((w) => "-".repeat(w)).join(" "));
|
|
289
|
+
for (const row of rows) {
|
|
290
|
+
console.log(formatRow(row));
|
|
291
|
+
}
|
|
292
|
+
console.log(colWidths.map((w) => "-".repeat(w)).join(" "));
|
|
293
|
+
console.log(formatRow(totalRow));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function reportCommand({ list = false, sessionId = null, format = "text", trace = false, currency = "usd" }) {
|
|
297
|
+
const dir = getSessionRoot();
|
|
298
|
+
if (!(await exists(dir))) {
|
|
299
|
+
console.log("No reports yet");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const entries = await fs.readdir(dir);
|
|
304
|
+
if (list) {
|
|
305
|
+
for (const item of entries) console.log(item);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const ids = [...entries].sort();
|
|
310
|
+
const selectedSessionId = sessionId || ids.at(-1);
|
|
311
|
+
if (!selectedSessionId) {
|
|
312
|
+
console.log("No reports yet");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (sessionId && !ids.includes(sessionId)) {
|
|
316
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const report = await buildReport(dir, selectedSessionId);
|
|
320
|
+
if (format === "json") {
|
|
321
|
+
console.log(JSON.stringify(report, null, 2));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (trace) {
|
|
326
|
+
const { config } = await loadConfig();
|
|
327
|
+
const cur = currency?.toLowerCase() || config?.budget?.currency || "usd";
|
|
328
|
+
const rate = config?.budget?.exchange_rate_eur ?? 0.92;
|
|
329
|
+
console.log(`Session: ${report.session_id}`);
|
|
330
|
+
console.log(`Status: ${report.status}`);
|
|
331
|
+
console.log(`Task: ${report.task_description || "N/A"}`);
|
|
332
|
+
console.log("");
|
|
333
|
+
printTraceTable(report.budget_trace, { currency: cur, exchangeRate: rate });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
printTextReport(report);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export { formatDuration, convertCost, formatCost, printTraceTable, buildReport };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { resumeFlow } from "../orchestrator.js";
|
|
3
|
+
import { createActivityLog } from "../activity-log.js";
|
|
4
|
+
import { printEvent } from "../utils/display.js";
|
|
5
|
+
|
|
6
|
+
export async function resumeCommand({ sessionId, answer, config, logger, flags }) {
|
|
7
|
+
const jsonMode = flags?.json;
|
|
8
|
+
|
|
9
|
+
const emitter = new EventEmitter();
|
|
10
|
+
let activityLog = null;
|
|
11
|
+
|
|
12
|
+
emitter.on("progress", (event) => {
|
|
13
|
+
if (!activityLog && event.sessionId) {
|
|
14
|
+
activityLog = createActivityLog(event.sessionId);
|
|
15
|
+
logger.onLog((entry) => activityLog.write(entry));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (activityLog) {
|
|
19
|
+
activityLog.writeEvent(event);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!jsonMode) {
|
|
23
|
+
printEvent(event);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const result = await resumeFlow({
|
|
28
|
+
sessionId,
|
|
29
|
+
answer: answer || null,
|
|
30
|
+
config,
|
|
31
|
+
logger,
|
|
32
|
+
flags: flags || {},
|
|
33
|
+
emitter
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (jsonMode || !answer) {
|
|
37
|
+
console.log(JSON.stringify(result, null, 2));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createAgent } from "../agents/index.js";
|
|
2
|
+
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
3
|
+
import { computeBaseRef, generateDiff } from "../review/diff-generator.js";
|
|
4
|
+
import { buildReviewerPrompt } from "../prompts/reviewer.js";
|
|
5
|
+
import { resolveRole } from "../config.js";
|
|
6
|
+
import { resolveReviewProfile } from "../review/profiles.js";
|
|
7
|
+
|
|
8
|
+
export async function reviewCommand({ task, config, logger, baseRef }) {
|
|
9
|
+
const reviewerRole = resolveRole(config, "reviewer");
|
|
10
|
+
await assertAgentsAvailable([reviewerRole.provider, config.reviewer_options?.fallback_reviewer]);
|
|
11
|
+
logger.info(`Reviewer (${reviewerRole.provider}) starting...`);
|
|
12
|
+
const reviewer = createAgent(reviewerRole.provider, config, logger);
|
|
13
|
+
const resolvedBase = await computeBaseRef({ baseBranch: config.base_branch, baseRef });
|
|
14
|
+
const diff = await generateDiff({ baseRef: resolvedBase });
|
|
15
|
+
const { rules } = await resolveReviewProfile({ mode: config.review_mode, projectDir: process.cwd() });
|
|
16
|
+
|
|
17
|
+
const prompt = buildReviewerPrompt({ task, diff, reviewRules: rules, mode: config.review_mode });
|
|
18
|
+
const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
|
|
19
|
+
const result = await reviewer.reviewTask({ prompt, onOutput, role: "reviewer" });
|
|
20
|
+
if (!result.ok) {
|
|
21
|
+
if (result.error) logger.error(result.error);
|
|
22
|
+
throw new Error(result.error || result.output || `Reviewer failed (exit ${result.exitCode})`);
|
|
23
|
+
}
|
|
24
|
+
console.log(result.output);
|
|
25
|
+
logger.info(`Reviewer completed (exit ${result.exitCode})`);
|
|
26
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
|
|
4
|
+
import { resolveRole } from "../config.js";
|
|
5
|
+
import { exists } from "../utils/fs.js";
|
|
6
|
+
|
|
7
|
+
const PIPELINE_ROLES = [
|
|
8
|
+
{ name: "triage", description: "Classifies task complexity and activates pipeline roles" },
|
|
9
|
+
{ name: "researcher", description: "Investigates codebase before planning" },
|
|
10
|
+
{ name: "planner", description: "Generates implementation plans" },
|
|
11
|
+
{ name: "coder", description: "Writes code and tests (TDD)" },
|
|
12
|
+
{ name: "refactorer", description: "Improves code clarity without changing behavior" },
|
|
13
|
+
{ name: "sonar", description: "SonarQube static analysis" },
|
|
14
|
+
{ name: "reviewer", description: "Code review with configurable strictness" },
|
|
15
|
+
{ name: "tester", description: "Test quality gate and coverage checks" },
|
|
16
|
+
{ name: "security", description: "OWASP security audit" },
|
|
17
|
+
{ name: "solomon", description: "Conflict resolver between agents" },
|
|
18
|
+
{ name: "commiter", description: "Git commit, push, and PR automation" }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const REVIEW_VARIANTS = ["reviewer-strict", "reviewer-relaxed", "reviewer-paranoid"];
|
|
22
|
+
|
|
23
|
+
function isRoleEnabled(config, roleName) {
|
|
24
|
+
if (roleName === "coder" || roleName === "commiter" || roleName === "sonar") {
|
|
25
|
+
if (roleName === "sonar") return Boolean(config?.sonarqube?.enabled);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (roleName === "reviewer") return config?.pipeline?.reviewer?.enabled !== false;
|
|
29
|
+
return Boolean(config?.pipeline?.[roleName]?.enabled);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function listRoles(config) {
|
|
33
|
+
return PIPELINE_ROLES.map((role) => {
|
|
34
|
+
const resolved = resolveRole(config, role.name);
|
|
35
|
+
return {
|
|
36
|
+
name: role.name,
|
|
37
|
+
description: role.description,
|
|
38
|
+
provider: resolved.provider || "-",
|
|
39
|
+
model: resolved.model || "-",
|
|
40
|
+
enabled: isRoleEnabled(config, role.name)
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function showRole(roleName, config) {
|
|
46
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
47
|
+
const candidates = resolveRoleMdPath(roleName, projectDir);
|
|
48
|
+
|
|
49
|
+
const customPath = candidates[0];
|
|
50
|
+
const hasCustom = await exists(customPath);
|
|
51
|
+
|
|
52
|
+
const content = await loadFirstExisting(candidates);
|
|
53
|
+
if (!content) {
|
|
54
|
+
return { found: false, roleName, content: null, source: null, customPath: null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let source = "built-in";
|
|
58
|
+
if (hasCustom) {
|
|
59
|
+
source = "custom";
|
|
60
|
+
} else if (candidates.length > 2) {
|
|
61
|
+
try {
|
|
62
|
+
await fs.readFile(candidates[1], "utf8");
|
|
63
|
+
source = "user";
|
|
64
|
+
} catch {
|
|
65
|
+
source = "built-in";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
found: true,
|
|
71
|
+
roleName,
|
|
72
|
+
content,
|
|
73
|
+
source,
|
|
74
|
+
customPath: hasCustom ? customPath : null
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function printRoleList(roles) {
|
|
79
|
+
const nameWidth = Math.max(...roles.map((r) => r.name.length), 4);
|
|
80
|
+
const provWidth = Math.max(...roles.map((r) => r.provider.length), 8);
|
|
81
|
+
|
|
82
|
+
const header = `${"Role".padEnd(nameWidth)} ${"Provider".padEnd(provWidth)} Enabled Description`;
|
|
83
|
+
console.log(header);
|
|
84
|
+
console.log("-".repeat(header.length));
|
|
85
|
+
|
|
86
|
+
for (const role of roles) {
|
|
87
|
+
const enabled = role.enabled ? "yes" : "no ";
|
|
88
|
+
console.log(
|
|
89
|
+
`${role.name.padEnd(nameWidth)} ${role.provider.padEnd(provWidth)} ${enabled.padEnd(7)} ${role.description}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function rolesCommand({ config, subcommand, roleName }) {
|
|
95
|
+
if (subcommand === "show" && roleName) {
|
|
96
|
+
const result = await showRole(roleName, config);
|
|
97
|
+
if (!result.found) {
|
|
98
|
+
console.log(`Role "${roleName}" not found.`);
|
|
99
|
+
console.log(`Available roles: ${PIPELINE_ROLES.map((r) => r.name).join(", ")}`);
|
|
100
|
+
console.log(`Review variants: ${REVIEW_VARIANTS.join(", ")}`);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
if (result.source === "custom") {
|
|
104
|
+
console.log(`[custom override: ${result.customPath}]\n`);
|
|
105
|
+
}
|
|
106
|
+
console.log(result.content);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const roles = listRoles(config);
|
|
111
|
+
printRoleList(roles);
|
|
112
|
+
console.log(`\nReview variants: ${REVIEW_VARIANTS.join(", ")}`);
|
|
113
|
+
console.log('\nUse "kj roles show <role>" to view template instructions.');
|
|
114
|
+
return roles;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { PIPELINE_ROLES, REVIEW_VARIANTS };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { runFlow } from "../orchestrator.js";
|
|
3
|
+
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
4
|
+
import { createActivityLog } from "../activity-log.js";
|
|
5
|
+
import { printHeader, printEvent } from "../utils/display.js";
|
|
6
|
+
import { resolveRole } from "../config.js";
|
|
7
|
+
import { parseCardId, buildTaskFromCard, buildCompletionUpdates } from "../planning-game/adapter.js";
|
|
8
|
+
|
|
9
|
+
export async function runCommandHandler({ task, config, logger, flags }) {
|
|
10
|
+
const requiredProviders = [
|
|
11
|
+
resolveRole(config, "coder").provider,
|
|
12
|
+
config.reviewer_options?.fallback_reviewer
|
|
13
|
+
];
|
|
14
|
+
if (config.pipeline?.reviewer?.enabled !== false) {
|
|
15
|
+
requiredProviders.push(resolveRole(config, "reviewer").provider);
|
|
16
|
+
}
|
|
17
|
+
if (config.pipeline?.triage?.enabled) requiredProviders.push(resolveRole(config, "triage").provider);
|
|
18
|
+
if (config.pipeline?.planner?.enabled) requiredProviders.push(resolveRole(config, "planner").provider);
|
|
19
|
+
if (config.pipeline?.refactorer?.enabled) requiredProviders.push(resolveRole(config, "refactorer").provider);
|
|
20
|
+
if (config.pipeline?.researcher?.enabled) requiredProviders.push(resolveRole(config, "researcher").provider);
|
|
21
|
+
if (config.pipeline?.tester?.enabled) requiredProviders.push(resolveRole(config, "tester").provider);
|
|
22
|
+
if (config.pipeline?.security?.enabled) requiredProviders.push(resolveRole(config, "security").provider);
|
|
23
|
+
await assertAgentsAvailable(requiredProviders);
|
|
24
|
+
|
|
25
|
+
// --- Planning Game: resolve card context ---
|
|
26
|
+
const pgCardId = flags?.pgTask || parseCardId(task);
|
|
27
|
+
const pgProject = flags?.pgProject || config.planning_game?.project_id || null;
|
|
28
|
+
let pgCard = null;
|
|
29
|
+
let enrichedTask = task;
|
|
30
|
+
|
|
31
|
+
if (pgCardId && pgProject && config.planning_game?.enabled !== false) {
|
|
32
|
+
try {
|
|
33
|
+
const { fetchCard, updateCard } = await import("../planning-game/client.js");
|
|
34
|
+
pgCard = await fetchCard({ projectId: pgProject, cardId: pgCardId });
|
|
35
|
+
if (pgCard) {
|
|
36
|
+
enrichedTask = buildTaskFromCard(pgCard);
|
|
37
|
+
logger.info(`Planning Game: loaded card ${pgCardId} from project ${pgProject}`);
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
logger.warn(`Planning Game: could not load card ${pgCardId}: ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const jsonMode = flags?.json;
|
|
45
|
+
|
|
46
|
+
const emitter = new EventEmitter();
|
|
47
|
+
let activityLog = null;
|
|
48
|
+
|
|
49
|
+
emitter.on("progress", (event) => {
|
|
50
|
+
if (!activityLog && event.sessionId) {
|
|
51
|
+
activityLog = createActivityLog(event.sessionId);
|
|
52
|
+
logger.onLog((entry) => activityLog.write(entry));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (activityLog) {
|
|
56
|
+
activityLog.writeEvent(event);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!jsonMode) {
|
|
60
|
+
printEvent(event);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!jsonMode) {
|
|
65
|
+
printHeader({ task: enrichedTask, config });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const startDate = new Date().toISOString();
|
|
69
|
+
const result = await runFlow({ task: enrichedTask, config, logger, flags, emitter });
|
|
70
|
+
|
|
71
|
+
// --- Planning Game: update card on completion ---
|
|
72
|
+
if (pgCard && pgProject && result?.approved) {
|
|
73
|
+
try {
|
|
74
|
+
const { updateCard } = await import("../planning-game/client.js");
|
|
75
|
+
const updates = buildCompletionUpdates({
|
|
76
|
+
approved: true,
|
|
77
|
+
commits: result.git?.commits || [],
|
|
78
|
+
startDate,
|
|
79
|
+
codeveloper: config.planning_game?.codeveloper || null
|
|
80
|
+
});
|
|
81
|
+
await updateCard({ projectId: pgProject, cardId: pgCardId, firebaseId: pgCard.firebaseId, updates });
|
|
82
|
+
logger.info(`Planning Game: updated ${pgCardId} to "${updates.status}"`);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
logger.warn(`Planning Game: could not update card ${pgCardId}: ${err.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (jsonMode) {
|
|
89
|
+
console.log(JSON.stringify(result, null, 2));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getOpenIssues, getQualityGateStatus } from "../sonar/api.js";
|
|
2
|
+
import { runSonarScan } from "../sonar/scanner.js";
|
|
3
|
+
import { summarizeIssues } from "../sonar/enforcer.js";
|
|
4
|
+
|
|
5
|
+
export async function scanCommand({ config }) {
|
|
6
|
+
const scan = await runSonarScan(config);
|
|
7
|
+
if (!scan.ok) {
|
|
8
|
+
throw new Error(`Sonar scan failed: ${scan.stderr || scan.stdout}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const gate = await getQualityGateStatus(config, scan.projectKey);
|
|
12
|
+
const issues = await getOpenIssues(config, scan.projectKey);
|
|
13
|
+
|
|
14
|
+
console.log(`Project key: ${scan.projectKey}`);
|
|
15
|
+
console.log(`Quality Gate: ${gate.status}`);
|
|
16
|
+
console.log(`Open issues: ${issues.total}`);
|
|
17
|
+
console.log(`By severity: ${summarizeIssues(issues.issues) || "none"}`);
|
|
18
|
+
}
|