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,127 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
4
|
+
|
|
5
|
+
const ROLE_EVENTS = {
|
|
6
|
+
START: "role:start",
|
|
7
|
+
END: "role:end",
|
|
8
|
+
ERROR: "role:error"
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function resolveRoleMdPath(roleName, projectDir) {
|
|
12
|
+
const fileName = `${roleName}.md`;
|
|
13
|
+
const candidates = [];
|
|
14
|
+
|
|
15
|
+
if (projectDir) {
|
|
16
|
+
candidates.push(path.join(projectDir, ".karajan", "roles", fileName));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
candidates.push(path.join(getKarajanHome(), "roles", fileName));
|
|
20
|
+
|
|
21
|
+
const builtIn = path.resolve(
|
|
22
|
+
path.dirname(new URL(import.meta.url).pathname),
|
|
23
|
+
"..",
|
|
24
|
+
"..",
|
|
25
|
+
"templates",
|
|
26
|
+
"roles",
|
|
27
|
+
fileName
|
|
28
|
+
);
|
|
29
|
+
candidates.push(builtIn);
|
|
30
|
+
|
|
31
|
+
return candidates;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function loadFirstExisting(paths) {
|
|
35
|
+
for (const p of paths) {
|
|
36
|
+
try {
|
|
37
|
+
return await fs.readFile(p, "utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class BaseRole {
|
|
46
|
+
constructor({ name, config, logger, emitter = null }) {
|
|
47
|
+
if (!name) throw new Error("Role name is required");
|
|
48
|
+
this.name = name;
|
|
49
|
+
this.config = config || {};
|
|
50
|
+
this.logger = logger;
|
|
51
|
+
this.emitter = emitter;
|
|
52
|
+
this.instructions = null;
|
|
53
|
+
this._initialized = false;
|
|
54
|
+
this._startTime = null;
|
|
55
|
+
this._output = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async init(context = {}) {
|
|
59
|
+
this.context = context;
|
|
60
|
+
const projectDir = this.config.projectDir || process.cwd();
|
|
61
|
+
const paths = resolveRoleMdPath(this.name, projectDir);
|
|
62
|
+
this.instructions = await loadFirstExisting(paths);
|
|
63
|
+
this._initialized = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async execute(_input) {
|
|
67
|
+
throw new Error(`${this.name}: execute() not implemented`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
report() {
|
|
71
|
+
return {
|
|
72
|
+
role: this.name,
|
|
73
|
+
ok: this._output?.ok ?? false,
|
|
74
|
+
result: this._output?.result ?? null,
|
|
75
|
+
summary: this._output?.summary ?? "",
|
|
76
|
+
timestamp: new Date().toISOString()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
validate(output) {
|
|
81
|
+
if (!output) return { valid: false, reason: "Output is null or undefined" };
|
|
82
|
+
if (typeof output.ok !== "boolean") return { valid: false, reason: "Output.ok must be a boolean" };
|
|
83
|
+
return { valid: true, reason: "" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async run(input) {
|
|
87
|
+
this._ensureInitialized();
|
|
88
|
+
this._startTime = Date.now();
|
|
89
|
+
this._emitEvent(ROLE_EVENTS.START, { input });
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const output = await this.execute(input);
|
|
93
|
+
this._output = output;
|
|
94
|
+
|
|
95
|
+
const validation = this.validate(output);
|
|
96
|
+
if (!validation.valid) {
|
|
97
|
+
throw new Error(`${this.name} output validation failed: ${validation.reason}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this._emitEvent(ROLE_EVENTS.END, { output: this.report() });
|
|
101
|
+
return output;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
this._emitEvent(ROLE_EVENTS.ERROR, { error: error.message });
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_ensureInitialized() {
|
|
109
|
+
if (!this._initialized) {
|
|
110
|
+
throw new Error(`${this.name}: init() must be called before run()`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_emitEvent(type, detail = {}) {
|
|
115
|
+
if (!this.emitter) return;
|
|
116
|
+
this.emitter.emit(type, {
|
|
117
|
+
role: this.name,
|
|
118
|
+
iteration: this.context?.iteration ?? 0,
|
|
119
|
+
sessionId: this.context?.sessionId ?? null,
|
|
120
|
+
elapsed: this._startTime ? Date.now() - this._startTime : 0,
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
...detail
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { ROLE_EVENTS, resolveRoleMdPath, loadFirstExisting };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
import { buildCoderPrompt } from "../prompts/coder.js";
|
|
4
|
+
|
|
5
|
+
function resolveProvider(config) {
|
|
6
|
+
return (
|
|
7
|
+
config?.roles?.coder?.provider ||
|
|
8
|
+
config?.coder ||
|
|
9
|
+
"claude"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class CoderRole extends BaseRole {
|
|
14
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
15
|
+
super({ name: "coder", config, logger, emitter });
|
|
16
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async execute(input) {
|
|
20
|
+
const { task, reviewerFeedback, sonarSummary, onOutput } = typeof input === "string"
|
|
21
|
+
? { task: input, reviewerFeedback: null, sonarSummary: null, onOutput: null }
|
|
22
|
+
: input || {};
|
|
23
|
+
|
|
24
|
+
const provider = resolveProvider(this.config);
|
|
25
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
26
|
+
|
|
27
|
+
const prompt = buildCoderPrompt({
|
|
28
|
+
task: task || this.context?.task || "",
|
|
29
|
+
reviewerFeedback: reviewerFeedback || null,
|
|
30
|
+
sonarSummary: sonarSummary || null,
|
|
31
|
+
coderRules: this.instructions,
|
|
32
|
+
methodology: this.config?.development?.methodology || "tdd"
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const coderArgs = { prompt, role: "coder" };
|
|
36
|
+
if (onOutput) coderArgs.onOutput = onOutput;
|
|
37
|
+
|
|
38
|
+
const result = await agent.runTask(coderArgs);
|
|
39
|
+
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
result: {
|
|
44
|
+
error: result.error || result.output || "Coder failed",
|
|
45
|
+
provider
|
|
46
|
+
},
|
|
47
|
+
summary: `Coder failed: ${result.error || result.output || "unknown error"}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
result: {
|
|
54
|
+
output: result.output || "",
|
|
55
|
+
provider
|
|
56
|
+
},
|
|
57
|
+
summary: "Coder completed"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import {
|
|
3
|
+
ensureGitRepo,
|
|
4
|
+
currentBranch,
|
|
5
|
+
fetchBase,
|
|
6
|
+
ensureBranchUpToDateWithBase,
|
|
7
|
+
hasChanges,
|
|
8
|
+
commitAll,
|
|
9
|
+
pushBranch,
|
|
10
|
+
createPullRequest,
|
|
11
|
+
revParse
|
|
12
|
+
} from "../utils/git.js";
|
|
13
|
+
|
|
14
|
+
function buildCommitMessage(task) {
|
|
15
|
+
const clean = String(task || "")
|
|
16
|
+
.replace(/\s+/g, " ")
|
|
17
|
+
.trim();
|
|
18
|
+
const prefix = "feat: ";
|
|
19
|
+
const maxBody = 72 - prefix.length;
|
|
20
|
+
return `${prefix}${clean.slice(0, maxBody) || "update"}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildPrTitle(task) {
|
|
24
|
+
const clean = String(task || "")
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.trim();
|
|
27
|
+
return clean.slice(0, 70) || "Karajan update";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class CommiterRole extends BaseRole {
|
|
31
|
+
constructor({ config, logger, emitter = null }) {
|
|
32
|
+
super({ name: "commiter", config, logger, emitter });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async execute(input) {
|
|
36
|
+
const { task, commitMessage, push = false, createPr = false } = input || {};
|
|
37
|
+
|
|
38
|
+
const isRepo = await ensureGitRepo();
|
|
39
|
+
if (!isRepo) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
result: { error: "Current directory is not a git repository" },
|
|
43
|
+
summary: "Commiter failed: not a git repo"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const branch = await currentBranch();
|
|
48
|
+
const changes = await hasChanges();
|
|
49
|
+
|
|
50
|
+
if (!changes) {
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
result: { branch, committed: false, commitHash: null, prUrl: null },
|
|
54
|
+
summary: "No changes to commit"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const msg = commitMessage || buildCommitMessage(task);
|
|
59
|
+
await commitAll(msg);
|
|
60
|
+
const commitHash = await revParse("HEAD");
|
|
61
|
+
|
|
62
|
+
if (push) {
|
|
63
|
+
const baseBranch = this.config.base_branch || "main";
|
|
64
|
+
await fetchBase(baseBranch);
|
|
65
|
+
await ensureBranchUpToDateWithBase({
|
|
66
|
+
branch,
|
|
67
|
+
baseBranch,
|
|
68
|
+
autoRebase: true
|
|
69
|
+
});
|
|
70
|
+
await pushBranch(branch);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let prUrl = null;
|
|
74
|
+
if (push && createPr) {
|
|
75
|
+
const baseBranch = this.config.base_branch || "main";
|
|
76
|
+
prUrl = await createPullRequest({
|
|
77
|
+
baseBranch,
|
|
78
|
+
branch,
|
|
79
|
+
title: buildPrTitle(task),
|
|
80
|
+
body: `Task: ${task || "N/A"}`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const parts = [`Committed on ${branch}`];
|
|
85
|
+
if (push) parts.push("pushed");
|
|
86
|
+
if (prUrl) parts.push(`PR: ${prUrl}`);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
result: { branch, committed: true, commitHash, prUrl },
|
|
91
|
+
summary: parts.join(", ")
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { BaseRole, ROLE_EVENTS, resolveRoleMdPath, loadFirstExisting } from "./base-role.js";
|
|
2
|
+
export { PlannerRole } from "./planner-role.js";
|
|
3
|
+
export { ReviewerRole } from "./reviewer-role.js";
|
|
4
|
+
export { RefactorerRole } from "./refactorer-role.js";
|
|
5
|
+
export { CommiterRole } from "./commiter-role.js";
|
|
6
|
+
export { CoderRole } from "./coder-role.js";
|
|
7
|
+
export { SonarRole } from "./sonar-role.js";
|
|
8
|
+
export { ResearcherRole } from "./researcher-role.js";
|
|
9
|
+
export { TriageRole } from "./triage-role.js";
|
|
10
|
+
export { TesterRole } from "./tester-role.js";
|
|
11
|
+
export { SecurityRole } from "./security-role.js";
|
|
12
|
+
export { SolomonRole } from "./solomon-role.js";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
|
|
4
|
+
function resolveProvider(config) {
|
|
5
|
+
return (
|
|
6
|
+
config?.roles?.planner?.provider ||
|
|
7
|
+
config?.roles?.coder?.provider ||
|
|
8
|
+
"claude"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildPrompt({ task, instructions, research }) {
|
|
13
|
+
const sections = [];
|
|
14
|
+
|
|
15
|
+
if (instructions) {
|
|
16
|
+
sections.push(instructions);
|
|
17
|
+
sections.push("");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
sections.push("Create an implementation plan for this task.");
|
|
21
|
+
sections.push("Return concise numbered steps focused on execution order and risk.");
|
|
22
|
+
sections.push("");
|
|
23
|
+
|
|
24
|
+
if (research) {
|
|
25
|
+
sections.push("## Research findings");
|
|
26
|
+
if (research.affected_files?.length) {
|
|
27
|
+
sections.push(`Affected files: ${research.affected_files.join(", ")}`);
|
|
28
|
+
}
|
|
29
|
+
if (research.patterns?.length) {
|
|
30
|
+
sections.push(`Patterns: ${research.patterns.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
if (research.constraints?.length) {
|
|
33
|
+
sections.push(`Constraints: ${research.constraints.join(", ")}`);
|
|
34
|
+
}
|
|
35
|
+
if (research.risks?.length) {
|
|
36
|
+
sections.push(`Risks: ${research.risks.join(", ")}`);
|
|
37
|
+
}
|
|
38
|
+
if (research.prior_decisions?.length) {
|
|
39
|
+
sections.push(`Prior decisions: ${research.prior_decisions.join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
sections.push("");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
sections.push("## Task");
|
|
45
|
+
sections.push(task);
|
|
46
|
+
|
|
47
|
+
return sections.join("\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class PlannerRole extends BaseRole {
|
|
51
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
52
|
+
super({ name: "planner", config, logger, emitter });
|
|
53
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async execute(input) {
|
|
57
|
+
const task = input || this.context?.task || "";
|
|
58
|
+
const research = this.context?.research || null;
|
|
59
|
+
const provider = resolveProvider(this.config);
|
|
60
|
+
|
|
61
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
62
|
+
const prompt = buildPrompt({ task, instructions: this.instructions, research });
|
|
63
|
+
|
|
64
|
+
const result = await agent.runTask({ prompt, role: "planner" });
|
|
65
|
+
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
result: { error: result.error || result.output || "Planner agent failed", plan: null },
|
|
70
|
+
summary: `Planner failed: ${result.error || "unknown error"}`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const plan = result.output?.trim() || "";
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
result: { plan, provider },
|
|
78
|
+
summary: plan ? `Plan generated (${plan.split("\n").length} lines)` : "Empty plan generated"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
|
|
4
|
+
function resolveProvider(config) {
|
|
5
|
+
return (
|
|
6
|
+
config?.roles?.refactorer?.provider ||
|
|
7
|
+
config?.roles?.coder?.provider ||
|
|
8
|
+
"claude"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildPrompt({ task, instructions }) {
|
|
13
|
+
const sections = [];
|
|
14
|
+
|
|
15
|
+
if (instructions) {
|
|
16
|
+
sections.push(instructions);
|
|
17
|
+
sections.push("");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
sections.push("Refactor the current changes for clarity and maintainability without changing behavior.");
|
|
21
|
+
sections.push("Do not expand scope and keep tests green.");
|
|
22
|
+
sections.push("");
|
|
23
|
+
sections.push("## Task context");
|
|
24
|
+
sections.push(task);
|
|
25
|
+
|
|
26
|
+
return sections.join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class RefactorerRole extends BaseRole {
|
|
30
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
31
|
+
super({ name: "refactorer", config, logger, emitter });
|
|
32
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async execute(input) {
|
|
36
|
+
const task = typeof input === "string"
|
|
37
|
+
? input
|
|
38
|
+
: input?.task || this.context?.task || "";
|
|
39
|
+
|
|
40
|
+
const provider = resolveProvider(this.config);
|
|
41
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
42
|
+
|
|
43
|
+
const prompt = buildPrompt({ task, instructions: this.instructions });
|
|
44
|
+
const result = await agent.runTask({ prompt, role: "refactorer" });
|
|
45
|
+
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
result: {
|
|
50
|
+
error: result.error || result.output || "Refactorer failed",
|
|
51
|
+
provider
|
|
52
|
+
},
|
|
53
|
+
summary: `Refactorer failed: ${result.error || "unknown error"}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
result: {
|
|
60
|
+
output: result.output?.trim() || "",
|
|
61
|
+
provider
|
|
62
|
+
},
|
|
63
|
+
summary: "Refactoring applied"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
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 researching the codebase."
|
|
8
|
+
].join(" ");
|
|
9
|
+
|
|
10
|
+
function resolveProvider(config) {
|
|
11
|
+
return (
|
|
12
|
+
config?.roles?.researcher?.provider ||
|
|
13
|
+
config?.roles?.coder?.provider ||
|
|
14
|
+
"claude"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildPrompt({ task, instructions }) {
|
|
19
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
20
|
+
|
|
21
|
+
if (instructions) {
|
|
22
|
+
sections.push(instructions);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
sections.push(
|
|
26
|
+
"Investigate the codebase for the following task.",
|
|
27
|
+
"Identify affected files, patterns, constraints, prior decisions, risks, and test coverage.",
|
|
28
|
+
"Return a single valid JSON object with your findings and nothing else.",
|
|
29
|
+
'JSON schema: {"affected_files":[string],"patterns":[string],"constraints":[string],"prior_decisions":[string],"risks":[string],"test_coverage":string}'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
sections.push(`## Task\n${task}`);
|
|
33
|
+
|
|
34
|
+
return sections.join("\n\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseResearchOutput(raw) {
|
|
38
|
+
const text = raw?.trim() || "";
|
|
39
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
40
|
+
if (!jsonMatch) return null;
|
|
41
|
+
return JSON.parse(jsonMatch[0]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildSummary(parsed) {
|
|
45
|
+
const files = parsed.affected_files?.length || 0;
|
|
46
|
+
const risks = parsed.risks?.length || 0;
|
|
47
|
+
const patterns = parsed.patterns?.length || 0;
|
|
48
|
+
const parts = [];
|
|
49
|
+
if (files) parts.push(`${files} file${files !== 1 ? "s" : ""}`);
|
|
50
|
+
if (risks) parts.push(`${risks} risk${risks !== 1 ? "s" : ""}`);
|
|
51
|
+
if (patterns) parts.push(`${patterns} pattern${patterns !== 1 ? "s" : ""}`);
|
|
52
|
+
return parts.length
|
|
53
|
+
? `Research complete: ${parts.join(", ")} identified`
|
|
54
|
+
: "Research complete";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class ResearcherRole extends BaseRole {
|
|
58
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
59
|
+
super({ name: "researcher", config, logger, emitter });
|
|
60
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async execute(input) {
|
|
64
|
+
const task = typeof input === "string"
|
|
65
|
+
? input
|
|
66
|
+
: input?.task || this.context?.task || "";
|
|
67
|
+
|
|
68
|
+
const provider = resolveProvider(this.config);
|
|
69
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
70
|
+
|
|
71
|
+
const prompt = buildPrompt({ task, instructions: this.instructions });
|
|
72
|
+
const result = await agent.runTask({ prompt, role: "researcher" });
|
|
73
|
+
|
|
74
|
+
if (!result.ok) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
result: {
|
|
78
|
+
error: result.error || result.output || "Researcher failed",
|
|
79
|
+
provider
|
|
80
|
+
},
|
|
81
|
+
summary: `Researcher failed: ${result.error || "unknown error"}`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const parsed = parseResearchOutput(result.output);
|
|
87
|
+
if (!parsed) {
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
result: {
|
|
91
|
+
affected_files: [],
|
|
92
|
+
patterns: [],
|
|
93
|
+
constraints: [],
|
|
94
|
+
prior_decisions: [],
|
|
95
|
+
risks: [],
|
|
96
|
+
test_coverage: "",
|
|
97
|
+
raw: result.output,
|
|
98
|
+
provider
|
|
99
|
+
},
|
|
100
|
+
summary: "Research complete (unstructured output)"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
result: {
|
|
107
|
+
affected_files: parsed.affected_files || [],
|
|
108
|
+
patterns: parsed.patterns || [],
|
|
109
|
+
constraints: parsed.constraints || [],
|
|
110
|
+
prior_decisions: parsed.prior_decisions || [],
|
|
111
|
+
risks: parsed.risks || [],
|
|
112
|
+
test_coverage: parsed.test_coverage || "",
|
|
113
|
+
provider
|
|
114
|
+
},
|
|
115
|
+
summary: buildSummary(parsed)
|
|
116
|
+
};
|
|
117
|
+
} catch {
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
result: {
|
|
121
|
+
affected_files: [],
|
|
122
|
+
patterns: [],
|
|
123
|
+
constraints: [],
|
|
124
|
+
prior_decisions: [],
|
|
125
|
+
risks: [],
|
|
126
|
+
test_coverage: "",
|
|
127
|
+
raw: result.output,
|
|
128
|
+
provider
|
|
129
|
+
},
|
|
130
|
+
summary: "Research complete (unstructured output)"
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
|
|
4
|
+
const MAX_DIFF_LENGTH = 12000;
|
|
5
|
+
|
|
6
|
+
const SUBAGENT_PREAMBLE = [
|
|
7
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
8
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
9
|
+
"Do NOT use any MCP tools. Focus only on reviewing the code."
|
|
10
|
+
].join(" ");
|
|
11
|
+
|
|
12
|
+
function resolveProvider(config) {
|
|
13
|
+
return (
|
|
14
|
+
config?.roles?.reviewer?.provider ||
|
|
15
|
+
config?.roles?.coder?.provider ||
|
|
16
|
+
"claude"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function truncateDiff(diff) {
|
|
21
|
+
if (!diff) return "";
|
|
22
|
+
return diff.length > MAX_DIFF_LENGTH
|
|
23
|
+
? `${diff.slice(0, MAX_DIFF_LENGTH)}\n\n[TRUNCATED]`
|
|
24
|
+
: diff;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildPrompt({ task, diff, reviewRules, reviewMode, instructions }) {
|
|
28
|
+
const sections = [];
|
|
29
|
+
|
|
30
|
+
sections.push(SUBAGENT_PREAMBLE);
|
|
31
|
+
|
|
32
|
+
if (instructions) {
|
|
33
|
+
sections.push(instructions);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
sections.push(`You are a code reviewer in ${reviewMode || "standard"} mode.`);
|
|
37
|
+
sections.push(
|
|
38
|
+
"Return only one valid JSON object and nothing else.",
|
|
39
|
+
'JSON schema:',
|
|
40
|
+
'{"approved":boolean,"blocking_issues":[{"id":string,"severity":"critical|high|medium|low","file":string,"line":number,"description":string,"suggested_fix":string}],"non_blocking_suggestions":[string],"summary":string,"confidence":number}'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
sections.push(`Task context:\n${task}`);
|
|
44
|
+
|
|
45
|
+
if (reviewRules) {
|
|
46
|
+
sections.push(`Review rules:\n${reviewRules}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
sections.push(`Git diff:\n${truncateDiff(diff)}`);
|
|
50
|
+
|
|
51
|
+
return sections.join("\n\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseReviewOutput(raw) {
|
|
55
|
+
const text = raw?.trim() || "";
|
|
56
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
57
|
+
if (!jsonMatch) {
|
|
58
|
+
throw new Error(`Failed to parse reviewer output: no JSON found`);
|
|
59
|
+
}
|
|
60
|
+
return JSON.parse(jsonMatch[0]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class ReviewerRole extends BaseRole {
|
|
64
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
65
|
+
super({ name: "reviewer", config, logger, emitter });
|
|
66
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async execute(input) {
|
|
70
|
+
const { task, diff, reviewRules, onOutput } = typeof input === "string"
|
|
71
|
+
? { task: input, diff: "", reviewRules: null, onOutput: null }
|
|
72
|
+
: input;
|
|
73
|
+
|
|
74
|
+
const provider = resolveProvider(this.config);
|
|
75
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
76
|
+
|
|
77
|
+
const prompt = buildPrompt({
|
|
78
|
+
task: task || this.context?.task || "",
|
|
79
|
+
diff: diff || "",
|
|
80
|
+
reviewRules: reviewRules || null,
|
|
81
|
+
reviewMode: this.config?.review_mode || "standard",
|
|
82
|
+
instructions: this.instructions
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const reviewArgs = { prompt, role: "reviewer" };
|
|
86
|
+
if (onOutput) reviewArgs.onOutput = onOutput;
|
|
87
|
+
|
|
88
|
+
const result = await agent.reviewTask(reviewArgs);
|
|
89
|
+
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
result: {
|
|
94
|
+
error: result.error || result.output || "Reviewer agent failed",
|
|
95
|
+
approved: false,
|
|
96
|
+
blocking_issues: []
|
|
97
|
+
},
|
|
98
|
+
summary: `Reviewer failed: ${result.error || "unknown error"}`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const parsed = parseReviewOutput(result.output);
|
|
104
|
+
const approved = Boolean(parsed.approved);
|
|
105
|
+
const blockingIssues = parsed.blocking_issues || [];
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
result: {
|
|
110
|
+
approved,
|
|
111
|
+
blocking_issues: blockingIssues,
|
|
112
|
+
non_blocking_suggestions: parsed.non_blocking_suggestions || [],
|
|
113
|
+
confidence: parsed.confidence ?? null,
|
|
114
|
+
raw_summary: parsed.summary || ""
|
|
115
|
+
},
|
|
116
|
+
summary: approved
|
|
117
|
+
? `Approved: ${parsed.summary || "no issues found"}`
|
|
118
|
+
: `Rejected: ${blockingIssues.length} blocking issue(s) — ${parsed.summary || ""}`
|
|
119
|
+
};
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
result: {
|
|
124
|
+
error: `Failed to parse reviewer output: ${err.message}`,
|
|
125
|
+
approved: false,
|
|
126
|
+
blocking_issues: []
|
|
127
|
+
},
|
|
128
|
+
summary: `Reviewer output parse error: ${err.message}`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|