karajan-code 1.59.0 → 2.0.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 (115) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/scripts/install.js +18 -15
  4. package/src/agents/aider-agent.js +0 -7
  5. package/src/agents/claude-agent.js +34 -6
  6. package/src/agents/codex-agent.js +2 -6
  7. package/src/agents/gemini-agent.js +0 -4
  8. package/src/agents/opencode-agent.js +0 -4
  9. package/src/{becaria → ci}/dispatch.js +13 -13
  10. package/src/{becaria → ci}/pr-diff.js +1 -1
  11. package/src/cli.js +3 -2
  12. package/src/commands/agents.js +0 -1
  13. package/src/commands/doctor.js +22 -40
  14. package/src/commands/init.js +8 -8
  15. package/src/commands/report.js +0 -13
  16. package/src/commands/review.js +17 -17
  17. package/src/commands/run.js +1 -2
  18. package/src/commands/status.js +1 -7
  19. package/src/config.js +10 -52
  20. package/src/domains/domain-loader.js +85 -4
  21. package/src/git/automation.js +9 -9
  22. package/src/guards/output-guard.js +0 -17
  23. package/src/mcp/handlers/direct-handlers.js +1 -1
  24. package/src/mcp/handlers/run-handler.js +1 -2
  25. package/src/mcp/sovereignty-guard.js +2 -2
  26. package/src/mcp/tools.js +3 -2
  27. package/src/orchestrator/brain-coordinator.js +165 -0
  28. package/src/orchestrator/ci-integration.js +139 -0
  29. package/src/orchestrator/config-init.js +435 -0
  30. package/src/orchestrator/direct-actions.js +186 -0
  31. package/src/orchestrator/feedback-enrichment.js +140 -0
  32. package/src/orchestrator/feedback-queue.js +152 -0
  33. package/src/orchestrator/flow-control.js +159 -0
  34. package/src/orchestrator/post-loop-stages.js +20 -15
  35. package/src/orchestrator/preflight-checks.js +7 -3
  36. package/src/orchestrator/role-output-compressor.js +190 -0
  37. package/src/orchestrator/session-journal.js +344 -0
  38. package/src/orchestrator/solomon-escalation.js +38 -5
  39. package/src/orchestrator/solomon-rules.js +32 -33
  40. package/src/orchestrator/stages/reviewer-stage.js +16 -5
  41. package/src/orchestrator/stages/sonar-stage.js +24 -1
  42. package/src/orchestrator/verification-gate.js +146 -0
  43. package/src/orchestrator.js +435 -661
  44. package/src/planning-game/client.js +2 -2
  45. package/src/planning-game/pipeline-adapter.js +1 -1
  46. package/src/prompts/coder.js +17 -3
  47. package/src/prompts/reviewer.js +2 -2
  48. package/src/prompts/rtk-snippet.js +3 -5
  49. package/src/review/diff-generator.js +66 -17
  50. package/src/review/snapshot-diff.js +122 -0
  51. package/src/review/tdd-policy.js +4 -2
  52. package/src/roles/agent-role.js +135 -0
  53. package/src/roles/architect-role.js +61 -108
  54. package/src/roles/audit-role.js +25 -71
  55. package/src/roles/coder-role.js +32 -57
  56. package/src/roles/discover-role.js +30 -71
  57. package/src/roles/hu-reviewer-role.js +51 -99
  58. package/src/roles/impeccable-role.js +34 -91
  59. package/src/roles/index.js +2 -0
  60. package/src/roles/karajan-brain-role.js +134 -0
  61. package/src/roles/planner-role.js +40 -75
  62. package/src/roles/refactorer-role.js +22 -64
  63. package/src/roles/researcher-role.js +52 -118
  64. package/src/roles/reviewer-role.js +69 -126
  65. package/src/roles/security-role.js +38 -102
  66. package/src/roles/solomon-role.js +71 -15
  67. package/src/roles/tester-role.js +121 -95
  68. package/src/roles/triage-role.js +50 -111
  69. package/src/session-store.js +1 -1
  70. package/src/skills/skill-detector.js +11 -0
  71. package/src/sonar/config-resolver.js +20 -1
  72. package/src/sonar/credentials.js +17 -0
  73. package/src/sonar/project-key.js +1 -1
  74. package/src/sonar/scanner.js +1 -1
  75. package/src/utils/banner.js +66 -0
  76. package/src/utils/display.js +35 -17
  77. package/src/utils/injection-guard.js +23 -8
  78. package/src/utils/logger.js +6 -1
  79. package/src/utils/process.js +3 -4
  80. package/src/utils/role-assigner.js +131 -0
  81. package/src/utils/update-check.js +5 -1
  82. package/src/utils/welcome.js +3 -8
  83. package/src/webperf/devtools-detect.js +70 -23
  84. package/templates/roles/coder.md +7 -0
  85. package/templates/roles/karajan-brain.md +95 -0
  86. package/templates/roles/solomon.md +67 -106
  87. package/templates/workflows/automerge.yml +4 -4
  88. package/templates/workflows/houston-override.yml +4 -4
  89. package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
  90. package/src/proxy/adapters/anthropic.js +0 -131
  91. package/src/proxy/adapters/gemini.js +0 -106
  92. package/src/proxy/adapters/index.js +0 -51
  93. package/src/proxy/adapters/openai.js +0 -71
  94. package/src/proxy/compression/ai-compressor.js +0 -209
  95. package/src/proxy/compression/dedup-cache.js +0 -100
  96. package/src/proxy/compression/deterministic/bash-build.js +0 -60
  97. package/src/proxy/compression/deterministic/bash-git.js +0 -101
  98. package/src/proxy/compression/deterministic/bash-infra.js +0 -64
  99. package/src/proxy/compression/deterministic/bash-misc.js +0 -53
  100. package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
  101. package/src/proxy/compression/deterministic/bash-test.js +0 -61
  102. package/src/proxy/compression/deterministic/glob.js +0 -26
  103. package/src/proxy/compression/deterministic/grep.js +0 -53
  104. package/src/proxy/compression/deterministic/index.js +0 -58
  105. package/src/proxy/compression/deterministic/read.js +0 -22
  106. package/src/proxy/compression/deterministic/utils.js +0 -33
  107. package/src/proxy/compression/pipeline.js +0 -270
  108. package/src/proxy/middleware/prompt-injector.js +0 -165
  109. package/src/proxy/middleware/response-interceptor.js +0 -223
  110. package/src/proxy/progress-monitor.js +0 -98
  111. package/src/proxy/proxy-lifecycle.js +0 -198
  112. package/src/proxy/proxy-server.js +0 -239
  113. package/src/proxy/security.js +0 -45
  114. /package/src/{becaria → ci}/index.js +0 -0
  115. /package/src/{becaria → ci}/repo.js +0 -0
@@ -65,11 +65,11 @@ export async function fetchCard({ projectId, cardId, timeoutMs = DEFAULT_TIMEOUT
65
65
  return data?.card || data;
66
66
  }
67
67
 
68
- export async function getCard({ projectId, cardId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
68
+ async function getCard({ projectId, cardId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
69
69
  return fetchCard({ projectId, cardId, timeoutMs });
70
70
  }
71
71
 
72
- export async function listCards({ projectId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
72
+ async function listCards({ projectId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
73
73
  const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards`;
74
74
  const response = await fetchWithRetry(url, {}, timeoutMs);
75
75
  const data = await parseJsonResponse(response);
@@ -188,7 +188,7 @@ export function accumulateCommit(session, commitInfo) {
188
188
  hash: commitInfo.hash,
189
189
  message: commitInfo.message || "",
190
190
  date: commitInfo.date || new Date().toISOString(),
191
- author: commitInfo.author || "BecarIA"
191
+ author: commitInfo.author || "Karajan"
192
192
  });
193
193
  }
194
194
 
@@ -33,7 +33,7 @@ const SERENA_INSTRUCTIONS = [
33
33
  "Fall back to reading files only when Serena tools are not sufficient."
34
34
  ].join("\n");
35
35
 
36
- export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, proxyEnabled = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en" }) {
36
+ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en" }) {
37
37
  const langInstruction = getLanguageInstruction(language);
38
38
  const sections = [
39
39
  serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
@@ -41,6 +41,10 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
41
41
  `Task:\n${task}`,
42
42
  "Implement directly in the repository.",
43
43
  "Keep changes minimal and production-ready.",
44
+ "Follow SOLID principles. Write small, focused functions (< 30 lines).",
45
+ "Make atomic commits: 1 logical change = 1 commit. Keep PRs small and reviewable.",
46
+ "Security: use httpOnly cookies for auth tokens, validate all input, parameterize queries, never expose secrets.",
47
+ "No console.log — use a structured logger. No 'any' types — use JSDoc annotations.",
44
48
  SUBPROCESS_CONSTRAINTS
45
49
  ];
46
50
 
@@ -48,7 +52,7 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
48
52
  sections.push(SERENA_INSTRUCTIONS);
49
53
  }
50
54
 
51
- const rtkSnippet = buildRtkInstructions({ rtkAvailable, proxyEnabled });
55
+ const rtkSnippet = buildRtkInstructions({ rtkAvailable });
52
56
  if (rtkSnippet) {
53
57
  sections.push(rtkSnippet);
54
58
  }
@@ -85,7 +89,17 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
85
89
  }
86
90
 
87
91
  if (reviewerFeedback) {
88
- sections.push(`Reviewer blocking feedback:\n${reviewerFeedback}`);
92
+ sections.push(
93
+ `## Reviewer blocking feedback — YOU MUST FIX THESE BEFORE PROCEEDING`,
94
+ reviewerFeedback,
95
+ "",
96
+ "For each issue above:",
97
+ "1. Find the relevant file(s) in the project",
98
+ "2. Make the specific code change needed to resolve the issue",
99
+ "3. If tests are missing, write them",
100
+ "4. If dependencies are needed, install them (npm install)",
101
+ "5. Do NOT skip any issue — all must be resolved"
102
+ );
89
103
  }
90
104
 
91
105
  if (deferredContext) {
@@ -24,7 +24,7 @@ const SERENA_INSTRUCTIONS = [
24
24
  "Fall back to reading files only when Serena tools are not sufficient."
25
25
  ].join("\n");
26
26
 
27
- export async function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, proxyEnabled = false, productContext = null, domainContext = null, projectDir = null, language = "en" }) {
27
+ export async function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, domainContext = null, projectDir = null, language = "en" }) {
28
28
  const truncatedDiff = diff.length > 12000 ? `${diff.slice(0, 12000)}\n\n[TRUNCATED]` : diff;
29
29
 
30
30
  const langInstruction = getLanguageInstruction(language);
@@ -43,7 +43,7 @@ export async function buildReviewerPrompt({ task, diff, reviewRules, mode, seren
43
43
  sections.push(SERENA_INSTRUCTIONS);
44
44
  }
45
45
 
46
- const rtkSnippet = buildRtkInstructions({ rtkAvailable, proxyEnabled });
46
+ const rtkSnippet = buildRtkInstructions({ rtkAvailable });
47
47
  if (rtkSnippet) {
48
48
  sections.push(rtkSnippet);
49
49
  }
@@ -15,13 +15,11 @@ export const RTK_INSTRUCTIONS = [
15
15
  ].join("\n");
16
16
 
17
17
  /**
18
- * Build RTK instructions, returning empty string when proxy compression
19
- * handles token optimization at the HTTP layer.
20
- * @param {{ rtkAvailable?: boolean, proxyEnabled?: boolean }} options
18
+ * Build RTK instructions when RTK is detected.
19
+ * @param {{ rtkAvailable?: boolean }} options
21
20
  * @returns {string}
22
21
  */
23
- export function buildRtkInstructions({ rtkAvailable = false, proxyEnabled = false } = {}) {
24
- if (proxyEnabled) return "";
22
+ export function buildRtkInstructions({ rtkAvailable = false } = {}) {
25
23
  if (!rtkAvailable) return "";
26
24
  return RTK_INSTRUCTIONS;
27
25
  }
@@ -1,8 +1,33 @@
1
1
  import { runCommand } from "../utils/process.js";
2
+ import { generateSnapshotDiff } from "./snapshot-diff.js";
2
3
 
3
4
  /** @type {((command: string, args?: string[], options?: object) => Promise<object>)|null} */
4
5
  let _runner = null;
5
6
 
7
+ /** @type {Map<string, string>|null} — pre-coder filesystem snapshot for git-free diff */
8
+ let _snapshot = null;
9
+
10
+ /** @type {string|null} — project directory scope for git diff (prevents leaking unrelated changes) */
11
+ let _projectDir = null;
12
+
13
+ /**
14
+ * Set the project directory scope. When set, all diffs are scoped to this directory.
15
+ * Call once at pipeline start when projectDir differs from repo root.
16
+ * @param {string|null} dir
17
+ */
18
+ export function setProjectDir(dir) {
19
+ _projectDir = dir;
20
+ }
21
+
22
+ /**
23
+ * Store a filesystem snapshot for git-free diff fallback.
24
+ * Call before the coder runs. If git is available, this is unused.
25
+ * @param {Map<string, string>} snapshot
26
+ */
27
+ export function setSnapshot(snapshot) {
28
+ _snapshot = snapshot;
29
+ }
30
+
6
31
  /**
7
32
  * Inject an RTK-aware runner so all git operations in this module benefit from token savings.
8
33
  * Call once at pipeline start when RTK is available.
@@ -18,29 +43,53 @@ function run(command, args, ...rest) {
18
43
 
19
44
  export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
20
45
  if (baseRef) return baseRef;
21
- const mergeBase = await run("git", ["merge-base", "HEAD", `origin/${baseBranch}`]);
22
- if (mergeBase.exitCode === 0) return mergeBase.stdout.trim();
23
46
 
24
- const fallback = await run("git", ["rev-parse", "HEAD~1"]);
25
- if (fallback.exitCode === 0) return fallback.stdout.trim();
47
+ try {
48
+ const mergeBase = await run("git", ["merge-base", "HEAD", `origin/${baseBranch}`]);
49
+ if (mergeBase.exitCode === 0) return mergeBase.stdout.trim();
26
50
 
27
- // Empty repo (zero commits): diff against the empty tree
28
- const emptyTree = await run("git", ["hash-object", "-t", "tree", "/dev/null"]);
29
- if (emptyTree.exitCode === 0) return emptyTree.stdout.trim();
51
+ const fallback = await run("git", ["rev-parse", "HEAD~1"]);
52
+ if (fallback.exitCode === 0) return fallback.stdout.trim();
30
53
 
31
- throw new Error("Could not compute diff base reference");
54
+ // Empty repo (zero commits): diff against the empty tree
55
+ const emptyTree = await run("git", ["hash-object", "-t", "tree", "/dev/null"]);
56
+ if (emptyTree.exitCode === 0) return emptyTree.stdout.trim();
57
+ } catch { /* git not available */ }
58
+
59
+ // No git or all strategies failed — return sentinel for snapshot-based diff
60
+ return "__snapshot__";
32
61
  }
33
62
 
34
- export async function generateDiff({ baseRef, stageNewFiles = false }) {
35
- // Stage untracked files so they appear in the diff (coder creates files but doesn't git add)
36
- if (stageNewFiles) {
37
- await run("git", ["add", "-A"]);
38
- }
39
- const result = await run("git", ["diff", stageNewFiles ? "--cached" : "", `${baseRef}`].filter(Boolean));
40
- if (result.exitCode !== 0) {
41
- throw new Error(`git diff failed: ${result.stderr || result.stdout}`);
63
+ export async function generateDiff({ baseRef, stageNewFiles = false, projectDir = null }) {
64
+ // Auto-resolve projectDir from config if not passed explicitly
65
+ const scopeDir = projectDir || _projectDir || null;
66
+
67
+ // Try git diff first
68
+ try {
69
+ if (stageNewFiles) {
70
+ const addArgs = scopeDir ? ["-A", scopeDir] : ["-A"];
71
+ await run("git", ["add", ...addArgs]);
72
+ }
73
+ const diffArgs = ["diff", stageNewFiles ? "--cached" : "", `${baseRef}`].filter(Boolean);
74
+ // Scope diff to projectDir when it's a subdirectory (prevents leaking unrelated changes)
75
+ if (scopeDir) diffArgs.push("--", scopeDir);
76
+ const result = await run("git", diffArgs);
77
+ if (result.exitCode === 0) {
78
+ return result.stdout;
79
+ }
80
+ } catch { /* git not available or failed */ }
81
+
82
+ // Fallback: snapshot-based diff (no git needed)
83
+ if (_snapshot) {
84
+ const dir = projectDir || process.cwd();
85
+ return generateSnapshotDiff(_snapshot, null, dir);
42
86
  }
43
- return result.stdout;
87
+
88
+ // No git and no snapshot — generate diff of all files as new
89
+ const dir = projectDir || process.cwd();
90
+ const { takeSnapshot } = await import("./snapshot-diff.js");
91
+ const emptySnapshot = new Map();
92
+ return generateSnapshotDiff(emptySnapshot, null, dir);
44
93
  }
45
94
 
46
95
  export async function getUntrackedFiles() {
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Git-free diff generation using filesystem snapshots.
3
+ *
4
+ * Takes a snapshot of project files (path → content hash), then compares
5
+ * a later snapshot to generate a unified diff. Works on any OS, no git needed.
6
+ */
7
+
8
+ import { readdir, readFile, stat } from "node:fs/promises";
9
+ import { join, relative } from "node:path";
10
+ import { createHash } from "node:crypto";
11
+
12
+ const DEFAULT_IGNORE = new Set([
13
+ "node_modules", ".git", ".kj", ".karajan", "dist", "build",
14
+ ".next", ".nuxt", ".svelte-kit", "__pycache__", ".venv",
15
+ "coverage", ".nyc_output", ".reviews"
16
+ ]);
17
+
18
+ const MAX_FILE_SIZE = 512 * 1024; // 512KB — skip large files
19
+
20
+ /**
21
+ * Scan a directory recursively and return a Map of relative path → content hash.
22
+ * @param {string} rootDir
23
+ * @returns {Promise<Map<string, string>>} path → sha256 hash
24
+ */
25
+ export async function takeSnapshot(rootDir) {
26
+ const snapshot = new Map();
27
+ await scanDir(rootDir, rootDir, snapshot);
28
+ return snapshot;
29
+ }
30
+
31
+ async function scanDir(dir, rootDir, snapshot) {
32
+ let entries;
33
+ try {
34
+ entries = await readdir(dir, { withFileTypes: true });
35
+ } catch { return; }
36
+
37
+ for (const entry of entries) {
38
+ if (DEFAULT_IGNORE.has(entry.name) || entry.name.startsWith(".")) continue;
39
+
40
+ const fullPath = join(dir, entry.name);
41
+ if (entry.isDirectory()) {
42
+ await scanDir(fullPath, rootDir, snapshot);
43
+ } else if (entry.isFile()) {
44
+ try {
45
+ const s = await stat(fullPath);
46
+ if (s.size > MAX_FILE_SIZE) continue;
47
+ const content = await readFile(fullPath);
48
+ const hash = createHash("sha256").update(content).digest("hex");
49
+ snapshot.set(relative(rootDir, fullPath), hash);
50
+ } catch { /* skip unreadable files */ }
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Compare two snapshots and generate a unified diff string.
57
+ * @param {Map<string, string>} before — snapshot before coder
58
+ * @param {Map<string, string>|null} after — snapshot after coder (null = read from disk)
59
+ * @param {string} rootDir — project root (for reading file contents)
60
+ * @returns {Promise<string>} unified diff text
61
+ */
62
+ export async function generateSnapshotDiff(before, after, rootDir) {
63
+ if (!after) {
64
+ after = await takeSnapshot(rootDir);
65
+ }
66
+
67
+ const lines = [];
68
+
69
+ // New files (in after but not before)
70
+ for (const [path, hash] of after) {
71
+ if (!before.has(path)) {
72
+ const content = await safeRead(join(rootDir, path));
73
+ if (content !== null) {
74
+ lines.push(`diff --snapshot a/${path} b/${path}`);
75
+ lines.push("new file");
76
+ lines.push(`--- /dev/null`);
77
+ lines.push(`+++ b/${path}`);
78
+ for (const line of content.split("\n")) {
79
+ lines.push(`+${line}`);
80
+ }
81
+ lines.push("");
82
+ }
83
+ }
84
+ }
85
+
86
+ // Modified files (in both, different hash)
87
+ for (const [path, hash] of after) {
88
+ if (before.has(path) && before.get(path) !== hash) {
89
+ const content = await safeRead(join(rootDir, path));
90
+ if (content !== null) {
91
+ lines.push(`diff --snapshot a/${path} b/${path}`);
92
+ lines.push("modified file");
93
+ lines.push(`--- a/${path}`);
94
+ lines.push(`+++ b/${path}`);
95
+ // Show full new content (we don't have the old content)
96
+ for (const line of content.split("\n")) {
97
+ lines.push(`+${line}`);
98
+ }
99
+ lines.push("");
100
+ }
101
+ }
102
+ }
103
+
104
+ // Deleted files (in before but not after)
105
+ for (const [path] of before) {
106
+ if (!after.has(path)) {
107
+ lines.push(`diff --snapshot a/${path} b/${path}`);
108
+ lines.push("deleted file");
109
+ lines.push(`--- a/${path}`);
110
+ lines.push(`+++ /dev/null`);
111
+ lines.push("");
112
+ }
113
+ }
114
+
115
+ return lines.join("\n");
116
+ }
117
+
118
+ async function safeRead(filePath) {
119
+ try {
120
+ return await readFile(filePath, "utf-8");
121
+ } catch { return null; }
122
+ }
@@ -2,7 +2,8 @@ function extractChangedFiles(diff) {
2
2
  const files = new Set();
3
3
  const lines = String(diff || "").split("\n");
4
4
  for (const line of lines) {
5
- if (!line.startsWith("diff --git ")) continue;
5
+ // Support both git diff and snapshot diff formats
6
+ if (!line.startsWith("diff --git ") && !line.startsWith("diff --snapshot ")) continue;
6
7
  const parts = line.split(" ");
7
8
  const b = parts[3] || "";
8
9
  if (b.startsWith("b/")) files.add(b.slice(2));
@@ -49,7 +50,8 @@ export function evaluateTddPolicy(diff, developmentConfig = {}, untrackedFiles =
49
50
  }
50
51
 
51
52
  if (sourceFiles.length === 0) {
52
- return { ok: true, reason: "no_source_changes", sourceFiles, testFiles };
53
+ const reason = files.length === 0 ? "no_diff_available" : "no_source_changes";
54
+ return { ok: true, reason, sourceFiles, testFiles };
53
55
  }
54
56
 
55
57
  if (testFiles.length === 0) {
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Base class for LLM-backed roles.
3
+ * Handles the common execute() flow: resolve provider → create agent → call LLM → parse output.
4
+ * Subclasses override hooks (buildPrompt, parseOutput, buildSuccessResult, buildSummary)
5
+ * instead of reimplementing the full execute() boilerplate.
6
+ */
7
+ import { BaseRole } from "./base-role.js";
8
+ import { createAgent as defaultCreateAgent } from "../agents/index.js";
9
+
10
+ export class AgentRole extends BaseRole {
11
+ constructor({ name, config, logger, emitter = null, createAgentFn = null }) {
12
+ super({ name, config, logger, emitter });
13
+ this._createAgent = createAgentFn || defaultCreateAgent;
14
+ }
15
+
16
+ /** Provider fallback chain. Override for roles with non-standard chains. */
17
+ resolveProvider() {
18
+ return (
19
+ this.config?.roles?.[this.name]?.provider ||
20
+ this.config?.roles?.coder?.provider ||
21
+ "claude"
22
+ );
23
+ }
24
+
25
+ /** Agent method to call. Override with "reviewTask" for reviewer. */
26
+ get agentMethod() { return "runTask"; }
27
+
28
+ /** Create the agent. Override for special logic (e.g. HostAgent in coder). */
29
+ createAgentInstance(provider) {
30
+ return this._createAgent(provider, this.config, this.logger);
31
+ }
32
+
33
+ /**
34
+ * Build the prompt for this role. MUST be overridden.
35
+ * @param {*} input - raw input from pipeline
36
+ * @returns {Promise<{prompt: string, onOutput?: Function}>|{prompt: string, onOutput?: Function}|string}
37
+ */
38
+ async buildPrompt(_input) {
39
+ throw new Error(`${this.name}: buildPrompt() not implemented`);
40
+ }
41
+
42
+ /**
43
+ * Parse raw LLM output into structured data. Return null to trigger handleParseNull.
44
+ * @param {string} raw - raw agent output
45
+ * @returns {*} parsed result
46
+ */
47
+ parseOutput(raw) { return raw; }
48
+
49
+ /**
50
+ * Build the result object for a successful parse.
51
+ * @param {*} parsed - output of parseOutput
52
+ * @param {string} provider
53
+ * @param {object} agentResult - full agent result (for accessing .usage, .output, etc.)
54
+ * @returns {object} result to include in { ok, result, summary, usage }
55
+ */
56
+ buildSuccessResult(parsed, provider, _agentResult) { return { ...parsed, provider }; }
57
+
58
+ /** Build human-readable summary from parsed output. */
59
+ buildSummary(_parsed) { return `${this.name} completed`; }
60
+
61
+ /** Determine ok status from parsed result. Default: true. Override for verdict-based roles. */
62
+ isSuccessful(_parsed) { return true; }
63
+
64
+ /** Handle null parse (no JSON found). Default returns error. Override for lenient roles. */
65
+ handleParseNull(agentResult, provider) {
66
+ return {
67
+ ok: false,
68
+ result: { error: `Failed to parse ${this.name} output: no JSON found`, provider },
69
+ summary: `${this.name} output parse error: no JSON found`,
70
+ usage: agentResult.usage
71
+ };
72
+ }
73
+
74
+ /** Handle parse exception. Default returns error. Override for lenient roles. */
75
+ handleParseError(err, agentResult, provider) {
76
+ return {
77
+ ok: false,
78
+ result: { error: `Failed to parse ${this.name} output: ${err.message}`, provider },
79
+ summary: `${this.name} output parse error: ${err.message}`,
80
+ usage: agentResult.usage
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Extract input fields from raw input. Default handles string-or-object.
86
+ * Override for roles needing extra fields.
87
+ */
88
+ extractInput(input) {
89
+ if (typeof input === "string") return { task: input, onOutput: null };
90
+ return {
91
+ task: input?.task || this.context?.task || "",
92
+ onOutput: input?.onOutput || null,
93
+ ...input
94
+ };
95
+ }
96
+
97
+ async execute(input) {
98
+ const extracted = this.extractInput(input);
99
+ const provider = this.resolveProvider();
100
+ const agent = this.createAgentInstance(provider);
101
+
102
+ const promptResult = await this.buildPrompt(extracted);
103
+ const { prompt, onOutput } = typeof promptResult === "string"
104
+ ? { prompt: promptResult, onOutput: extracted.onOutput }
105
+ : { onOutput: extracted.onOutput, ...promptResult };
106
+
107
+ const runArgs = { prompt, role: this.name };
108
+ if (onOutput) runArgs.onOutput = onOutput;
109
+
110
+ const result = await agent[this.agentMethod](runArgs);
111
+
112
+ if (!result.ok) {
113
+ return {
114
+ ok: false,
115
+ result: { error: result.error || result.output || `${this.name} failed`, provider },
116
+ summary: `${this.name} failed: ${result.error || "unknown error"}`,
117
+ usage: result.usage
118
+ };
119
+ }
120
+
121
+ try {
122
+ const parsed = this.parseOutput(result.output);
123
+ if (parsed === null) return this.handleParseNull(result, provider);
124
+
125
+ return {
126
+ ok: this.isSuccessful(parsed),
127
+ result: this.buildSuccessResult(parsed, provider, result),
128
+ summary: this.buildSummary(parsed),
129
+ usage: result.usage
130
+ };
131
+ } catch (err) {
132
+ return this.handleParseError(err, result, provider);
133
+ }
134
+ }
135
+ }