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,32 @@
|
|
|
1
|
+
import { runCommand } from "./process.js";
|
|
2
|
+
import { resolveBin } from "../agents/resolve-bin.js";
|
|
3
|
+
|
|
4
|
+
const KNOWN_AGENTS = [
|
|
5
|
+
{ name: "claude", install: "npm install -g @anthropic-ai/claude-code" },
|
|
6
|
+
{ name: "codex", install: "npm install -g @openai/codex" },
|
|
7
|
+
{ name: "gemini", install: "npm install -g @anthropic-ai/gemini-code (or check Gemini CLI docs)" },
|
|
8
|
+
{ name: "aider", install: "pip install aider-chat" }
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export async function checkBinary(name, versionArg = "--version") {
|
|
12
|
+
const resolved = resolveBin(name);
|
|
13
|
+
const res = await runCommand(resolved, [versionArg]);
|
|
14
|
+
const version = (res.stdout || res.stderr || "").split("\n")[0].trim();
|
|
15
|
+
return { ok: res.exitCode === 0, version, path: resolved };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function detectAvailableAgents() {
|
|
19
|
+
const results = [];
|
|
20
|
+
for (const agent of KNOWN_AGENTS) {
|
|
21
|
+
const check = await checkBinary(agent.name);
|
|
22
|
+
results.push({
|
|
23
|
+
name: agent.name,
|
|
24
|
+
available: check.ok,
|
|
25
|
+
version: check.ok ? check.version : null,
|
|
26
|
+
install: agent.install
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { KNOWN_AGENTS };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { calculateUsageCostUsd, DEFAULT_MODEL_PRICING, mergePricing } from "./pricing.js";
|
|
2
|
+
|
|
3
|
+
function toSafeNumber(value) {
|
|
4
|
+
const n = Number(value);
|
|
5
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
6
|
+
return n;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function roundUsd(value) {
|
|
10
|
+
return Number(toSafeNumber(value).toFixed(6));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeLimit(limit) {
|
|
14
|
+
if (limit === null || limit === undefined || limit === "") return null;
|
|
15
|
+
const n = Number(limit);
|
|
16
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
17
|
+
return n;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function addToBreakdown(map, key, entry) {
|
|
21
|
+
const current = map[key] || { tokens_in: 0, tokens_out: 0, total_tokens: 0, total_cost_usd: 0, count: 0 };
|
|
22
|
+
current.tokens_in += entry.tokens_in;
|
|
23
|
+
current.tokens_out += entry.tokens_out;
|
|
24
|
+
current.total_tokens += entry.tokens_in + entry.tokens_out;
|
|
25
|
+
current.total_cost_usd = roundUsd(current.total_cost_usd + entry.cost_usd);
|
|
26
|
+
current.count += 1;
|
|
27
|
+
map[key] = current;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class BudgetTracker {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.entries = [];
|
|
33
|
+
this.pricing = mergePricing(DEFAULT_MODEL_PRICING, options.pricing || {});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
record({ role, provider, model, tokens_in, tokens_out, cost_usd, duration_ms, stage_index } = {}) {
|
|
37
|
+
const safeTokensIn = toSafeNumber(tokens_in);
|
|
38
|
+
const safeTokensOut = toSafeNumber(tokens_out);
|
|
39
|
+
const hasExplicitCost = cost_usd !== undefined && cost_usd !== null && cost_usd !== "";
|
|
40
|
+
const modelName = model || provider || null;
|
|
41
|
+
const computedCost = calculateUsageCostUsd({
|
|
42
|
+
model: modelName,
|
|
43
|
+
tokens_in: safeTokensIn,
|
|
44
|
+
tokens_out: safeTokensOut,
|
|
45
|
+
pricing: this.pricing
|
|
46
|
+
});
|
|
47
|
+
const entry = {
|
|
48
|
+
role: role || "unknown",
|
|
49
|
+
provider: provider || "unknown",
|
|
50
|
+
model: modelName,
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
tokens_in: safeTokensIn,
|
|
53
|
+
tokens_out: safeTokensOut,
|
|
54
|
+
cost_usd: roundUsd(hasExplicitCost ? cost_usd : computedCost)
|
|
55
|
+
};
|
|
56
|
+
if (duration_ms !== undefined && duration_ms !== null) {
|
|
57
|
+
entry.duration_ms = toSafeNumber(duration_ms);
|
|
58
|
+
}
|
|
59
|
+
if (stage_index !== undefined && stage_index !== null) {
|
|
60
|
+
entry.stage_index = Number(stage_index);
|
|
61
|
+
}
|
|
62
|
+
this.entries.push(entry);
|
|
63
|
+
return entry;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
total() {
|
|
67
|
+
let tokensIn = 0;
|
|
68
|
+
let tokensOut = 0;
|
|
69
|
+
let totalCost = 0;
|
|
70
|
+
for (const entry of this.entries) {
|
|
71
|
+
tokensIn += entry.tokens_in;
|
|
72
|
+
tokensOut += entry.tokens_out;
|
|
73
|
+
totalCost = roundUsd(totalCost + entry.cost_usd);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
tokens_in: tokensIn,
|
|
77
|
+
tokens_out: tokensOut,
|
|
78
|
+
cost_usd: totalCost
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
remaining(limit) {
|
|
83
|
+
const n = normalizeLimit(limit);
|
|
84
|
+
if (n === null) return Infinity;
|
|
85
|
+
return roundUsd(n - this.total().cost_usd);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
isOverBudget(limit) {
|
|
89
|
+
const n = normalizeLimit(limit);
|
|
90
|
+
if (n === null) return false;
|
|
91
|
+
return this.total().cost_usd > n;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
summary() {
|
|
95
|
+
const totals = this.total();
|
|
96
|
+
const byRole = {};
|
|
97
|
+
|
|
98
|
+
for (const entry of this.entries) {
|
|
99
|
+
addToBreakdown(byRole, entry.role, entry);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
total_tokens: totals.tokens_in + totals.tokens_out,
|
|
104
|
+
total_cost_usd: totals.cost_usd,
|
|
105
|
+
breakdown_by_role: byRole,
|
|
106
|
+
entries: [...this.entries]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
trace() {
|
|
111
|
+
return this.entries.map((entry, index) => ({
|
|
112
|
+
index: entry.stage_index ?? index,
|
|
113
|
+
role: entry.role,
|
|
114
|
+
provider: entry.provider,
|
|
115
|
+
model: entry.model,
|
|
116
|
+
timestamp: entry.timestamp,
|
|
117
|
+
duration_ms: entry.duration_ms ?? null,
|
|
118
|
+
tokens_in: entry.tokens_in,
|
|
119
|
+
tokens_out: entry.tokens_out,
|
|
120
|
+
cost_usd: entry.cost_usd
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
const ANSI = {
|
|
2
|
+
reset: "\x1b[0m",
|
|
3
|
+
bold: "\x1b[1m",
|
|
4
|
+
dim: "\x1b[2m",
|
|
5
|
+
cyan: "\x1b[36m",
|
|
6
|
+
green: "\x1b[32m",
|
|
7
|
+
yellow: "\x1b[33m",
|
|
8
|
+
red: "\x1b[31m",
|
|
9
|
+
gray: "\x1b[90m",
|
|
10
|
+
white: "\x1b[37m",
|
|
11
|
+
magenta: "\x1b[35m"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const ICONS = {
|
|
15
|
+
"session:start": "\u25b6",
|
|
16
|
+
"planner:start": "\ud83e\udde0",
|
|
17
|
+
"planner:end": "\ud83e\udde0",
|
|
18
|
+
"researcher:start": "\ud83d\udd2c",
|
|
19
|
+
"researcher:end": "\ud83d\udd2c",
|
|
20
|
+
"iteration:start": "\u25b6",
|
|
21
|
+
"coder:start": "\ud83d\udd28",
|
|
22
|
+
"coder:end": "\ud83d\udd28",
|
|
23
|
+
"refactorer:start": "\u267b\ufe0f",
|
|
24
|
+
"refactorer:end": "\u267b\ufe0f",
|
|
25
|
+
"tdd:result": "\ud83d\udccb",
|
|
26
|
+
"sonar:start": "\ud83d\udd0d",
|
|
27
|
+
"sonar:end": "\ud83d\udd0d",
|
|
28
|
+
"reviewer:start": "\ud83d\udc41\ufe0f",
|
|
29
|
+
"reviewer:end": "\ud83d\udc41\ufe0f",
|
|
30
|
+
"tester:start": "\ud83e\uddea",
|
|
31
|
+
"tester:end": "\ud83e\uddea",
|
|
32
|
+
"security:start": "\ud83d\udd12",
|
|
33
|
+
"security:end": "\ud83d\udd12",
|
|
34
|
+
"solomon:start": "\u2696\ufe0f",
|
|
35
|
+
"solomon:end": "\u2696\ufe0f",
|
|
36
|
+
"solomon:escalate": "\u26a0\ufe0f",
|
|
37
|
+
"budget:update": "\ud83d\udcb8",
|
|
38
|
+
"iteration:end": "\u23f1\ufe0f",
|
|
39
|
+
"session:end": "\ud83c\udfc1",
|
|
40
|
+
question: "\u2753"
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const STATUS_ICON = {
|
|
44
|
+
ok: `${ANSI.green}\u2705${ANSI.reset}`,
|
|
45
|
+
fail: `${ANSI.red}\u274c${ANSI.reset}`,
|
|
46
|
+
paused: `${ANSI.yellow}\u23f8\ufe0f${ANSI.reset}`,
|
|
47
|
+
running: `${ANSI.cyan}\u25b6${ANSI.reset}`
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function formatElapsed(ms) {
|
|
51
|
+
const totalSec = Math.floor((ms || 0) / 1000);
|
|
52
|
+
const min = String(Math.floor(totalSec / 60)).padStart(2, "0");
|
|
53
|
+
const sec = String(totalSec % 60).padStart(2, "0");
|
|
54
|
+
return `${min}:${sec}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const BAR = `${ANSI.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${ANSI.reset}`;
|
|
58
|
+
|
|
59
|
+
export function printHeader({ task, config }) {
|
|
60
|
+
const version = "0.1.0";
|
|
61
|
+
console.log(BAR);
|
|
62
|
+
console.log(`${ANSI.bold}${ANSI.cyan}\u25b6 Karajan Code v${version}${ANSI.reset}`);
|
|
63
|
+
console.log(BAR);
|
|
64
|
+
console.log(`${ANSI.bold}Task:${ANSI.reset} ${task}`);
|
|
65
|
+
console.log(
|
|
66
|
+
`${ANSI.bold}Coder:${ANSI.reset} ${config.roles?.coder?.provider || config.coder} ${ANSI.dim}|${ANSI.reset} ${ANSI.bold}Reviewer:${ANSI.reset} ${config.roles?.reviewer?.provider || config.reviewer}`
|
|
67
|
+
);
|
|
68
|
+
console.log(
|
|
69
|
+
`${ANSI.bold}Max iterations:${ANSI.reset} ${config.max_iterations} ${ANSI.dim}|${ANSI.reset} ${ANSI.bold}Timeout:${ANSI.reset} ${config.session.max_total_minutes}min`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const pipeline = config.pipeline || {};
|
|
73
|
+
const activeRoles = [];
|
|
74
|
+
if (pipeline.planner?.enabled) activeRoles.push(`Planner (${config.roles?.planner?.provider || "?"})`);
|
|
75
|
+
if (pipeline.researcher?.enabled) activeRoles.push(`Researcher (${config.roles?.researcher?.provider || "?"})`);
|
|
76
|
+
if (pipeline.tester?.enabled) activeRoles.push("Tester");
|
|
77
|
+
if (pipeline.security?.enabled) activeRoles.push("Security");
|
|
78
|
+
if (pipeline.solomon?.enabled) activeRoles.push(`Solomon (${config.roles?.solomon?.provider || "?"})`);
|
|
79
|
+
if (activeRoles.length > 0) {
|
|
80
|
+
console.log(`${ANSI.bold}Pipeline:${ANSI.reset} ${activeRoles.join(` ${ANSI.dim}|${ANSI.reset} `)}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(BAR);
|
|
84
|
+
console.log();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function printEvent(event) {
|
|
88
|
+
const icon = ICONS[event.type] || "\u2022";
|
|
89
|
+
const elapsed = event.elapsed !== undefined ? `${ANSI.dim}[${formatElapsed(event.elapsed)}]${ANSI.reset}` : "";
|
|
90
|
+
const status = event.status ? STATUS_ICON[event.status] || "" : "";
|
|
91
|
+
|
|
92
|
+
switch (event.type) {
|
|
93
|
+
case "session:start":
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case "iteration:start":
|
|
97
|
+
console.log(
|
|
98
|
+
`\n${ANSI.bold}${icon} Iteration ${event.detail?.iteration}/${event.detail?.maxIterations}${ANSI.reset} ${elapsed}`
|
|
99
|
+
);
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case "planner:start":
|
|
103
|
+
console.log(` \u251c\u2500 ${icon} Planner (${event.detail?.planner || "?"}) running...`);
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case "planner:end":
|
|
107
|
+
console.log(` \u251c\u2500 ${status} Planner completed ${elapsed}`);
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case "coder:start":
|
|
111
|
+
console.log(` \u251c\u2500 ${icon} Coder (${event.detail?.coder || "?"}) running...`);
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case "coder:end":
|
|
115
|
+
console.log(` \u251c\u2500 ${status} Coder completed ${elapsed}`);
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
case "refactorer:start":
|
|
119
|
+
console.log(` \u251c\u2500 ${icon} Refactorer (${event.detail?.refactorer || "?"}) running...`);
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case "refactorer:end":
|
|
123
|
+
console.log(` \u251c\u2500 ${status} Refactorer completed ${elapsed}`);
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case "tdd:result": {
|
|
127
|
+
const tdd = event.detail || {};
|
|
128
|
+
const label = tdd.ok ? `${ANSI.green}PASS${ANSI.reset}` : `${ANSI.red}FAIL${ANSI.reset}`;
|
|
129
|
+
const files = tdd.sourceFiles !== undefined ? ` (${tdd.sourceFiles} src, ${tdd.testFiles} test)` : "";
|
|
130
|
+
console.log(` \u251c\u2500 ${icon} TDD policy: ${label}${files}`);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case "researcher:start":
|
|
135
|
+
console.log(` \u251c\u2500 ${icon} Researcher (${event.detail?.researcher || "?"}) investigating...`);
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case "researcher:end":
|
|
139
|
+
console.log(` \u251c\u2500 ${status} Researcher completed ${elapsed}`);
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case "sonar:start":
|
|
143
|
+
console.log(` \u251c\u2500 ${icon} SonarQube scanning...`);
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case "sonar:end": {
|
|
147
|
+
const gate = event.detail?.gateStatus || "?";
|
|
148
|
+
const gateColor = gate === "OK" ? ANSI.green : ANSI.red;
|
|
149
|
+
console.log(` \u251c\u2500 ${status} Quality gate: ${gateColor}${gate}${ANSI.reset} ${elapsed}`);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case "reviewer:start":
|
|
154
|
+
console.log(` \u251c\u2500 ${icon} Reviewer (${event.detail?.reviewer || "?"}) running...`);
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case "reviewer:end": {
|
|
158
|
+
const review = event.detail || {};
|
|
159
|
+
if (review.approved) {
|
|
160
|
+
console.log(` \u251c\u2500 ${ANSI.green}\u2705 Review: APPROVED${ANSI.reset} ${elapsed}`);
|
|
161
|
+
} else {
|
|
162
|
+
const count = review.blockingCount || 0;
|
|
163
|
+
console.log(` \u251c\u2500 ${ANSI.red}\u274c Review: REJECTED (${count} blocking)${ANSI.reset}`);
|
|
164
|
+
if (review.issues) {
|
|
165
|
+
for (const issue of review.issues) {
|
|
166
|
+
console.log(` \u2502 ${ANSI.dim}${issue}${ANSI.reset}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case "tester:start":
|
|
174
|
+
console.log(` \u251c\u2500 ${icon} Tester evaluating...`);
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
case "tester:end": {
|
|
178
|
+
const testerOk = event.detail?.ok !== false;
|
|
179
|
+
if (testerOk) {
|
|
180
|
+
console.log(` \u251c\u2500 ${ANSI.green}\u2705 Tester: passed${ANSI.reset} ${elapsed}`);
|
|
181
|
+
} else {
|
|
182
|
+
const testerSummary = event.detail?.summary || "issues found";
|
|
183
|
+
console.log(` \u251c\u2500 ${ANSI.red}\u274c Tester: ${testerSummary}${ANSI.reset} ${elapsed}`);
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case "security:start":
|
|
189
|
+
console.log(` \u251c\u2500 ${icon} Security auditing...`);
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
case "security:end": {
|
|
193
|
+
const secOk = event.detail?.ok !== false;
|
|
194
|
+
if (secOk) {
|
|
195
|
+
console.log(` \u251c\u2500 ${ANSI.green}\u2705 Security: passed${ANSI.reset} ${elapsed}`);
|
|
196
|
+
} else {
|
|
197
|
+
const secSummary = event.detail?.summary || "vulnerabilities found";
|
|
198
|
+
console.log(` \u251c\u2500 ${ANSI.red}\u274c Security: ${secSummary}${ANSI.reset} ${elapsed}`);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case "solomon:start":
|
|
204
|
+
console.log(` \u251c\u2500 ${icon} Solomon arbitrating ${event.detail?.conflictStage || "?"} conflict...`);
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case "solomon:end": {
|
|
208
|
+
const ruling = event.detail?.ruling || "unknown";
|
|
209
|
+
const rulingUpper = ruling.toUpperCase().replace(/_/g, " ");
|
|
210
|
+
if (ruling === "approve") {
|
|
211
|
+
const dismissedCount = event.detail?.dismissed?.length || 0;
|
|
212
|
+
console.log(` \u251c\u2500 ${ANSI.green}\u2696\ufe0f Solomon: APPROVE${dismissedCount > 0 ? ` (${dismissedCount} dismissed)` : ""}${ANSI.reset} ${elapsed}`);
|
|
213
|
+
} else if (ruling === "approve_with_conditions") {
|
|
214
|
+
const condCount = event.detail?.conditions?.length || 0;
|
|
215
|
+
console.log(` \u251c\u2500 ${ANSI.yellow}\u2696\ufe0f Solomon: ${condCount} condition${condCount !== 1 ? "s" : ""}${ANSI.reset} ${elapsed}`);
|
|
216
|
+
if (event.detail?.conditions) {
|
|
217
|
+
for (const cond of event.detail.conditions) {
|
|
218
|
+
console.log(` \u2502 ${ANSI.dim}${cond}${ANSI.reset}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else if (ruling === "escalate_human") {
|
|
222
|
+
const reason = event.detail?.escalate_reason || "unknown reason";
|
|
223
|
+
console.log(` \u251c\u2500 ${ANSI.red}\u2696\ufe0f Solomon: ESCALATE \u2014 ${reason}${ANSI.reset} ${elapsed}`);
|
|
224
|
+
} else if (ruling === "create_subtask") {
|
|
225
|
+
const subtaskTitle = event.detail?.subtask?.title || "untitled";
|
|
226
|
+
console.log(` \u251c\u2500 ${ANSI.magenta}\u2696\ufe0f Solomon: SUBTASK \u2014 ${subtaskTitle}${ANSI.reset} ${elapsed}`);
|
|
227
|
+
} else {
|
|
228
|
+
console.log(` \u251c\u2500 \u2696\ufe0f Solomon: ${rulingUpper} ${elapsed}`);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case "solomon:escalate": {
|
|
234
|
+
const subloop = event.detail?.subloop || "?";
|
|
235
|
+
const retryCount = event.detail?.retryCount || 0;
|
|
236
|
+
const limit = event.detail?.limit || "?";
|
|
237
|
+
console.log(` \u251c\u2500 ${icon} ${subloop} sub-loop limit reached (${retryCount}/${limit}), invoking Solomon...`);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case "iteration:end":
|
|
242
|
+
console.log(` \u2514\u2500 ${icon} Duration: ${formatElapsed(event.detail?.duration)} ${elapsed}`);
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case "budget:update": {
|
|
246
|
+
const total = Number(event.detail?.total_cost_usd || 0);
|
|
247
|
+
const max = Number(event.detail?.max_budget_usd);
|
|
248
|
+
const pct = Number(event.detail?.pct_used ?? 0);
|
|
249
|
+
const warn = Number(event.detail?.warn_threshold_pct ?? 80);
|
|
250
|
+
const color = max > 0 && pct >= 100 ? ANSI.red : max > 0 && pct >= warn ? ANSI.yellow : ANSI.green;
|
|
251
|
+
if (Number.isFinite(max) && max >= 0) {
|
|
252
|
+
console.log(` \u251c\u2500 ${icon} Budget: ${color}$${total.toFixed(2)} / $${max.toFixed(2)} (${pct.toFixed(1)}%)${ANSI.reset}`);
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case "session:end": {
|
|
258
|
+
console.log();
|
|
259
|
+
const resultLabel = event.detail?.approved
|
|
260
|
+
? `${ANSI.bold}${ANSI.green}APPROVED${ANSI.reset}`
|
|
261
|
+
: `${ANSI.bold}${ANSI.red}${event.detail?.reason || "FAILED"}${ANSI.reset}`;
|
|
262
|
+
console.log(`${icon} Result: ${resultLabel} ${elapsed}`);
|
|
263
|
+
|
|
264
|
+
const stages = event.detail?.stages;
|
|
265
|
+
if (stages) {
|
|
266
|
+
if (stages.researcher?.summary) {
|
|
267
|
+
console.log(` ${ANSI.dim}\ud83d\udd2c Research: ${stages.researcher.summary}${ANSI.reset}`);
|
|
268
|
+
}
|
|
269
|
+
if (stages.planner?.title || stages.planner?.approach || stages.planner?.completedSteps?.length) {
|
|
270
|
+
const planParts = [];
|
|
271
|
+
if (stages.planner.title) planParts.push(stages.planner.title);
|
|
272
|
+
if (stages.planner.approach) planParts.push(`approach: ${stages.planner.approach}`);
|
|
273
|
+
console.log(` ${ANSI.dim}\ud83d\uddfa Plan: ${planParts.join(" | ")}${ANSI.reset}`);
|
|
274
|
+
for (const step of stages.planner.completedSteps || []) {
|
|
275
|
+
console.log(` ${ANSI.dim} \u2713 ${step}${ANSI.reset}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (stages.tester?.summary) {
|
|
279
|
+
console.log(` ${ANSI.dim}\ud83e\uddea Tester: ${stages.tester.summary}${ANSI.reset}`);
|
|
280
|
+
}
|
|
281
|
+
if (stages.security?.summary) {
|
|
282
|
+
console.log(` ${ANSI.dim}\ud83d\udd12 Security: ${stages.security.summary}${ANSI.reset}`);
|
|
283
|
+
}
|
|
284
|
+
if (stages.sonar) {
|
|
285
|
+
const gateLabel = stages.sonar.gateStatus === "OK" ? ANSI.green : ANSI.red;
|
|
286
|
+
console.log(` ${ANSI.dim}\ud83d\udd0d Sonar: ${gateLabel}${stages.sonar.gateStatus}${ANSI.reset}${ANSI.dim} (${stages.sonar.openIssues ?? 0} issues)${ANSI.reset}`);
|
|
287
|
+
if (typeof stages.sonar.issuesInitial === "number" || typeof stages.sonar.issuesResolved === "number") {
|
|
288
|
+
const issuesInitial = stages.sonar.issuesInitial ?? stages.sonar.openIssues ?? 0;
|
|
289
|
+
const issuesFinal = stages.sonar.issuesFinal ?? stages.sonar.openIssues ?? 0;
|
|
290
|
+
const issuesResolved = stages.sonar.issuesResolved ?? Math.max(issuesInitial - issuesFinal, 0);
|
|
291
|
+
console.log(` ${ANSI.dim}\ud83d\udee0 Issues: ${issuesInitial} detected, ${issuesFinal} open, ${issuesResolved} resolved${ANSI.reset}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const git = event.detail?.git;
|
|
297
|
+
if (git?.branch) {
|
|
298
|
+
const parts = [`branch: ${git.branch}`];
|
|
299
|
+
if (git.committed) parts.push("committed");
|
|
300
|
+
if (git.pushed) parts.push("pushed");
|
|
301
|
+
if (git.pr || git.prUrl) parts.push(`PR: ${git.pr || git.prUrl}`);
|
|
302
|
+
console.log(` ${ANSI.dim}\ud83d\udcce Git: ${parts.join(", ")}${ANSI.reset}`);
|
|
303
|
+
if (Array.isArray(git.commits) && git.commits.length > 0) {
|
|
304
|
+
console.log(` ${ANSI.dim}\ud83e\uddfe Commits:${ANSI.reset}`);
|
|
305
|
+
for (const commit of git.commits) {
|
|
306
|
+
const shortHash = (commit.hash || "").slice(0, 7) || "unknown";
|
|
307
|
+
const message = commit.message || "";
|
|
308
|
+
console.log(` ${ANSI.dim} - ${shortHash} ${message}${ANSI.reset}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const budget = event.detail?.budget;
|
|
314
|
+
if (budget) {
|
|
315
|
+
console.log(` ${ANSI.dim}\ud83d\udcb0 Total tokens: ${budget.total_tokens ?? 0}${ANSI.reset}`);
|
|
316
|
+
console.log(` ${ANSI.dim}\ud83d\udcb0 Total cost: $${Number(budget.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`);
|
|
317
|
+
const byRole = budget.breakdown_by_role || {};
|
|
318
|
+
const roles = Object.entries(byRole);
|
|
319
|
+
if (roles.length > 0) {
|
|
320
|
+
for (const [role, metrics] of roles) {
|
|
321
|
+
console.log(
|
|
322
|
+
` ${ANSI.dim} - ${role}: ${metrics.total_tokens ?? 0} tokens, $${Number(metrics.total_cost_usd || 0).toFixed(2)}${ANSI.reset}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(`${ANSI.dim}Session: ${event.sessionId}${ANSI.reset}`);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
case "question":
|
|
333
|
+
console.log();
|
|
334
|
+
console.log(`${ANSI.bold}${ANSI.yellow}${icon} Paused - question:${ANSI.reset}`);
|
|
335
|
+
console.log(` ${event.detail?.question || event.message}`);
|
|
336
|
+
console.log(`${ANSI.dim}Resume with: kj resume ${event.sessionId} --answer "<response>"${ANSI.reset}`);
|
|
337
|
+
break;
|
|
338
|
+
|
|
339
|
+
case "agent:output":
|
|
340
|
+
console.log(` \u2502 ${ANSI.dim}${event.message}${ANSI.reset}`);
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
default:
|
|
344
|
+
console.log(` \u251c\u2500 ${icon} ${event.message || event.type} ${elapsed}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline event helpers.
|
|
3
|
+
* Extracted from orchestrator.js for reuse across modules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function emitProgress(emitter, data) {
|
|
7
|
+
if (!emitter) return;
|
|
8
|
+
emitter.emit("progress", data);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function makeEvent(type, base, extra = {}) {
|
|
12
|
+
return {
|
|
13
|
+
type,
|
|
14
|
+
sessionId: base.sessionId,
|
|
15
|
+
iteration: base.iteration,
|
|
16
|
+
stage: base.stage,
|
|
17
|
+
status: extra.status || "ok",
|
|
18
|
+
message: extra.message || type,
|
|
19
|
+
detail: extra.detail || {},
|
|
20
|
+
elapsed: base.startedAt ? Date.now() - base.startedAt : 0,
|
|
21
|
+
timestamp: new Date().toISOString()
|
|
22
|
+
};
|
|
23
|
+
}
|
package/src/utils/fs.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function ensureDir(dir) {
|
|
5
|
+
await fs.mkdir(dir, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function exists(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.access(filePath);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveFromCwd(...parts) {
|
|
18
|
+
return path.resolve(process.cwd(), ...parts);
|
|
19
|
+
}
|
package/src/utils/git.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { runCommand } from "./process.js";
|
|
2
|
+
|
|
3
|
+
function slugifyTask(task) {
|
|
4
|
+
return String(task)
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
7
|
+
.replace(/^-+|-+$/g, "")
|
|
8
|
+
.slice(0, 40);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function runGit(args, options = {}) {
|
|
12
|
+
const res = await runCommand("git", args, options);
|
|
13
|
+
if (res.exitCode !== 0) {
|
|
14
|
+
throw new Error(`git ${args.join(" ")} failed: ${res.stderr || res.stdout}`);
|
|
15
|
+
}
|
|
16
|
+
return res.stdout.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function ensureGitRepo() {
|
|
20
|
+
const res = await runCommand("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
21
|
+
return res.exitCode === 0 && res.stdout.trim() === "true";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function currentBranch() {
|
|
25
|
+
return runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function fetchBase(baseBranch) {
|
|
29
|
+
await runGit(["fetch", "origin", baseBranch]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function revParse(ref) {
|
|
33
|
+
return runGit(["rev-parse", ref]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function syncBaseBranch({ baseBranch, autoRebase }) {
|
|
37
|
+
const local = await revParse(baseBranch);
|
|
38
|
+
const remote = await revParse(`origin/${baseBranch}`);
|
|
39
|
+
if (local === remote) return { synced: true, rebased: false };
|
|
40
|
+
|
|
41
|
+
if (!autoRebase) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Base branch '${baseBranch}' is behind origin/${baseBranch}. Re-run with auto-rebase enabled or rebase manually.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await runGit(["rebase", `origin/${baseBranch}`]);
|
|
48
|
+
return { synced: true, rebased: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function ensureBranchUpToDateWithBase({ branch, baseBranch, autoRebase }) {
|
|
52
|
+
const mergeBase = await runGit(["merge-base", branch, `origin/${baseBranch}`]);
|
|
53
|
+
const remoteBase = await revParse(`origin/${baseBranch}`);
|
|
54
|
+
if (mergeBase === remoteBase) return { upToDate: true, rebased: false };
|
|
55
|
+
|
|
56
|
+
if (!autoRebase) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Base branch '${baseBranch}' advanced during run. Re-run with auto-rebase enabled or rebase '${branch}' manually.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await runGit(["rebase", `origin/${baseBranch}`]);
|
|
63
|
+
return { upToDate: true, rebased: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function createBranch(branchName) {
|
|
67
|
+
await runGit(["checkout", "-b", branchName]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildBranchName(prefix, task) {
|
|
71
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
|
|
72
|
+
return `${prefix}${slugifyTask(task) || "task"}-${stamp}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function hasChanges() {
|
|
76
|
+
const status = await runGit(["status", "--porcelain"]);
|
|
77
|
+
return status.length > 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function commitAll(message) {
|
|
81
|
+
await runGit(["add", "-A"]);
|
|
82
|
+
const changed = await hasChanges();
|
|
83
|
+
if (!changed) return { committed: false };
|
|
84
|
+
await runGit(["commit", "-m", message]);
|
|
85
|
+
const raw = await runGit(["log", "-1", "--pretty=format:%H%x1f%s"]);
|
|
86
|
+
const [hash, commitMessage] = raw.split("\x1f");
|
|
87
|
+
return { committed: true, commit: { hash, message: commitMessage } };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function pushBranch(branch) {
|
|
91
|
+
await runGit(["push", "-u", "origin", branch]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function createPullRequest({ baseBranch, branch, title, body }) {
|
|
95
|
+
const args = ["pr", "create", "--base", baseBranch, "--head", branch, "--title", title, "--body", body];
|
|
96
|
+
const res = await runCommand("gh", args);
|
|
97
|
+
if (res.exitCode !== 0) {
|
|
98
|
+
throw new Error(`gh ${args.join(" ")} failed: ${res.stderr || res.stdout}`);
|
|
99
|
+
}
|
|
100
|
+
return res.stdout.trim();
|
|
101
|
+
}
|