karajan-code 1.59.1 → 2.0.1

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 +1 -4
  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 +2 -2
  12. package/src/commands/agents.js +0 -1
  13. package/src/commands/board.js +8 -1
  14. package/src/commands/doctor.js +22 -40
  15. package/src/commands/init.js +8 -8
  16. package/src/commands/report.js +0 -13
  17. package/src/commands/review.js +17 -17
  18. package/src/commands/run.js +1 -2
  19. package/src/commands/status.js +1 -7
  20. package/src/config.js +17 -52
  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 +1 -1
  26. package/src/mcp/tools.js +1 -1
  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/pipeline-context.js +3 -0
  35. package/src/orchestrator/post-loop-stages.js +20 -15
  36. package/src/orchestrator/preflight-checks.js +7 -3
  37. package/src/orchestrator/role-output-compressor.js +190 -0
  38. package/src/orchestrator/session-journal.js +344 -0
  39. package/src/orchestrator/solomon-escalation.js +38 -5
  40. package/src/orchestrator/solomon-rules.js +32 -33
  41. package/src/orchestrator/stages/coder-stage.js +49 -4
  42. package/src/orchestrator/stages/reviewer-stage.js +52 -9
  43. package/src/orchestrator/stages/sonar-stage.js +24 -1
  44. package/src/orchestrator/verification-gate.js +146 -0
  45. package/src/orchestrator.js +481 -672
  46. package/src/planning-game/client.js +2 -2
  47. package/src/planning-game/pipeline-adapter.js +1 -1
  48. package/src/prompts/coder.js +17 -3
  49. package/src/prompts/reviewer.js +2 -2
  50. package/src/prompts/rtk-snippet.js +3 -5
  51. package/src/review/diff-generator.js +66 -17
  52. package/src/review/snapshot-diff.js +122 -0
  53. package/src/review/tdd-policy.js +4 -2
  54. package/src/roles/agent-role.js +135 -0
  55. package/src/roles/architect-role.js +61 -108
  56. package/src/roles/audit-role.js +25 -71
  57. package/src/roles/coder-role.js +32 -57
  58. package/src/roles/discover-role.js +30 -71
  59. package/src/roles/hu-reviewer-role.js +51 -99
  60. package/src/roles/impeccable-role.js +33 -88
  61. package/src/roles/index.js +2 -0
  62. package/src/roles/karajan-brain-role.js +134 -0
  63. package/src/roles/planner-role.js +40 -75
  64. package/src/roles/refactorer-role.js +22 -64
  65. package/src/roles/researcher-role.js +51 -115
  66. package/src/roles/reviewer-role.js +68 -125
  67. package/src/roles/security-role.js +38 -102
  68. package/src/roles/solomon-role.js +69 -11
  69. package/src/roles/tester-role.js +121 -95
  70. package/src/roles/triage-role.js +50 -111
  71. package/src/session-store.js +1 -1
  72. package/src/skills/skill-detector.js +11 -0
  73. package/src/sonar/config-resolver.js +20 -1
  74. package/src/sonar/credentials.js +17 -0
  75. package/src/sonar/project-key.js +1 -1
  76. package/src/sonar/scanner.js +1 -1
  77. package/src/utils/display.js +33 -14
  78. package/src/utils/injection-guard.js +23 -8
  79. package/src/utils/logger.js +6 -1
  80. package/src/utils/process.js +3 -4
  81. package/src/utils/role-assigner.js +131 -0
  82. package/src/utils/update-check.js +5 -1
  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 -201
  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
@@ -1,72 +1,30 @@
1
- import { BaseRole } from "./base-role.js";
2
- import { createAgent as defaultCreateAgent } from "../agents/index.js";
1
+ import { AgentRole } from "./agent-role.js";
3
2
 
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, "");
3
+ export class RefactorerRole extends AgentRole {
4
+ constructor(opts) {
5
+ super({ ...opts, name: "refactorer" });
17
6
  }
18
7
 
19
- sections.push(
20
- "Refactor the current changes for clarity and maintainability without changing behavior.",
21
- "Do not expand scope and keep tests green.",
22
- "",
23
- "## Task context",
24
- task
25
- );
26
-
27
- return sections.join("\n");
28
- }
29
-
30
- export class RefactorerRole extends BaseRole {
31
- constructor({ config, logger, emitter = null, createAgentFn = null }) {
32
- super({ name: "refactorer", config, logger, emitter });
33
- this._createAgent = createAgentFn || defaultCreateAgent;
8
+ resolveProvider() {
9
+ return this.config?.roles?.refactorer?.provider || this.config?.roles?.coder?.provider || "claude";
34
10
  }
35
11
 
36
- async execute(input) {
37
- const task = typeof input === "string"
38
- ? input
39
- : input?.task || this.context?.task || "";
40
- const onOutput = typeof input === "string" ? null : input?.onOutput || null;
41
-
42
- const provider = resolveProvider(this.config);
43
- const agent = this._createAgent(provider, this.config, this.logger);
44
-
45
- const prompt = buildPrompt({ task, instructions: this.instructions });
46
- const runArgs = { prompt, role: "refactorer" };
47
- if (onOutput) runArgs.onOutput = onOutput;
48
- const result = await agent.runTask(runArgs);
49
-
50
- if (!result.ok) {
51
- return {
52
- ok: false,
53
- result: {
54
- ...result,
55
- error: result.error || result.output || "Refactorer failed",
56
- provider
57
- },
58
- summary: `Refactorer failed: ${result.error || "unknown error"}`
59
- };
60
- }
12
+ async buildPrompt({ task }) {
13
+ const sections = [];
14
+ if (this.instructions) sections.push(this.instructions, "");
15
+ sections.push(
16
+ "Refactor the current changes for clarity and maintainability without changing behavior.",
17
+ "Do not expand scope and keep tests green.",
18
+ "",
19
+ "## Task context",
20
+ task
21
+ );
22
+ return { prompt: sections.join("\n") };
23
+ }
61
24
 
62
- return {
63
- ok: true,
64
- result: {
65
- ...result,
66
- output: result.output?.trim() || "",
67
- provider
68
- },
69
- summary: "Refactoring applied"
70
- };
25
+ buildSuccessResult(parsed, provider, agentResult) {
26
+ return { ...agentResult, output: (agentResult.output || "").trim(), provider };
71
27
  }
28
+
29
+ buildSummary() { return "Refactoring applied"; }
72
30
  }
@@ -1,5 +1,4 @@
1
- import { BaseRole } from "./base-role.js";
2
- import { createAgent as defaultCreateAgent } from "../agents/index.js";
1
+ import { AgentRole } from "./agent-role.js";
3
2
  import { extractFirstJson } from "../utils/json-extract.js";
4
3
 
5
4
  const SUBAGENT_PREAMBLE = [
@@ -8,127 +7,64 @@ const SUBAGENT_PREAMBLE = [
8
7
  "Do NOT use any MCP tools. Focus only on researching the codebase."
9
8
  ].join(" ");
10
9
 
11
- function resolveProvider(config) {
12
- return (
13
- config?.roles?.researcher?.provider ||
14
- config?.roles?.coder?.provider ||
15
- "claude"
16
- );
17
- }
18
-
19
- function buildPrompt({ task, instructions }) {
20
- const sections = [SUBAGENT_PREAMBLE];
10
+ const EMPTY_RESEARCH = {
11
+ affected_files: [], patterns: [], constraints: [],
12
+ prior_decisions: [], risks: [], test_coverage: ""
13
+ };
21
14
 
22
- if (instructions) {
23
- sections.push(instructions);
15
+ export class ResearcherRole extends AgentRole {
16
+ constructor(opts) {
17
+ super({ ...opts, name: "researcher" });
24
18
  }
25
19
 
26
- sections.push(
27
- "Investigate the codebase for the following task.",
28
- "Identify affected files, patterns, constraints, prior decisions, risks, and test coverage.",
29
- "Return a single valid JSON object with your findings and nothing else.",
30
- 'JSON schema: {"affected_files":[string],"patterns":[string],"constraints":[string],"prior_decisions":[string],"risks":[string],"test_coverage":string}',
31
- `## Task\n${task}`
32
- );
33
-
34
- return sections.join("\n\n");
35
- }
36
-
37
- function parseResearchOutput(raw) {
38
- return extractFirstJson(raw);
39
- }
40
-
41
- function buildSummary(parsed) {
42
- const files = parsed.affected_files?.length || 0;
43
- const risks = parsed.risks?.length || 0;
44
- const patterns = parsed.patterns?.length || 0;
45
- const parts = [];
46
- if (files) parts.push(`${files} file${files === 1 ? "" : "s"}`);
47
- if (risks) parts.push(`${risks} risk${risks === 1 ? "" : "s"}`);
48
- if (patterns) parts.push(`${patterns} pattern${patterns === 1 ? "" : "s"}`);
49
- return parts.length
50
- ? `Research complete: ${parts.join(", ")} identified`
51
- : "Research complete";
52
- }
53
-
54
- export class ResearcherRole extends BaseRole {
55
- constructor({ config, logger, emitter = null, createAgentFn = null }) {
56
- super({ name: "researcher", config, logger, emitter });
57
- this._createAgent = createAgentFn || defaultCreateAgent;
20
+ async buildPrompt({ task }) {
21
+ const sections = [SUBAGENT_PREAMBLE];
22
+ if (this.instructions) sections.push(this.instructions);
23
+ sections.push(
24
+ "Investigate the codebase for the following task.",
25
+ "Identify affected files, patterns, constraints, prior decisions, risks, and test coverage.",
26
+ "Return a single valid JSON object with your findings and nothing else.",
27
+ '{"affected_files":[string],"patterns":[string],"constraints":[string],"prior_decisions":[string],"risks":[string],"test_coverage":string}',
28
+ `## Task\n${task}`
29
+ );
30
+ return { prompt: sections.join("\n\n") };
58
31
  }
59
32
 
60
- async execute(input) {
61
- const task = typeof input === "string"
62
- ? input
63
- : input?.task || this.context?.task || "";
64
- const onOutput = typeof input === "string" ? null : input?.onOutput || null;
65
-
66
- const provider = resolveProvider(this.config);
67
- const agent = this._createAgent(provider, this.config, this.logger);
33
+ parseOutput(raw) { return extractFirstJson(raw); }
68
34
 
69
- const prompt = buildPrompt({ task, instructions: this.instructions });
70
- const runArgs = { prompt, role: "researcher" };
71
- if (onOutput) runArgs.onOutput = onOutput;
72
- const result = await agent.runTask(runArgs);
35
+ buildSuccessResult(parsed, provider) {
36
+ return {
37
+ affected_files: parsed.affected_files || [],
38
+ patterns: parsed.patterns || [],
39
+ constraints: parsed.constraints || [],
40
+ prior_decisions: parsed.prior_decisions || [],
41
+ risks: parsed.risks || [],
42
+ test_coverage: parsed.test_coverage || "",
43
+ provider
44
+ };
45
+ }
73
46
 
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
- }
47
+ buildSummary(parsed) {
48
+ const files = parsed.affected_files?.length || 0;
49
+ const risks = parsed.risks?.length || 0;
50
+ const patterns = parsed.patterns?.length || 0;
51
+ const parts = [];
52
+ if (files) parts.push(`${files} file${files === 1 ? "" : "s"}`);
53
+ if (risks) parts.push(`${risks} risk${risks === 1 ? "" : "s"}`);
54
+ if (patterns) parts.push(`${patterns} pattern${patterns === 1 ? "" : "s"}`);
55
+ return parts.length ? `Research complete: ${parts.join(", ")} identified` : "Research complete";
56
+ }
84
57
 
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
- }
58
+ handleParseNull(agentResult, provider) {
59
+ return {
60
+ ok: true,
61
+ result: { ...EMPTY_RESEARCH, raw: agentResult.output, provider },
62
+ summary: "Research complete (unstructured output)",
63
+ usage: agentResult.usage
64
+ };
65
+ }
103
66
 
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 { /* agent output is not structured JSON */
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
- }
67
+ handleParseError(_err, agentResult, provider) {
68
+ return this.handleParseNull(agentResult, provider);
133
69
  }
134
70
  }
@@ -1,5 +1,4 @@
1
- import { BaseRole } from "./base-role.js";
2
- import { createAgent as defaultCreateAgent } from "../agents/index.js";
1
+ import { AgentRole } from "./agent-role.js";
3
2
  import { buildRtkInstructions } from "../prompts/rtk-snippet.js";
4
3
  import { extractFirstJson } from "../utils/json-extract.js";
5
4
 
@@ -11,146 +10,90 @@ const SUBAGENT_PREAMBLE = [
11
10
  "Do NOT use any MCP tools. Focus only on reviewing the code."
12
11
  ].join(" ");
13
12
 
14
- function resolveProvider(config) {
15
- return (
16
- config?.roles?.reviewer?.provider ||
17
- config?.roles?.coder?.provider ||
18
- "claude"
19
- );
20
- }
21
-
22
13
  function truncateDiff(diff) {
23
14
  if (!diff) return "";
24
- return diff.length > MAX_DIFF_LENGTH
25
- ? `${diff.slice(0, MAX_DIFF_LENGTH)}\n\n[TRUNCATED]`
26
- : diff;
15
+ return diff.length > MAX_DIFF_LENGTH ? `${diff.slice(0, MAX_DIFF_LENGTH)}\n\n[TRUNCATED]` : diff;
27
16
  }
28
17
 
29
- function buildPrompt({ task, diff, reviewRules, reviewMode, instructions, rtkAvailable = false, proxyEnabled = false, productContext = null, domainContext = null }) {
30
- const sections = [];
31
-
32
- sections.push(SUBAGENT_PREAMBLE);
33
-
34
- if (instructions) {
35
- sections.push(instructions);
18
+ export class ReviewerRole extends AgentRole {
19
+ constructor(opts) {
20
+ super({ ...opts, name: "reviewer" });
36
21
  }
37
22
 
38
- sections.push(
39
- `You are a code reviewer in ${reviewMode || "standard"} mode.`,
40
- "Return only one valid JSON object and nothing else.",
41
- 'JSON schema:',
42
- '{"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}',
43
- `Task context:\n${task}`
44
- );
23
+ get agentMethod() { return "reviewTask"; }
45
24
 
46
- if (productContext) {
47
- sections.push(`## Product Context\n${productContext}`);
25
+ extractInput(input) {
26
+ if (typeof input === "string") return { task: input, diff: "", reviewRules: null, onOutput: null };
27
+ return {
28
+ task: input?.task || this.context?.task || "",
29
+ diff: input?.diff || "",
30
+ reviewRules: input?.reviewRules || null,
31
+ onOutput: input?.onOutput || null
32
+ };
48
33
  }
49
34
 
50
- if (domainContext) {
51
- sections.push(`## Domain Context\n${domainContext}`);
52
- }
35
+ async buildPrompt({ task, diff, reviewRules }) {
36
+ const sections = [SUBAGENT_PREAMBLE];
37
+ if (this.instructions) sections.push(this.instructions);
38
+ sections.push(
39
+ `You are a code reviewer in ${this.config?.review_mode || "standard"} mode.`,
40
+ "Return only one valid JSON object and nothing else.",
41
+ "JSON schema:",
42
+ '{"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}',
43
+ `Task context:\n${task}`
44
+ );
45
+
46
+ const pc = this.config?.productContext;
47
+ if (pc) sections.push(`## Product Context\n${pc}`);
48
+ const dc = this.config?.domainContext;
49
+ if (dc) sections.push(`## Domain Context\n${dc}`);
50
+
51
+ const rtkSnippet = buildRtkInstructions({
52
+ rtkAvailable: Boolean(this.config?.rtk?.available)
53
+ });
54
+ if (rtkSnippet) sections.push(rtkSnippet);
55
+ if (reviewRules) sections.push(`Review rules:\n${reviewRules}`);
56
+ sections.push(`Git diff:\n${truncateDiff(diff)}`);
53
57
 
54
- const rtkSnippet = buildRtkInstructions({ rtkAvailable, proxyEnabled });
55
- if (rtkSnippet) {
56
- sections.push(rtkSnippet);
58
+ return { prompt: sections.join("\n\n") };
57
59
  }
58
60
 
59
- if (reviewRules) {
60
- sections.push(`Review rules:\n${reviewRules}`);
61
+ parseOutput(raw) {
62
+ const parsed = extractFirstJson(raw);
63
+ if (!parsed) throw new Error("Failed to parse reviewer output: no JSON found");
64
+ return parsed;
61
65
  }
62
66
 
63
- sections.push(`Git diff:\n${truncateDiff(diff)}`);
64
-
65
- return sections.join("\n\n");
66
- }
67
-
68
- function parseReviewOutput(raw) {
69
- const parsed = extractFirstJson(raw);
70
- if (!parsed) {
71
- throw new Error(`Failed to parse reviewer output: no JSON found`);
67
+ buildSuccessResult(parsed, provider, agentResult) {
68
+ return {
69
+ ...agentResult,
70
+ approved: parsed.approved,
71
+ blocking_issues: parsed.blocking_issues || [],
72
+ non_blocking_suggestions: parsed.non_blocking_suggestions || [],
73
+ confidence: parsed.confidence ?? null,
74
+ raw_summary: parsed.summary || ""
75
+ };
72
76
  }
73
- return parsed;
74
- }
75
77
 
76
- export class ReviewerRole extends BaseRole {
77
- constructor({ config, logger, emitter = null, createAgentFn = null }) {
78
- super({ name: "reviewer", config, logger, emitter });
79
- this._createAgent = createAgentFn || defaultCreateAgent;
78
+ buildSummary(parsed) {
79
+ const blockingIssues = parsed.blocking_issues || [];
80
+ return parsed.approved
81
+ ? `Approved: ${parsed.summary || "no issues found"}`
82
+ : `Rejected: ${blockingIssues.length} blocking issue(s) — ${parsed.summary || ""}`;
80
83
  }
81
84
 
82
- async execute(input) {
83
- const { task, diff, reviewRules, onOutput } = typeof input === "string"
84
- ? { task: input, diff: "", reviewRules: null, onOutput: null }
85
- : input;
86
-
87
- const provider = resolveProvider(this.config);
88
- const agent = this._createAgent(provider, this.config, this.logger);
89
-
90
- const prompt = buildPrompt({
91
- task: task || this.context?.task || "",
92
- diff: diff || "",
93
- reviewRules: reviewRules || null,
94
- reviewMode: this.config?.review_mode || "standard",
95
- instructions: this.instructions,
96
- rtkAvailable: Boolean(this.config?.rtk?.available),
97
- proxyEnabled: Boolean(this.config?.proxy?.enabled),
98
- productContext: this.config?.productContext || null,
99
- domainContext: this.config?.domainContext || null
100
- });
101
-
102
- const reviewArgs = { prompt, role: "reviewer" };
103
- if (onOutput) reviewArgs.onOutput = onOutput;
104
-
105
- const result = await agent.reviewTask(reviewArgs);
106
-
107
- if (!result.ok) {
108
- return {
109
- ok: false,
110
- result: {
111
- error: result.error || result.output || "Reviewer agent failed",
112
- approved: false,
113
- blocking_issues: []
114
- },
115
- summary: `Reviewer failed: ${result.error || "unknown error"}`
116
- };
117
- }
118
-
119
- try {
120
- const parsed = parseReviewOutput(result.output);
121
- const blockingIssues = parsed.blocking_issues || [];
122
-
123
- return {
124
- ok: true,
125
- result: {
126
- ...result,
127
- approved: parsed.approved,
128
- blocking_issues: blockingIssues,
129
- non_blocking_suggestions: parsed.non_blocking_suggestions || [],
130
- confidence: parsed.confidence ?? null,
131
- raw_summary: parsed.summary || ""
132
- },
133
- summary: parsed.approved
134
- ? `Approved: ${parsed.summary || "no issues found"}`
135
- : `Rejected: ${blockingIssues.length} blocking issue(s) — ${parsed.summary || ""}`
136
- };
137
- } catch (err) {
138
- return {
139
- ok: true,
140
- result: {
141
- ...result,
142
- approved: false,
143
- blocking_issues: [{
144
- id: "PARSE_ERROR",
145
- severity: "high",
146
- description: `Reviewer output could not be parsed: ${err.message}`
147
- }],
148
- non_blocking_suggestions: [],
149
- confidence: 0,
150
- raw_summary: `Parse error: ${err.message}`
151
- },
152
- summary: `Reviewer output parse error: ${err.message}`
153
- };
154
- }
85
+ handleParseError(err, agentResult, _provider) {
86
+ return {
87
+ ok: true,
88
+ result: {
89
+ ...agentResult,
90
+ approved: false,
91
+ blocking_issues: [{ id: "PARSE_ERROR", severity: "high", description: `Reviewer output could not be parsed: ${err.message}` }],
92
+ non_blocking_suggestions: [],
93
+ confidence: 0,
94
+ raw_summary: `Parse error: ${err.message}`
95
+ },
96
+ summary: `Reviewer output parse error: ${err.message}`
97
+ };
155
98
  }
156
99
  }
@@ -1,5 +1,4 @@
1
- import { BaseRole } from "./base-role.js";
2
- import { createAgent as defaultCreateAgent } from "../agents/index.js";
1
+ import { AgentRole } from "./agent-role.js";
3
2
  import { extractFirstJson } from "../utils/json-extract.js";
4
3
 
5
4
  const SUBAGENT_PREAMBLE = [
@@ -8,118 +7,55 @@ const SUBAGENT_PREAMBLE = [
8
7
  "Do NOT use any MCP tools. Focus only on auditing code for security vulnerabilities."
9
8
  ].join(" ");
10
9
 
11
- function resolveProvider(config) {
12
- return (
13
- config?.roles?.security?.provider ||
14
- config?.roles?.coder?.provider ||
15
- "claude"
16
- );
17
- }
18
-
19
- function buildPrompt({ task, diff, instructions }) {
20
- const sections = [SUBAGENT_PREAMBLE];
21
-
22
- if (instructions) {
23
- sections.push(instructions);
10
+ export class SecurityRole extends AgentRole {
11
+ constructor(opts) {
12
+ super({ ...opts, name: "security" });
24
13
  }
25
14
 
26
- sections.push(
27
- "You are a security auditor. Analyze the code changes for vulnerabilities.",
28
- "Check for: OWASP top 10, exposed secrets/API keys, hardcoded credentials, command injection, XSS, SQL injection, path traversal, prototype pollution, insecure dependencies.",
29
- "Return a single valid JSON object with your findings and nothing else.",
30
- 'JSON schema: {"vulnerabilities":[{"severity":"critical|high|medium|low","category":string,"file":string,"line":number,"description":string,"fix_suggestion":string}],"verdict":"pass"|"fail"}',
31
- `## Task\n${task}`
32
- );
33
-
34
- if (diff) {
35
- sections.push(`## Git diff to audit\n${diff}`);
15
+ async buildPrompt({ task, diff }) {
16
+ const sections = [SUBAGENT_PREAMBLE];
17
+ if (this.instructions) sections.push(this.instructions);
18
+ sections.push(
19
+ "You are a security auditor. Analyze the code changes for vulnerabilities.",
20
+ "Check for: OWASP top 10, exposed secrets/API keys, hardcoded credentials, command injection, XSS, SQL injection, path traversal, prototype pollution, insecure dependencies.",
21
+ "Return a single valid JSON object with your findings and nothing else.",
22
+ '{"vulnerabilities":[{"severity":"critical|high|medium|low","category":string,"file":string,"line":number,"description":string,"fix_suggestion":string}],"verdict":"pass"|"fail"}',
23
+ `## Task\n${task}`
24
+ );
25
+ if (diff) sections.push(`## Git diff to audit\n${diff}`);
26
+ return { prompt: sections.join("\n\n") };
36
27
  }
37
28
 
38
- return sections.join("\n\n");
39
- }
40
-
41
- function parseSecurityOutput(raw) {
42
- return extractFirstJson(raw);
43
- }
44
-
45
- function buildSummary(parsed) {
46
- const vulns = parsed.vulnerabilities || [];
47
- if (vulns.length === 0) {
48
- return `Verdict: ${parsed.verdict || "pass"}; No vulnerabilities found`;
29
+ extractInput(input) {
30
+ if (typeof input === "string") return { task: input, diff: null };
31
+ return { task: input?.task || this.context?.task || "", diff: input?.diff || null, onOutput: input?.onOutput || null };
49
32
  }
50
33
 
51
- const bySeverity = {};
52
- for (const v of vulns) {
53
- const sev = v.severity || "unknown";
54
- bySeverity[sev] = (bySeverity[sev] || 0) + 1;
55
- }
56
-
57
- const parts = Object.entries(bySeverity)
58
- .sort(([a], [b]) => {
59
- const order = { critical: 0, high: 1, medium: 2, low: 3 };
60
- return (order[a] ?? 4) - (order[b] ?? 4);
61
- })
62
- .map(([sev, count]) => `${count} ${sev}`);
63
-
64
- return `Verdict: ${parsed.verdict || "fail"}; ${parts.join(", ")}`;
65
- }
34
+ parseOutput(raw) { return extractFirstJson(raw); }
66
35
 
67
- export class SecurityRole extends BaseRole {
68
- constructor({ config, logger, emitter = null, createAgentFn = null }) {
69
- super({ name: "security", config, logger, emitter });
70
- this._createAgent = createAgentFn || defaultCreateAgent;
36
+ isSuccessful(parsed) {
37
+ const verdict = parsed.verdict || (parsed.vulnerabilities?.length ? "fail" : "pass");
38
+ return verdict === "pass";
71
39
  }
72
40
 
73
- async execute(input) {
74
- const { task, diff } = typeof input === "string"
75
- ? { task: input, diff: null }
76
- : { task: input?.task || this.context?.task || "", diff: input?.diff || null };
77
-
78
- const provider = resolveProvider(this.config);
79
- const agent = this._createAgent(provider, this.config, this.logger);
80
-
81
- const prompt = buildPrompt({ task, diff, instructions: this.instructions });
82
- const result = await agent.runTask({ prompt, role: "security" });
41
+ buildSuccessResult(parsed, provider) {
42
+ const verdict = parsed.verdict || (parsed.vulnerabilities?.length ? "fail" : "pass");
43
+ return { vulnerabilities: parsed.vulnerabilities || [], verdict, provider };
44
+ }
83
45
 
84
- if (!result.ok) {
85
- return {
86
- ok: false,
87
- result: {
88
- error: result.error || result.output || "Security audit failed",
89
- provider
90
- },
91
- summary: `Security audit failed: ${result.error || "unknown error"}`
92
- };
93
- }
46
+ buildSummary(parsed) {
47
+ const verdict = parsed.verdict || (parsed.vulnerabilities?.length ? "fail" : "pass");
48
+ const vulns = parsed.vulnerabilities || [];
49
+ if (!vulns.length) return `Verdict: ${verdict}; No vulnerabilities found`;
94
50
 
95
- try {
96
- const parsed = parseSecurityOutput(result.output);
97
- if (!parsed) {
98
- return {
99
- ok: false,
100
- result: { error: "Failed to parse security output: no JSON found", provider },
101
- summary: "Security output parse error: no JSON found"
102
- };
103
- }
51
+ const bySeverity = {};
52
+ for (const v of vulns) bySeverity[v.severity || "unknown"] = (bySeverity[v.severity || "unknown"] || 0) + 1;
104
53
 
105
- const verdict = parsed.verdict || (parsed.vulnerabilities?.length ? "fail" : "pass");
106
- const ok = verdict === "pass";
54
+ const order = { critical: 0, high: 1, medium: 2, low: 3 };
55
+ const parts = Object.entries(bySeverity)
56
+ .sort(([a], [b]) => (order[a] ?? 4) - (order[b] ?? 4))
57
+ .map(([sev, count]) => `${count} ${sev}`);
107
58
 
108
- return {
109
- ok,
110
- result: {
111
- vulnerabilities: parsed.vulnerabilities || [],
112
- verdict,
113
- provider
114
- },
115
- summary: buildSummary({ ...parsed, verdict })
116
- };
117
- } catch (err) {
118
- return {
119
- ok: false,
120
- result: { error: `Failed to parse security output: ${err.message}`, provider },
121
- summary: `Security output parse error: ${err.message}`
122
- };
123
- }
59
+ return `Verdict: ${verdict}; ${parts.join(", ")}`;
124
60
  }
125
61
  }