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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +441 -0
  3. package/docs/karajan-code-logo-small.png +0 -0
  4. package/package.json +60 -0
  5. package/scripts/install.js +898 -0
  6. package/scripts/install.sh +7 -0
  7. package/scripts/postinstall.js +117 -0
  8. package/scripts/setup-multi-instance.sh +150 -0
  9. package/src/activity-log.js +59 -0
  10. package/src/agents/aider-agent.js +25 -0
  11. package/src/agents/availability.js +32 -0
  12. package/src/agents/base-agent.js +27 -0
  13. package/src/agents/claude-agent.js +24 -0
  14. package/src/agents/codex-agent.js +27 -0
  15. package/src/agents/gemini-agent.js +25 -0
  16. package/src/agents/index.js +19 -0
  17. package/src/agents/resolve-bin.js +60 -0
  18. package/src/cli.js +200 -0
  19. package/src/commands/code.js +32 -0
  20. package/src/commands/config.js +74 -0
  21. package/src/commands/doctor.js +155 -0
  22. package/src/commands/init.js +181 -0
  23. package/src/commands/plan.js +67 -0
  24. package/src/commands/report.js +340 -0
  25. package/src/commands/resume.js +39 -0
  26. package/src/commands/review.js +26 -0
  27. package/src/commands/roles.js +117 -0
  28. package/src/commands/run.js +91 -0
  29. package/src/commands/scan.js +18 -0
  30. package/src/commands/sonar.js +53 -0
  31. package/src/config.js +322 -0
  32. package/src/git/automation.js +100 -0
  33. package/src/mcp/progress.js +69 -0
  34. package/src/mcp/run-kj.js +87 -0
  35. package/src/mcp/server-handlers.js +259 -0
  36. package/src/mcp/server.js +37 -0
  37. package/src/mcp/tool-arg-normalizers.js +16 -0
  38. package/src/mcp/tools.js +184 -0
  39. package/src/orchestrator.js +1277 -0
  40. package/src/planning-game/adapter.js +105 -0
  41. package/src/planning-game/client.js +81 -0
  42. package/src/prompts/coder.js +60 -0
  43. package/src/prompts/planner.js +26 -0
  44. package/src/prompts/reviewer.js +45 -0
  45. package/src/repeat-detector.js +77 -0
  46. package/src/review/diff-generator.js +22 -0
  47. package/src/review/parser.js +93 -0
  48. package/src/review/profiles.js +66 -0
  49. package/src/review/schema.js +31 -0
  50. package/src/review/tdd-policy.js +57 -0
  51. package/src/roles/base-role.js +127 -0
  52. package/src/roles/coder-role.js +60 -0
  53. package/src/roles/commiter-role.js +94 -0
  54. package/src/roles/index.js +12 -0
  55. package/src/roles/planner-role.js +81 -0
  56. package/src/roles/refactorer-role.js +66 -0
  57. package/src/roles/researcher-role.js +134 -0
  58. package/src/roles/reviewer-role.js +132 -0
  59. package/src/roles/security-role.js +128 -0
  60. package/src/roles/solomon-role.js +199 -0
  61. package/src/roles/sonar-role.js +65 -0
  62. package/src/roles/tester-role.js +114 -0
  63. package/src/roles/triage-role.js +128 -0
  64. package/src/session-store.js +80 -0
  65. package/src/sonar/api.js +78 -0
  66. package/src/sonar/enforcer.js +19 -0
  67. package/src/sonar/manager.js +163 -0
  68. package/src/sonar/project-key.js +83 -0
  69. package/src/sonar/scanner.js +267 -0
  70. package/src/utils/agent-detect.js +32 -0
  71. package/src/utils/budget.js +123 -0
  72. package/src/utils/display.js +346 -0
  73. package/src/utils/events.js +23 -0
  74. package/src/utils/fs.js +19 -0
  75. package/src/utils/git.js +101 -0
  76. package/src/utils/logger.js +86 -0
  77. package/src/utils/paths.js +18 -0
  78. package/src/utils/pricing.js +28 -0
  79. package/src/utils/process.js +67 -0
  80. package/src/utils/wizard.js +41 -0
  81. package/templates/coder-rules.md +24 -0
  82. package/templates/docker-compose.sonar.yml +60 -0
  83. package/templates/kj.config.yml +82 -0
  84. package/templates/review-rules.md +11 -0
  85. package/templates/roles/coder.md +42 -0
  86. package/templates/roles/commiter.md +44 -0
  87. package/templates/roles/planner.md +45 -0
  88. package/templates/roles/refactorer.md +39 -0
  89. package/templates/roles/researcher.md +37 -0
  90. package/templates/roles/reviewer-paranoid.md +38 -0
  91. package/templates/roles/reviewer-relaxed.md +34 -0
  92. package/templates/roles/reviewer-strict.md +37 -0
  93. package/templates/roles/reviewer.md +55 -0
  94. package/templates/roles/security.md +54 -0
  95. package/templates/roles/solomon.md +106 -0
  96. package/templates/roles/sonar.md +49 -0
  97. package/templates/roles/tester.md +41 -0
  98. 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
+ }