karajan-code 1.16.0 → 1.17.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 (68) hide show
  1. package/package.json +1 -1
  2. package/src/activity-log.js +13 -13
  3. package/src/agents/availability.js +2 -3
  4. package/src/agents/claude-agent.js +42 -21
  5. package/src/agents/model-registry.js +1 -1
  6. package/src/becaria/dispatch.js +1 -1
  7. package/src/becaria/repo.js +3 -3
  8. package/src/cli.js +5 -2
  9. package/src/commands/doctor.js +154 -108
  10. package/src/commands/init.js +101 -90
  11. package/src/commands/plan.js +1 -1
  12. package/src/commands/report.js +77 -71
  13. package/src/commands/roles.js +0 -1
  14. package/src/commands/run.js +2 -3
  15. package/src/config.js +155 -93
  16. package/src/git/automation.js +3 -4
  17. package/src/guards/policy-resolver.js +3 -3
  18. package/src/mcp/orphan-guard.js +1 -2
  19. package/src/mcp/progress.js +4 -3
  20. package/src/mcp/run-kj.js +1 -0
  21. package/src/mcp/server-handlers.js +242 -253
  22. package/src/mcp/server.js +4 -3
  23. package/src/mcp/tools.js +2 -0
  24. package/src/orchestrator/agent-fallback.js +1 -3
  25. package/src/orchestrator/iteration-stages.js +206 -170
  26. package/src/orchestrator/pre-loop-stages.js +200 -34
  27. package/src/orchestrator/solomon-rules.js +2 -2
  28. package/src/orchestrator.js +820 -748
  29. package/src/planning-game/adapter.js +23 -20
  30. package/src/planning-game/architect-adrs.js +45 -0
  31. package/src/planning-game/client.js +15 -1
  32. package/src/planning-game/decomposition.js +7 -5
  33. package/src/prompts/architect.js +88 -0
  34. package/src/prompts/discover.js +54 -53
  35. package/src/prompts/planner.js +53 -33
  36. package/src/prompts/triage.js +8 -16
  37. package/src/review/parser.js +18 -19
  38. package/src/review/profiles.js +2 -2
  39. package/src/review/schema.js +3 -3
  40. package/src/review/scope-filter.js +3 -4
  41. package/src/roles/architect-role.js +122 -0
  42. package/src/roles/commiter-role.js +2 -2
  43. package/src/roles/discover-role.js +59 -67
  44. package/src/roles/index.js +1 -0
  45. package/src/roles/planner-role.js +54 -38
  46. package/src/roles/refactorer-role.js +8 -7
  47. package/src/roles/researcher-role.js +6 -7
  48. package/src/roles/reviewer-role.js +4 -5
  49. package/src/roles/security-role.js +3 -4
  50. package/src/roles/solomon-role.js +6 -18
  51. package/src/roles/sonar-role.js +5 -1
  52. package/src/roles/tester-role.js +8 -5
  53. package/src/roles/triage-role.js +2 -2
  54. package/src/session-cleanup.js +29 -24
  55. package/src/session-store.js +1 -1
  56. package/src/sonar/api.js +1 -1
  57. package/src/sonar/manager.js +1 -1
  58. package/src/sonar/project-key.js +5 -5
  59. package/src/sonar/scanner.js +34 -65
  60. package/src/utils/display.js +312 -272
  61. package/src/utils/git.js +3 -3
  62. package/src/utils/logger.js +6 -1
  63. package/src/utils/model-selector.js +5 -5
  64. package/src/utils/process.js +80 -102
  65. package/src/utils/rate-limit-detector.js +13 -13
  66. package/src/utils/run-log.js +55 -52
  67. package/templates/roles/architect.md +62 -0
  68. package/templates/roles/planner.md +1 -0
@@ -10,7 +10,7 @@ import path from "node:path";
10
10
  import { loadFirstExisting } from "../roles/base-role.js";
11
11
  import { getKarajanHome } from "../utils/paths.js";
12
12
 
13
- const KNOWN_MODES = ["paranoid", "strict", "standard", "relaxed"];
13
+ const KNOWN_MODES = new Set(["paranoid", "strict", "standard", "relaxed"]);
14
14
 
15
15
  const DEFAULT_RULES = "Focus on critical issues: security vulnerabilities, logic errors, and broken tests.";
16
16
 
@@ -38,7 +38,7 @@ function buildCandidates(fileName, projectDir) {
38
38
 
39
39
  export async function resolveReviewProfile({ mode = "standard", projectDir } = {}) {
40
40
  // Known modes: try mode-specific file first, then base reviewer.md
41
- if (KNOWN_MODES.includes(mode)) {
41
+ if (KNOWN_MODES.has(mode)) {
42
42
  const modePaths = buildCandidates(`reviewer-${mode}.md`, projectDir);
43
43
  const modeRules = await loadFirstExisting(modePaths);
44
44
  if (modeRules) {
@@ -1,14 +1,14 @@
1
1
  export function validateReviewResult(reviewResult) {
2
2
  if (!reviewResult || typeof reviewResult !== "object") {
3
- throw new Error("Reviewer output must be a JSON object");
3
+ throw new TypeError("Reviewer output must be a JSON object");
4
4
  }
5
5
 
6
6
  if (typeof reviewResult.approved !== "boolean") {
7
- throw new Error("Reviewer output missing boolean field: approved");
7
+ throw new TypeError("Reviewer output missing boolean field: approved");
8
8
  }
9
9
 
10
10
  if (!Array.isArray(reviewResult.blocking_issues)) {
11
- throw new Error("Reviewer output missing array field: blocking_issues");
11
+ throw new TypeError("Reviewer output missing array field: blocking_issues");
12
12
  }
13
13
 
14
14
  if (!Array.isArray(reviewResult.non_blocking_suggestions)) {
@@ -17,7 +17,7 @@ export function extractDiffFiles(diff) {
17
17
  const files = new Set();
18
18
  for (const line of (diff || "").split("\n")) {
19
19
  // Match "+++ b/path" lines in unified diff
20
- const m = line.match(/^\+\+\+ b\/(.+)/);
20
+ const m = /^\+\+\+ b\/(.+)/.exec(line);
21
21
  if (m) files.add(m[1]);
22
22
  }
23
23
  return files;
@@ -50,7 +50,7 @@ export function isIssueInScope(issue, diffFiles, diffContent) {
50
50
 
51
51
  // Check if the file path appears anywhere in the diff content
52
52
  // (covers cases where the file is referenced in imports/requires)
53
- if (diffContent && diffContent.includes(file)) return true;
53
+ if (diffContent?.includes(file)) return true;
54
54
 
55
55
  return false;
56
56
  }
@@ -146,8 +146,7 @@ export function buildDeferredContext(deferredIssues) {
146
146
  lines.push(`- [${issue.severity}] ${file}: ${issue.description}${fix}`);
147
147
  }
148
148
 
149
- lines.push("");
150
- lines.push("If your current changes naturally address any of these, great. Otherwise, they will be tracked for future resolution.");
149
+ lines.push("", "If your current changes naturally address any of these, great. Otherwise, they will be tracked for future resolution.");
151
150
 
152
151
  return lines.join("\n");
153
152
  }
@@ -0,0 +1,122 @@
1
+ import { BaseRole } from "./base-role.js";
2
+ import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
+ import { buildArchitectPrompt, parseArchitectOutput } from "../prompts/architect.js";
4
+
5
+ function resolveProvider(config) {
6
+ return (
7
+ config?.roles?.architect?.provider ||
8
+ config?.roles?.coder?.provider ||
9
+ "claude"
10
+ );
11
+ }
12
+
13
+ function buildSummary(parsed) {
14
+ const parts = [];
15
+ const arch = parsed.architecture;
16
+
17
+ if (arch.type) parts.push(arch.type);
18
+
19
+ const layers = arch.layers?.length || 0;
20
+ if (layers) parts.push(`${layers} layer${layers === 1 ? "" : "s"}`);
21
+
22
+ const patterns = arch.patterns?.length || 0;
23
+ if (patterns) parts.push(`${patterns} pattern${patterns === 1 ? "" : "s"}`);
24
+
25
+ const entities = arch.dataModel?.entities?.length || 0;
26
+ if (entities) parts.push(`${entities} entit${entities === 1 ? "y" : "ies"}`);
27
+
28
+ const questions = parsed.questions?.length || 0;
29
+ if (questions) parts.push(`${questions} question${questions === 1 ? "" : "s"}`);
30
+
31
+ return parts.length
32
+ ? `Architecture complete: ${parts.join(", ")} (verdict: ${parsed.verdict})`
33
+ : "Architecture complete";
34
+ }
35
+
36
+ const EMPTY_ARCHITECTURE = {
37
+ type: "",
38
+ layers: [],
39
+ patterns: [],
40
+ dataModel: { entities: [] },
41
+ apiContracts: [],
42
+ dependencies: [],
43
+ tradeoffs: []
44
+ };
45
+
46
+ export class ArchitectRole extends BaseRole {
47
+ constructor({ config, logger, emitter = null, createAgentFn = null }) {
48
+ super({ name: "architect", config, logger, emitter });
49
+ this._createAgent = createAgentFn || defaultCreateAgent;
50
+ }
51
+
52
+ async execute(input) {
53
+ const task = typeof input === "string"
54
+ ? input
55
+ : input?.task || this.context?.task || "";
56
+ const onOutput = typeof input === "string" ? null : input?.onOutput || null;
57
+ const researchContext = typeof input === "object" ? input?.researchContext || null : null;
58
+
59
+ const provider = resolveProvider(this.config);
60
+ const agent = this._createAgent(provider, this.config, this.logger);
61
+
62
+ const prompt = buildArchitectPrompt({ task, instructions: this.instructions, researchContext });
63
+ const runArgs = { prompt, role: "architect" };
64
+ if (onOutput) runArgs.onOutput = onOutput;
65
+ const result = await agent.runTask(runArgs);
66
+
67
+ if (!result.ok) {
68
+ return {
69
+ ok: false,
70
+ result: {
71
+ error: result.error || result.output || "Architect failed",
72
+ provider
73
+ },
74
+ summary: `Architect failed: ${result.error || "unknown error"}`,
75
+ usage: result.usage
76
+ };
77
+ }
78
+
79
+ try {
80
+ const parsed = parseArchitectOutput(result.output);
81
+ if (!parsed) {
82
+ return {
83
+ ok: true,
84
+ result: {
85
+ verdict: "needs_clarification",
86
+ architecture: { ...EMPTY_ARCHITECTURE },
87
+ questions: [],
88
+ raw: result.output,
89
+ provider
90
+ },
91
+ summary: "Architecture complete (unstructured output)",
92
+ usage: result.usage
93
+ };
94
+ }
95
+
96
+ return {
97
+ ok: true,
98
+ result: {
99
+ verdict: parsed.verdict,
100
+ architecture: parsed.architecture,
101
+ questions: parsed.questions,
102
+ provider
103
+ },
104
+ summary: buildSummary(parsed),
105
+ usage: result.usage
106
+ };
107
+ } catch {
108
+ return {
109
+ ok: true,
110
+ result: {
111
+ verdict: "needs_clarification",
112
+ architecture: { ...EMPTY_ARCHITECTURE },
113
+ questions: [],
114
+ raw: result.output,
115
+ provider
116
+ },
117
+ summary: "Architecture complete (unstructured output)",
118
+ usage: result.usage
119
+ };
120
+ }
121
+ }
122
+ }
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  function buildCommitMessage(task) {
15
15
  const clean = String(task || "")
16
- .replace(/\s+/g, " ")
16
+ .replaceAll(/\s+/g, " ")
17
17
  .trim();
18
18
  const prefix = "feat: ";
19
19
  const maxBody = 72 - prefix.length;
@@ -22,7 +22,7 @@ function buildCommitMessage(task) {
22
22
 
23
23
  function buildPrTitle(task) {
24
24
  const clean = String(task || "")
25
- .replace(/\s+/g, " ")
25
+ .replaceAll(/\s+/g, " ")
26
26
  .trim();
27
27
  return clean.slice(0, 70) || "Karajan update";
28
28
  }
@@ -10,31 +10,76 @@ function resolveProvider(config) {
10
10
  );
11
11
  }
12
12
 
13
- function buildSummary(parsed, mode) {
14
- const gapCount = parsed.gaps?.length || 0;
15
- if (gapCount === 0 && mode !== "wendel" && mode !== "jtbd") return "Discovery complete: task is ready";
13
+ function pluralize(count, singular, plural) {
14
+ return `${count} ${count === 1 ? singular : plural}`;
15
+ }
16
+
17
+ function collectModeParts(parsed, mode, gapCount) {
16
18
  const parts = [];
17
- if (gapCount > 0) parts.push(`${gapCount} gap${gapCount !== 1 ? "s" : ""} found`);
19
+ if (gapCount > 0) parts.push(`${pluralize(gapCount, "gap", "gaps")} found`);
20
+
18
21
  if (mode === "momtest") {
19
22
  const qCount = parsed.momTestQuestions?.length || 0;
20
- if (qCount > 0) parts.push(`${qCount} Mom Test question${qCount !== 1 ? "s" : ""}`);
23
+ if (qCount > 0) parts.push(`${pluralize(qCount, "Mom Test question", "Mom Test questions")}`);
21
24
  }
22
25
  if (mode === "wendel") {
23
26
  const failCount = (parsed.wendelChecklist || []).filter(c => c.status === "fail").length;
24
- if (failCount > 0) parts.push(`${failCount} Wendel condition${failCount !== 1 ? "s" : ""} failed`);
25
- else if (gapCount === 0) return "Discovery complete: task is ready";
27
+ if (failCount > 0) parts.push(`${pluralize(failCount, "Wendel condition", "Wendel conditions")} failed`);
26
28
  }
27
29
  if (mode === "classify" && parsed.classification) {
28
30
  parts.push(`type: ${parsed.classification.type}, risk: ${parsed.classification.adoptionRisk}`);
29
31
  }
30
32
  if (mode === "jtbd") {
31
33
  const jCount = parsed.jtbds?.length || 0;
32
- if (jCount > 0) parts.push(`${jCount} JTBD${jCount !== 1 ? "s" : ""} generated`);
33
- else if (gapCount === 0) return "Discovery complete: task is ready";
34
+ if (jCount > 0) parts.push(`${pluralize(jCount, "JTBD", "JTBDs")} generated`);
34
35
  }
36
+ return parts;
37
+ }
38
+
39
+ function buildSummary(parsed, mode) {
40
+ const gapCount = parsed.gaps?.length || 0;
41
+ const parts = collectModeParts(parsed, mode, gapCount);
42
+ if (parts.length === 0) return "Discovery complete: task is ready";
35
43
  return `Discovery complete: ${parts.join(", ")} (verdict: ${parsed.verdict})`;
36
44
  }
37
45
 
46
+ function resolveInput(input, context) {
47
+ if (typeof input === "string") {
48
+ return { task: input, onOutput: null, mode: "gaps", context: null };
49
+ }
50
+ return {
51
+ task: input?.task || context?.task || "",
52
+ onOutput: input?.onOutput || null,
53
+ mode: input?.mode || "gaps",
54
+ context: input?.context || null
55
+ };
56
+ }
57
+
58
+ function buildUnstructuredResult(output, mode, provider, usage) {
59
+ return {
60
+ ok: true,
61
+ result: { verdict: "ready", gaps: [], mode, raw: output, provider },
62
+ summary: "Discovery complete (unstructured output)",
63
+ usage
64
+ };
65
+ }
66
+
67
+ const MODE_FIELDS = {
68
+ momtest: { key: "momTestQuestions", fallback: [] },
69
+ wendel: { key: "wendelChecklist", fallback: [] },
70
+ classify: { key: "classification", fallback: null },
71
+ jtbd: { key: "jtbds", fallback: [] }
72
+ };
73
+
74
+ function buildResultFromParsed(parsed, mode, provider) {
75
+ const resultObj = { verdict: parsed.verdict, gaps: parsed.gaps, mode, provider };
76
+ const fieldDef = MODE_FIELDS[mode];
77
+ if (fieldDef) {
78
+ resultObj[fieldDef.key] = parsed[fieldDef.key] ?? fieldDef.fallback;
79
+ }
80
+ return resultObj;
81
+ }
82
+
38
83
  export class DiscoverRole extends BaseRole {
39
84
  constructor({ config, logger, emitter = null, createAgentFn = null }) {
40
85
  super({ name: "discover", config, logger, emitter });
@@ -42,13 +87,7 @@ export class DiscoverRole extends BaseRole {
42
87
  }
43
88
 
44
89
  async execute(input) {
45
- const task = typeof input === "string"
46
- ? input
47
- : input?.task || this.context?.task || "";
48
- const onOutput = typeof input === "string" ? null : input?.onOutput || null;
49
- const mode = (typeof input === "object" ? input?.mode : null) || "gaps";
50
- const context = typeof input === "object" ? input?.context || null : null;
51
-
90
+ const { task, onOutput, mode, context } = resolveInput(input, this.context);
52
91
  const provider = resolveProvider(this.config);
53
92
  const agent = this._createAgent(provider, this.config, this.logger);
54
93
 
@@ -60,11 +99,7 @@ export class DiscoverRole extends BaseRole {
60
99
  if (!result.ok) {
61
100
  return {
62
101
  ok: false,
63
- result: {
64
- error: result.error || result.output || "Discovery failed",
65
- provider,
66
- mode
67
- },
102
+ result: { error: result.error || result.output || "Discovery failed", provider, mode },
68
103
  summary: `Discovery failed: ${result.error || "unknown error"}`,
69
104
  usage: result.usage
70
105
  };
@@ -72,59 +107,16 @@ export class DiscoverRole extends BaseRole {
72
107
 
73
108
  try {
74
109
  const parsed = parseDiscoverOutput(result.output);
75
- if (!parsed) {
76
- return {
77
- ok: true,
78
- result: {
79
- verdict: "ready",
80
- gaps: [],
81
- mode,
82
- raw: result.output,
83
- provider
84
- },
85
- summary: "Discovery complete (unstructured output)",
86
- usage: result.usage
87
- };
88
- }
89
-
90
- const resultObj = {
91
- verdict: parsed.verdict,
92
- gaps: parsed.gaps,
93
- mode,
94
- provider
95
- };
96
- if (mode === "momtest") {
97
- resultObj.momTestQuestions = parsed.momTestQuestions || [];
98
- }
99
- if (mode === "wendel") {
100
- resultObj.wendelChecklist = parsed.wendelChecklist || [];
101
- }
102
- if (mode === "classify") {
103
- resultObj.classification = parsed.classification || null;
104
- }
105
- if (mode === "jtbd") {
106
- resultObj.jtbds = parsed.jtbds || [];
107
- }
110
+ if (!parsed) return buildUnstructuredResult(result.output, mode, provider, result.usage);
108
111
 
109
112
  return {
110
113
  ok: true,
111
- result: resultObj,
114
+ result: buildResultFromParsed(parsed, mode, provider),
112
115
  summary: buildSummary(parsed, mode),
113
116
  usage: result.usage
114
117
  };
115
118
  } catch {
116
- return {
117
- ok: true,
118
- result: {
119
- verdict: "ready",
120
- gaps: [],
121
- mode,
122
- raw: result.output,
123
- provider
124
- },
125
- summary: "Discovery complete (unstructured output)",
126
- usage: result.usage
127
- };
119
+ return buildUnstructuredResult(result.output, mode, provider, result.usage);
128
120
  }
129
121
  }
130
122
  }
@@ -11,3 +11,4 @@ export { TesterRole } from "./tester-role.js";
11
11
  export { SecurityRole } from "./security-role.js";
12
12
  export { SolomonRole } from "./solomon-role.js";
13
13
  export { DiscoverRole } from "./discover-role.js";
14
+ export { ArchitectRole } from "./architect-role.js";
@@ -21,51 +21,66 @@ function resolvePlannerRuntimeTimeoutMs(config) {
21
21
  return Math.round(minutes * 60 * 1000);
22
22
  }
23
23
 
24
- function buildPrompt({ task, instructions, research, triageDecomposition }) {
25
- const sections = [];
24
+ function appendDecompositionSection(sections, triageDecomposition) {
25
+ if (!triageDecomposition?.length) return;
26
+ sections.push("## Triage decomposition recommendation", "The triage stage determined this task should be decomposed. Suggested subtasks:");
27
+ for (let i = 0; i < triageDecomposition.length; i++) {
28
+ sections.push(`${i + 1}. ${triageDecomposition[i]}`);
29
+ }
30
+ sections.push("", "Focus your plan on the FIRST subtask only. List the remaining subtasks as 'pending_subtasks' in your output for documentation.", "");
31
+ }
26
32
 
27
- if (instructions) {
28
- sections.push(instructions);
29
- sections.push("");
33
+ const RESEARCH_FIELDS = [
34
+ { key: "affected_files", label: "Affected files" },
35
+ { key: "patterns", label: "Patterns" },
36
+ { key: "constraints", label: "Constraints" },
37
+ { key: "risks", label: "Risks" },
38
+ { key: "prior_decisions", label: "Prior decisions" }
39
+ ];
40
+
41
+ function appendResearchSection(sections, research) {
42
+ if (!research) return;
43
+ sections.push("## Research findings");
44
+ for (const { key, label } of RESEARCH_FIELDS) {
45
+ if (research[key]?.length) {
46
+ sections.push(`${label}: ${research[key].join(", ")}`);
47
+ }
30
48
  }
49
+ sections.push("");
50
+ }
31
51
 
32
- sections.push("Create an implementation plan for this task.");
33
- sections.push("Return concise numbered steps focused on execution order and risk.");
52
+ function appendArchitectSection(sections, architectContext) {
53
+ if (!architectContext) return;
54
+ const arch = architectContext.architecture || {};
55
+ sections.push("## Architecture context");
56
+ if (arch.type) sections.push(`Type: ${arch.type}`);
57
+ if (arch.layers?.length) sections.push(`Layers: ${arch.layers.join(", ")}`);
58
+ if (arch.patterns?.length) sections.push(`Patterns: ${arch.patterns.join(", ")}`);
59
+ if (arch.dataModel?.entities?.length) sections.push(`Data model entities: ${arch.dataModel.entities.join(", ")}`);
60
+ if (arch.apiContracts?.length) sections.push(`API contracts: ${arch.apiContracts.join(", ")}`);
61
+ if (arch.tradeoffs?.length) sections.push(`Tradeoffs: ${arch.tradeoffs.join(", ")}`);
62
+ if (architectContext.summary) sections.push(`Summary: ${architectContext.summary}`);
34
63
  sections.push("");
64
+ }
35
65
 
36
- if (triageDecomposition?.length) {
37
- sections.push("## Triage decomposition recommendation");
38
- sections.push("The triage stage determined this task should be decomposed. Suggested subtasks:");
39
- for (let i = 0; i < triageDecomposition.length; i++) {
40
- sections.push(`${i + 1}. ${triageDecomposition[i]}`);
41
- }
42
- sections.push("");
43
- sections.push("Focus your plan on the FIRST subtask only. List the remaining subtasks as 'pending_subtasks' in your output for documentation.");
44
- sections.push("");
45
- }
66
+ function buildPrompt({ task, instructions, research, triageDecomposition, architectContext }) {
67
+ const sections = [];
46
68
 
47
- if (research) {
48
- sections.push("## Research findings");
49
- if (research.affected_files?.length) {
50
- sections.push(`Affected files: ${research.affected_files.join(", ")}`);
51
- }
52
- if (research.patterns?.length) {
53
- sections.push(`Patterns: ${research.patterns.join(", ")}`);
54
- }
55
- if (research.constraints?.length) {
56
- sections.push(`Constraints: ${research.constraints.join(", ")}`);
57
- }
58
- if (research.risks?.length) {
59
- sections.push(`Risks: ${research.risks.join(", ")}`);
60
- }
61
- if (research.prior_decisions?.length) {
62
- sections.push(`Prior decisions: ${research.prior_decisions.join(", ")}`);
63
- }
64
- sections.push("");
69
+ if (instructions) {
70
+ sections.push(instructions, "");
65
71
  }
66
72
 
67
- sections.push("## Task");
68
- sections.push(task);
73
+ sections.push(
74
+ "Create an implementation plan for this task.",
75
+ "Return concise numbered steps focused on execution order and risk.",
76
+ ""
77
+ );
78
+
79
+ appendDecompositionSection(sections, triageDecomposition);
80
+ appendArchitectSection(sections, architectContext);
81
+ appendResearchSection(sections, research);
82
+
83
+ sections.push("## Task", task);
69
84
 
70
85
  return sections.join("\n");
71
86
  }
@@ -83,10 +98,11 @@ export class PlannerRole extends BaseRole {
83
98
  const taskStr = task || this.context?.task || "";
84
99
  const research = this.context?.research || null;
85
100
  const triageDecomposition = this.context?.triageDecomposition || null;
101
+ const architectContext = this.context?.architecture || null;
86
102
  const provider = resolveProvider(this.config);
87
103
 
88
104
  const agent = this._createAgent(provider, this.config, this.logger);
89
- const prompt = buildPrompt({ task: taskStr, instructions: this.instructions, research, triageDecomposition });
105
+ const prompt = buildPrompt({ task: taskStr, instructions: this.instructions, research, triageDecomposition, architectContext });
90
106
 
91
107
  const runArgs = { prompt, role: "planner" };
92
108
  if (onOutput) runArgs.onOutput = onOutput;
@@ -13,15 +13,16 @@ function buildPrompt({ task, instructions }) {
13
13
  const sections = [];
14
14
 
15
15
  if (instructions) {
16
- sections.push(instructions);
17
- sections.push("");
16
+ sections.push(instructions, "");
18
17
  }
19
18
 
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);
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
+ );
25
26
 
26
27
  return sections.join("\n");
27
28
  }
@@ -26,17 +26,16 @@ function buildPrompt({ task, instructions }) {
26
26
  "Investigate the codebase for the following task.",
27
27
  "Identify affected files, patterns, constraints, prior decisions, risks, and test coverage.",
28
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}'
29
+ 'JSON schema: {"affected_files":[string],"patterns":[string],"constraints":[string],"prior_decisions":[string],"risks":[string],"test_coverage":string}',
30
+ `## Task\n${task}`
30
31
  );
31
32
 
32
- sections.push(`## Task\n${task}`);
33
-
34
33
  return sections.join("\n\n");
35
34
  }
36
35
 
37
36
  function parseResearchOutput(raw) {
38
37
  const text = raw?.trim() || "";
39
- const jsonMatch = text.match(/\{[\s\S]*\}/);
38
+ const jsonMatch = /\{[\s\S]*\}/.exec(text);
40
39
  if (!jsonMatch) return null;
41
40
  return JSON.parse(jsonMatch[0]);
42
41
  }
@@ -46,9 +45,9 @@ function buildSummary(parsed) {
46
45
  const risks = parsed.risks?.length || 0;
47
46
  const patterns = parsed.patterns?.length || 0;
48
47
  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" : ""}`);
48
+ if (files) parts.push(`${files} file${files === 1 ? "" : "s"}`);
49
+ if (risks) parts.push(`${risks} risk${risks === 1 ? "" : "s"}`);
50
+ if (patterns) parts.push(`${patterns} pattern${patterns === 1 ? "" : "s"}`);
52
51
  return parts.length
53
52
  ? `Research complete: ${parts.join(", ")} identified`
54
53
  : "Research complete";
@@ -33,15 +33,14 @@ function buildPrompt({ task, diff, reviewRules, reviewMode, instructions }) {
33
33
  sections.push(instructions);
34
34
  }
35
35
 
36
- sections.push(`You are a code reviewer in ${reviewMode || "standard"} mode.`);
37
36
  sections.push(
37
+ `You are a code reviewer in ${reviewMode || "standard"} mode.`,
38
38
  "Return only one valid JSON object and nothing else.",
39
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}'
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
+ `Task context:\n${task}`
41
42
  );
42
43
 
43
- sections.push(`Task context:\n${task}`);
44
-
45
44
  if (reviewRules) {
46
45
  sections.push(`Review rules:\n${reviewRules}`);
47
46
  }
@@ -53,7 +52,7 @@ function buildPrompt({ task, diff, reviewRules, reviewMode, instructions }) {
53
52
 
54
53
  function parseReviewOutput(raw) {
55
54
  const text = raw?.trim() || "";
56
- const jsonMatch = text.match(/\{[\s\S]*\}/);
55
+ const jsonMatch = /\{[\s\S]*\}/.exec(text);
57
56
  if (!jsonMatch) {
58
57
  throw new Error(`Failed to parse reviewer output: no JSON found`);
59
58
  }
@@ -26,11 +26,10 @@ function buildPrompt({ task, diff, instructions }) {
26
26
  "You are a security auditor. Analyze the code changes for vulnerabilities.",
27
27
  "Check for: OWASP top 10, exposed secrets/API keys, hardcoded credentials, command injection, XSS, SQL injection, path traversal, prototype pollution, insecure dependencies.",
28
28
  "Return a single valid JSON object with your findings and nothing else.",
29
- 'JSON schema: {"vulnerabilities":[{"severity":"critical|high|medium|low","category":string,"file":string,"line":number,"description":string,"fix_suggestion":string}],"verdict":"pass"|"fail"}'
29
+ 'JSON schema: {"vulnerabilities":[{"severity":"critical|high|medium|low","category":string,"file":string,"line":number,"description":string,"fix_suggestion":string}],"verdict":"pass"|"fail"}',
30
+ `## Task\n${task}`
30
31
  );
31
32
 
32
- sections.push(`## Task\n${task}`);
33
-
34
33
  if (diff) {
35
34
  sections.push(`## Git diff to audit\n${diff}`);
36
35
  }
@@ -40,7 +39,7 @@ function buildPrompt({ task, diff, instructions }) {
40
39
 
41
40
  function parseSecurityOutput(raw) {
42
41
  const text = raw?.trim() || "";
43
- const jsonMatch = text.match(/\{[\s\S]*\}/);
42
+ const jsonMatch = /\{[\s\S]*\}/.exec(text);
44
43
  if (!jsonMatch) return null;
45
44
  return JSON.parse(jsonMatch[0]);
46
45
  }