karajan-code 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/docs/karajan-code-logo-small.png +0 -0
- package/package.json +60 -0
- package/scripts/install.js +898 -0
- package/scripts/install.sh +7 -0
- package/scripts/postinstall.js +117 -0
- package/scripts/setup-multi-instance.sh +150 -0
- package/src/activity-log.js +59 -0
- package/src/agents/aider-agent.js +25 -0
- package/src/agents/availability.js +32 -0
- package/src/agents/base-agent.js +27 -0
- package/src/agents/claude-agent.js +24 -0
- package/src/agents/codex-agent.js +27 -0
- package/src/agents/gemini-agent.js +25 -0
- package/src/agents/index.js +19 -0
- package/src/agents/resolve-bin.js +60 -0
- package/src/cli.js +200 -0
- package/src/commands/code.js +32 -0
- package/src/commands/config.js +74 -0
- package/src/commands/doctor.js +155 -0
- package/src/commands/init.js +181 -0
- package/src/commands/plan.js +67 -0
- package/src/commands/report.js +340 -0
- package/src/commands/resume.js +39 -0
- package/src/commands/review.js +26 -0
- package/src/commands/roles.js +117 -0
- package/src/commands/run.js +91 -0
- package/src/commands/scan.js +18 -0
- package/src/commands/sonar.js +53 -0
- package/src/config.js +322 -0
- package/src/git/automation.js +100 -0
- package/src/mcp/progress.js +69 -0
- package/src/mcp/run-kj.js +87 -0
- package/src/mcp/server-handlers.js +259 -0
- package/src/mcp/server.js +37 -0
- package/src/mcp/tool-arg-normalizers.js +16 -0
- package/src/mcp/tools.js +184 -0
- package/src/orchestrator.js +1277 -0
- package/src/planning-game/adapter.js +105 -0
- package/src/planning-game/client.js +81 -0
- package/src/prompts/coder.js +60 -0
- package/src/prompts/planner.js +26 -0
- package/src/prompts/reviewer.js +45 -0
- package/src/repeat-detector.js +77 -0
- package/src/review/diff-generator.js +22 -0
- package/src/review/parser.js +93 -0
- package/src/review/profiles.js +66 -0
- package/src/review/schema.js +31 -0
- package/src/review/tdd-policy.js +57 -0
- package/src/roles/base-role.js +127 -0
- package/src/roles/coder-role.js +60 -0
- package/src/roles/commiter-role.js +94 -0
- package/src/roles/index.js +12 -0
- package/src/roles/planner-role.js +81 -0
- package/src/roles/refactorer-role.js +66 -0
- package/src/roles/researcher-role.js +134 -0
- package/src/roles/reviewer-role.js +132 -0
- package/src/roles/security-role.js +128 -0
- package/src/roles/solomon-role.js +199 -0
- package/src/roles/sonar-role.js +65 -0
- package/src/roles/tester-role.js +114 -0
- package/src/roles/triage-role.js +128 -0
- package/src/session-store.js +80 -0
- package/src/sonar/api.js +78 -0
- package/src/sonar/enforcer.js +19 -0
- package/src/sonar/manager.js +163 -0
- package/src/sonar/project-key.js +83 -0
- package/src/sonar/scanner.js +267 -0
- package/src/utils/agent-detect.js +32 -0
- package/src/utils/budget.js +123 -0
- package/src/utils/display.js +346 -0
- package/src/utils/events.js +23 -0
- package/src/utils/fs.js +19 -0
- package/src/utils/git.js +101 -0
- package/src/utils/logger.js +86 -0
- package/src/utils/paths.js +18 -0
- package/src/utils/pricing.js +28 -0
- package/src/utils/process.js +67 -0
- package/src/utils/wizard.js +41 -0
- package/templates/coder-rules.md +24 -0
- package/templates/docker-compose.sonar.yml +60 -0
- package/templates/kj.config.yml +82 -0
- package/templates/review-rules.md +11 -0
- package/templates/roles/coder.md +42 -0
- package/templates/roles/commiter.md +44 -0
- package/templates/roles/planner.md +45 -0
- package/templates/roles/refactorer.md +39 -0
- package/templates/roles/researcher.md +37 -0
- package/templates/roles/reviewer-paranoid.md +38 -0
- package/templates/roles/reviewer-relaxed.md +34 -0
- package/templates/roles/reviewer-strict.md +37 -0
- package/templates/roles/reviewer.md +55 -0
- package/templates/roles/security.md +54 -0
- package/templates/roles/solomon.md +106 -0
- package/templates/roles/sonar.md +49 -0
- package/templates/roles/tester.md +41 -0
- package/templates/roles/triage.md +25 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planning Game MCP adapter.
|
|
3
|
+
* Handles card ID detection, task enrichment, and completion updates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const CARD_ID_PATTERN = /[A-Z0-9]{2,5}-(?:TSK|BUG|PCS|PRP|SPR|QA)-\d{4}/;
|
|
7
|
+
|
|
8
|
+
export function parseCardId(text) {
|
|
9
|
+
if (!text) return null;
|
|
10
|
+
const match = String(text).match(CARD_ID_PATTERN);
|
|
11
|
+
return match ? match[0] : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildTaskFromCard(card) {
|
|
15
|
+
const parts = [`## ${card.cardId}: ${card.title}`];
|
|
16
|
+
|
|
17
|
+
if (card.descriptionStructured?.length) {
|
|
18
|
+
parts.push("", "### User Story");
|
|
19
|
+
for (const s of card.descriptionStructured) {
|
|
20
|
+
parts.push(`- **Como** ${s.role}`);
|
|
21
|
+
parts.push(` **Quiero** ${s.goal}`);
|
|
22
|
+
parts.push(` **Para** ${s.benefit}`);
|
|
23
|
+
}
|
|
24
|
+
} else if (card.description) {
|
|
25
|
+
parts.push("", "### Description", card.description);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (card.acceptanceCriteriaStructured?.length) {
|
|
29
|
+
parts.push("", "### Acceptance Criteria");
|
|
30
|
+
for (const ac of card.acceptanceCriteriaStructured) {
|
|
31
|
+
if (ac.given && ac.when && ac.then) {
|
|
32
|
+
parts.push(`- **Given** ${ac.given}`);
|
|
33
|
+
parts.push(` **When** ${ac.when}`);
|
|
34
|
+
parts.push(` **Then** ${ac.then}`);
|
|
35
|
+
} else if (ac.raw) {
|
|
36
|
+
parts.push(`- ${ac.raw}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} else if (card.acceptanceCriteria) {
|
|
40
|
+
parts.push("", "### Acceptance Criteria", card.acceptanceCriteria);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (card.implementationPlan) {
|
|
44
|
+
const plan = card.implementationPlan;
|
|
45
|
+
parts.push("", "### Implementation Plan");
|
|
46
|
+
if (plan.approach) parts.push(`**Approach:** ${plan.approach}`);
|
|
47
|
+
if (plan.steps?.length) {
|
|
48
|
+
parts.push("**Steps:**");
|
|
49
|
+
for (const step of plan.steps) {
|
|
50
|
+
parts.push(`1. ${step.description}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return parts.join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildCommitsPayload(gitLog) {
|
|
59
|
+
if (!gitLog || !gitLog.length) return [];
|
|
60
|
+
return gitLog.map((entry) => ({
|
|
61
|
+
hash: entry.hash,
|
|
62
|
+
message: entry.message,
|
|
63
|
+
date: entry.date,
|
|
64
|
+
author: entry.author
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildCompletionUpdates({ approved, commits, startDate, codeveloper }) {
|
|
69
|
+
if (!approved) return {};
|
|
70
|
+
|
|
71
|
+
const updates = {
|
|
72
|
+
status: "To Validate",
|
|
73
|
+
endDate: new Date().toISOString(),
|
|
74
|
+
developer: "dev_016",
|
|
75
|
+
commits: commits || []
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (startDate) updates.startDate = startDate;
|
|
79
|
+
if (codeveloper) updates.codeveloper = codeveloper;
|
|
80
|
+
|
|
81
|
+
return updates;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildTaskPrompt({ task, card }) {
|
|
85
|
+
const cardId = parseCardId(task) || card?.cardId || null;
|
|
86
|
+
const prompt = card ? buildTaskFromCard(card) : task;
|
|
87
|
+
return { cardId, prompt };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function updateCardOnCompletion({
|
|
91
|
+
client,
|
|
92
|
+
projectId,
|
|
93
|
+
cardId,
|
|
94
|
+
firebaseId,
|
|
95
|
+
approved,
|
|
96
|
+
gitLog,
|
|
97
|
+
startDate,
|
|
98
|
+
codeveloper
|
|
99
|
+
}) {
|
|
100
|
+
if (!approved) return null;
|
|
101
|
+
|
|
102
|
+
const commits = buildCommitsPayload(gitLog);
|
|
103
|
+
const updates = buildCompletionUpdates({ approved, commits, startDate, codeveloper });
|
|
104
|
+
return client.updateCard({ projectId, cardId, firebaseId, updates });
|
|
105
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planning Game MCP client.
|
|
3
|
+
* Communicates with Planning Game via HTTP API or MCP tool calls.
|
|
4
|
+
*
|
|
5
|
+
* When Karajan runs as an MCP server (kj_run), the caller (Claude Code)
|
|
6
|
+
* is expected to provide card data directly via pgTask/pgProject flags.
|
|
7
|
+
*
|
|
8
|
+
* This client provides standalone HTTP access for CLI usage (kj run --pg-task).
|
|
9
|
+
* Requires planning_game.api_url in config or PG_API_URL env var.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_API_URL = "http://localhost:3000/api";
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
14
|
+
|
|
15
|
+
function getApiUrl() {
|
|
16
|
+
return process.env.PG_API_URL || DEFAULT_API_URL;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
22
|
+
try {
|
|
23
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error?.name === "AbortError") {
|
|
26
|
+
throw new Error(`Planning Game API timeout after ${timeoutMs}ms`);
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Planning Game network error: ${error?.message || "unknown error"}`);
|
|
29
|
+
} finally {
|
|
30
|
+
clearTimeout(timeoutId);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function parseJsonResponse(response) {
|
|
35
|
+
try {
|
|
36
|
+
return await response.json();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error(`Planning Game invalid response: ${error?.message || "invalid JSON"}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function fetchCard({ projectId, cardId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
43
|
+
const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards/${encodeURIComponent(cardId)}`;
|
|
44
|
+
const response = await fetchWithTimeout(url, {}, timeoutMs);
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Planning Game API error: ${response.status} ${response.statusText}`);
|
|
47
|
+
}
|
|
48
|
+
const data = await parseJsonResponse(response);
|
|
49
|
+
return data?.card || data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getCard({ projectId, cardId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
53
|
+
return fetchCard({ projectId, cardId, timeoutMs });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function listCards({ projectId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
57
|
+
const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards`;
|
|
58
|
+
const response = await fetchWithTimeout(url, {}, timeoutMs);
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(`Planning Game API error: ${response.status} ${response.statusText}`);
|
|
61
|
+
}
|
|
62
|
+
const data = await parseJsonResponse(response);
|
|
63
|
+
return data?.cards || data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function updateCard({ projectId, cardId, firebaseId, updates, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
67
|
+
const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards/${encodeURIComponent(firebaseId)}`;
|
|
68
|
+
const response = await fetchWithTimeout(
|
|
69
|
+
url,
|
|
70
|
+
{
|
|
71
|
+
method: "PATCH",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ updates })
|
|
74
|
+
},
|
|
75
|
+
timeoutMs
|
|
76
|
+
);
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(`Planning Game API error: ${response.status} ${response.statusText}`);
|
|
79
|
+
}
|
|
80
|
+
return parseJsonResponse(response);
|
|
81
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const SUBAGENT_PREAMBLE = [
|
|
2
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
3
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
4
|
+
"Execute the task directly. Do NOT use any MCP tools. Focus only on coding."
|
|
5
|
+
].join(" ");
|
|
6
|
+
|
|
7
|
+
const SUBAGENT_PREAMBLE_SERENA = [
|
|
8
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
9
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
10
|
+
"Execute the task directly. Focus only on coding."
|
|
11
|
+
].join(" ");
|
|
12
|
+
|
|
13
|
+
const SERENA_INSTRUCTIONS = [
|
|
14
|
+
"## Serena MCP — symbol-level code navigation",
|
|
15
|
+
"You have access to Serena MCP tools for efficient code navigation.",
|
|
16
|
+
"Prefer these over reading entire files to save tokens:",
|
|
17
|
+
"- `find_symbol(name)` — locate a function, class, or variable definition",
|
|
18
|
+
"- `find_referencing_symbols(name)` — find all code that references a symbol",
|
|
19
|
+
"- `insert_after_symbol(name, code)` — insert code precisely after a symbol",
|
|
20
|
+
"Use Serena for understanding existing code structure before making changes.",
|
|
21
|
+
"Fall back to reading files only when Serena tools are not sufficient."
|
|
22
|
+
].join("\n");
|
|
23
|
+
|
|
24
|
+
export function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false }) {
|
|
25
|
+
const sections = [
|
|
26
|
+
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
27
|
+
`Task:\n${task}`,
|
|
28
|
+
"Implement directly in the repository.",
|
|
29
|
+
"Keep changes minimal and production-ready."
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
if (serenaEnabled) {
|
|
33
|
+
sections.push(SERENA_INSTRUCTIONS);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (coderRules) {
|
|
37
|
+
sections.push(`Coder rules (MUST follow):\n${coderRules}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (methodology === "tdd") {
|
|
41
|
+
sections.push(
|
|
42
|
+
[
|
|
43
|
+
"Default development policy: TDD.",
|
|
44
|
+
"1) Add or update failing tests first.",
|
|
45
|
+
"2) Implement minimal code to make tests pass.",
|
|
46
|
+
"3) Refactor safely while keeping tests green."
|
|
47
|
+
].join("\n")
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (sonarSummary) {
|
|
52
|
+
sections.push(`Sonar summary:\n${sonarSummary}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (reviewerFeedback) {
|
|
56
|
+
sections.push(`Reviewer blocking feedback:\n${reviewerFeedback}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return sections.join("\n\n");
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function buildPlannerPrompt({ task, context }) {
|
|
2
|
+
const parts = [
|
|
3
|
+
"You are an expert software architect. Create an implementation plan for the following task.",
|
|
4
|
+
"",
|
|
5
|
+
"## Task",
|
|
6
|
+
task,
|
|
7
|
+
""
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
if (context) {
|
|
11
|
+
parts.push("## Context", context, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
parts.push(
|
|
15
|
+
"## Output format",
|
|
16
|
+
"Respond with a JSON object containing:",
|
|
17
|
+
"- `approach`: A concise paragraph describing the overall strategy",
|
|
18
|
+
"- `steps`: An array of objects, each with `description` (what to do) and `commit` (conventional commit message). Each step should correspond to a single commit.",
|
|
19
|
+
"- `risks`: An array of strings describing potential risks or challenges",
|
|
20
|
+
"- `outOfScope`: An array of strings listing what is explicitly NOT included",
|
|
21
|
+
"",
|
|
22
|
+
"Respond ONLY with valid JSON, no markdown fences or extra text."
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return parts.join("\n");
|
|
26
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const SUBAGENT_PREAMBLE = [
|
|
2
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
3
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
4
|
+
"Do NOT use any MCP tools. Focus only on reviewing the code."
|
|
5
|
+
].join(" ");
|
|
6
|
+
|
|
7
|
+
const SUBAGENT_PREAMBLE_SERENA = [
|
|
8
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
9
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
10
|
+
"Focus only on reviewing the code."
|
|
11
|
+
].join(" ");
|
|
12
|
+
|
|
13
|
+
const SERENA_INSTRUCTIONS = [
|
|
14
|
+
"## Serena MCP — symbol-level code navigation",
|
|
15
|
+
"You have access to Serena MCP tools for efficient code review.",
|
|
16
|
+
"Use these to verify context without reading entire files:",
|
|
17
|
+
"- `find_symbol(name)` — locate a function, class, or variable definition",
|
|
18
|
+
"- `find_referencing_symbols(name)` — find all callers/references of a symbol",
|
|
19
|
+
"Use Serena to check how changed symbols are used across the codebase.",
|
|
20
|
+
"Fall back to reading files only when Serena tools are not sufficient."
|
|
21
|
+
].join("\n");
|
|
22
|
+
|
|
23
|
+
export function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false }) {
|
|
24
|
+
const truncatedDiff = diff.length > 12000 ? `${diff.slice(0, 12000)}\n\n[TRUNCATED]` : diff;
|
|
25
|
+
|
|
26
|
+
const sections = [
|
|
27
|
+
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
28
|
+
`You are a code reviewer in ${mode} mode.`,
|
|
29
|
+
"Return only one valid JSON object and nothing else.",
|
|
30
|
+
"JSON schema:",
|
|
31
|
+
'{"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}'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
if (serenaEnabled) {
|
|
35
|
+
sections.push(SERENA_INSTRUCTIONS);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
sections.push(
|
|
39
|
+
`Task context:\n${task}`,
|
|
40
|
+
`Review rules:\n${reviewRules}`,
|
|
41
|
+
`Git diff:\n${truncatedDiff}`
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return sections.join("\n\n");
|
|
45
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_THRESHOLD = 2;
|
|
4
|
+
|
|
5
|
+
function normalizeIssueKey(value) {
|
|
6
|
+
if (value === null || value === undefined) return "";
|
|
7
|
+
return String(value).trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function buildIssueSignature(issues, keySelector) {
|
|
11
|
+
const counts = new Map();
|
|
12
|
+
for (const issue of issues || []) {
|
|
13
|
+
const key = normalizeIssueKey(keySelector(issue));
|
|
14
|
+
if (!key) continue;
|
|
15
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
16
|
+
}
|
|
17
|
+
if (counts.size === 0) return "";
|
|
18
|
+
return Array.from(counts.entries())
|
|
19
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
20
|
+
.map(([key, count]) => `${key}:${count}`)
|
|
21
|
+
.join("|");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hashSignature(signature) {
|
|
25
|
+
if (!signature) return "";
|
|
26
|
+
return createHash("sha256").update(signature).digest("hex");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function updateRepeatState(state, signatureHash) {
|
|
30
|
+
if (!signatureHash) {
|
|
31
|
+
return { lastHash: null, repeatCount: 0 };
|
|
32
|
+
}
|
|
33
|
+
const nextCount = signatureHash === state.lastHash ? state.repeatCount + 1 : 1;
|
|
34
|
+
return { lastHash: signatureHash, repeatCount: nextCount };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseThreshold(value) {
|
|
38
|
+
const parsed = Number(value);
|
|
39
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
40
|
+
return DEFAULT_THRESHOLD;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class RepeatDetector {
|
|
44
|
+
constructor({ threshold = DEFAULT_THRESHOLD } = {}) {
|
|
45
|
+
this.threshold = parseThreshold(threshold);
|
|
46
|
+
this.sonar = { lastHash: null, repeatCount: 0 };
|
|
47
|
+
this.reviewer = { lastHash: null, repeatCount: 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
addIteration(sonarIssues, reviewerIssues) {
|
|
51
|
+
const sonarSignature = buildIssueSignature(
|
|
52
|
+
sonarIssues || [],
|
|
53
|
+
(issue) => issue?.rule || issue?.message
|
|
54
|
+
);
|
|
55
|
+
const reviewerSignature = buildIssueSignature(
|
|
56
|
+
reviewerIssues || [],
|
|
57
|
+
(issue) => issue?.description || issue?.content || issue?.id
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
this.sonar = updateRepeatState(this.sonar, hashSignature(sonarSignature));
|
|
61
|
+
this.reviewer = updateRepeatState(this.reviewer, hashSignature(reviewerSignature));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isStalled() {
|
|
65
|
+
if (this.sonar.repeatCount >= this.threshold) {
|
|
66
|
+
return { stalled: true, reason: "sonar_repeat" };
|
|
67
|
+
}
|
|
68
|
+
if (this.reviewer.repeatCount >= this.threshold) {
|
|
69
|
+
return { stalled: true, reason: "reviewer_repeat" };
|
|
70
|
+
}
|
|
71
|
+
return { stalled: false, reason: "" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getRepeatCounts() {
|
|
75
|
+
return { sonar: this.sonar.repeatCount, reviewer: this.reviewer.repeatCount };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { runCommand } from "../utils/process.js";
|
|
2
|
+
|
|
3
|
+
export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
|
|
4
|
+
if (baseRef) return baseRef;
|
|
5
|
+
const mergeBase = await runCommand("git", ["merge-base", "HEAD", `origin/${baseBranch}`]);
|
|
6
|
+
if (mergeBase.exitCode !== 0) {
|
|
7
|
+
const fallback = await runCommand("git", ["rev-parse", "HEAD~1"]);
|
|
8
|
+
if (fallback.exitCode !== 0) {
|
|
9
|
+
throw new Error("Could not compute diff base reference");
|
|
10
|
+
}
|
|
11
|
+
return fallback.stdout.trim();
|
|
12
|
+
}
|
|
13
|
+
return mergeBase.stdout.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function generateDiff({ baseRef }) {
|
|
17
|
+
const result = await runCommand("git", ["diff", `${baseRef}`]);
|
|
18
|
+
if (result.exitCode !== 0) {
|
|
19
|
+
throw new Error(`git diff failed: ${result.stderr || result.stdout}`);
|
|
20
|
+
}
|
|
21
|
+
return result.stdout;
|
|
22
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review output parsing helpers.
|
|
3
|
+
* Extracted from orchestrator.js to improve testability and reduce complexity.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function parseMaybeJsonString(value) {
|
|
7
|
+
if (typeof value !== "string") return null;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(value);
|
|
10
|
+
} catch {
|
|
11
|
+
const start = value.indexOf("{");
|
|
12
|
+
const end = value.lastIndexOf("}");
|
|
13
|
+
if (start >= 0 && end > start) {
|
|
14
|
+
const candidate = value.slice(start, end + 1);
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(candidate);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeReviewPayload(payload) {
|
|
26
|
+
if (!payload) return null;
|
|
27
|
+
|
|
28
|
+
if (payload.approved !== undefined && payload.blocking_issues !== undefined) {
|
|
29
|
+
return payload;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(payload)) {
|
|
33
|
+
for (let i = payload.length - 1; i >= 0; i -= 1) {
|
|
34
|
+
const item = payload[i];
|
|
35
|
+
if (item?.approved !== undefined && item?.blocking_issues !== undefined) {
|
|
36
|
+
return item;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const nested = item?.result || item?.message?.content?.[0]?.text;
|
|
40
|
+
if (typeof nested === "string") {
|
|
41
|
+
const parsedNested = parseMaybeJsonString(nested);
|
|
42
|
+
if (parsedNested?.approved !== undefined) return parsedNested;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof payload.result === "string") {
|
|
49
|
+
const parsedResult = parseMaybeJsonString(payload.result);
|
|
50
|
+
if (parsedResult?.approved !== undefined) return parsedResult;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseJsonOutput(raw) {
|
|
58
|
+
const cleaned = raw.trim();
|
|
59
|
+
if (!cleaned) return null;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(cleaned);
|
|
62
|
+
return normalizeReviewPayload(parsed);
|
|
63
|
+
} catch {
|
|
64
|
+
const lines = cleaned
|
|
65
|
+
.split("\n")
|
|
66
|
+
.map((x) => x.trim())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
|
|
69
|
+
const parsedLines = [];
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
try {
|
|
72
|
+
parsedLines.push(JSON.parse(line));
|
|
73
|
+
} catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const normalizedLines = normalizeReviewPayload(parsedLines);
|
|
79
|
+
if (normalizedLines) {
|
|
80
|
+
return normalizedLines;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(lines[i]);
|
|
86
|
+
return normalizeReviewPayload(parsed);
|
|
87
|
+
} catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review profile resolver.
|
|
3
|
+
* Loads mode-specific reviewer instructions with fallback chain:
|
|
4
|
+
* 1. Project-level / user-level / built-in reviewer-{mode}.md
|
|
5
|
+
* 2. Project-level / user-level / built-in reviewer.md
|
|
6
|
+
* 3. Hardcoded default
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { loadFirstExisting } from "../roles/base-role.js";
|
|
11
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
12
|
+
|
|
13
|
+
const KNOWN_MODES = ["paranoid", "strict", "standard", "relaxed"];
|
|
14
|
+
|
|
15
|
+
const DEFAULT_RULES = "Focus on critical issues: security vulnerabilities, logic errors, and broken tests.";
|
|
16
|
+
|
|
17
|
+
function buildCandidates(fileName, projectDir) {
|
|
18
|
+
const candidates = [];
|
|
19
|
+
|
|
20
|
+
if (projectDir) {
|
|
21
|
+
candidates.push(path.join(projectDir, ".karajan", "roles", fileName));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
candidates.push(path.join(getKarajanHome(), "roles", fileName));
|
|
25
|
+
|
|
26
|
+
const builtIn = path.resolve(
|
|
27
|
+
path.dirname(new URL(import.meta.url).pathname),
|
|
28
|
+
"..",
|
|
29
|
+
"..",
|
|
30
|
+
"templates",
|
|
31
|
+
"roles",
|
|
32
|
+
fileName
|
|
33
|
+
);
|
|
34
|
+
candidates.push(builtIn);
|
|
35
|
+
|
|
36
|
+
return candidates;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function resolveReviewProfile({ mode = "standard", projectDir } = {}) {
|
|
40
|
+
// Known modes: try mode-specific file first, then base reviewer.md
|
|
41
|
+
if (KNOWN_MODES.includes(mode)) {
|
|
42
|
+
const modePaths = buildCandidates(`reviewer-${mode}.md`, projectDir);
|
|
43
|
+
const modeRules = await loadFirstExisting(modePaths);
|
|
44
|
+
if (modeRules) {
|
|
45
|
+
return { mode, rules: modeRules };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fallback to base reviewer.md
|
|
49
|
+
const basePaths = buildCandidates("reviewer.md", projectDir);
|
|
50
|
+
const baseRules = await loadFirstExisting(basePaths);
|
|
51
|
+
if (baseRules) {
|
|
52
|
+
return { mode, rules: baseRules };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { mode, rules: DEFAULT_RULES };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Custom/unknown modes: only check base reviewer.md
|
|
59
|
+
const basePaths = buildCandidates("reviewer.md", projectDir);
|
|
60
|
+
const baseRules = await loadFirstExisting(basePaths);
|
|
61
|
+
if (baseRules) {
|
|
62
|
+
return { mode, rules: baseRules };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { mode, rules: DEFAULT_RULES };
|
|
66
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function validateReviewResult(reviewResult) {
|
|
2
|
+
if (!reviewResult || typeof reviewResult !== "object") {
|
|
3
|
+
throw new Error("Reviewer output must be a JSON object");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (typeof reviewResult.approved !== "boolean") {
|
|
7
|
+
throw new Error("Reviewer output missing boolean field: approved");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!Array.isArray(reviewResult.blocking_issues)) {
|
|
11
|
+
throw new Error("Reviewer output missing array field: blocking_issues");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!Array.isArray(reviewResult.non_blocking_suggestions)) {
|
|
15
|
+
reviewResult.non_blocking_suggestions = [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof reviewResult.summary !== "string") {
|
|
19
|
+
reviewResult.summary = "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof reviewResult.confidence !== "number") {
|
|
23
|
+
reviewResult.confidence = 0.5;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (reviewResult.approved && reviewResult.blocking_issues.length > 0) {
|
|
27
|
+
throw new Error("Invalid reviewer output: approved=true with blocking issues");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return reviewResult;
|
|
31
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function extractChangedFiles(diff) {
|
|
2
|
+
const files = new Set();
|
|
3
|
+
const lines = String(diff || "").split("\n");
|
|
4
|
+
for (const line of lines) {
|
|
5
|
+
if (!line.startsWith("diff --git ")) continue;
|
|
6
|
+
const parts = line.split(" ");
|
|
7
|
+
const b = parts[3] || "";
|
|
8
|
+
if (b.startsWith("b/")) files.add(b.slice(2));
|
|
9
|
+
}
|
|
10
|
+
return [...files];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isTestFile(file, patterns = []) {
|
|
14
|
+
const normalized = `/${file}`;
|
|
15
|
+
return patterns.some((pattern) => normalized.includes(pattern));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isSourceFile(file, extensions = []) {
|
|
19
|
+
return extensions.some((ext) => file.endsWith(ext));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function evaluateTddPolicy(diff, developmentConfig = {}) {
|
|
23
|
+
const requireTestChanges = developmentConfig.require_test_changes !== false;
|
|
24
|
+
const patterns = developmentConfig.test_file_patterns || ["/tests/", "/__tests__/", ".test.", ".spec."];
|
|
25
|
+
const extensions =
|
|
26
|
+
developmentConfig.source_file_extensions || [".js", ".jsx", ".ts", ".tsx", ".py", ".go", ".java", ".rb", ".php", ".cs"];
|
|
27
|
+
|
|
28
|
+
const files = extractChangedFiles(diff);
|
|
29
|
+
const sourceFiles = files.filter((f) => isSourceFile(f, extensions) && !isTestFile(f, patterns));
|
|
30
|
+
const testFiles = files.filter((f) => isTestFile(f, patterns));
|
|
31
|
+
|
|
32
|
+
if (!requireTestChanges) {
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
reason: "test_changes_not_required",
|
|
36
|
+
sourceFiles,
|
|
37
|
+
testFiles
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (sourceFiles.length === 0) {
|
|
42
|
+
return { ok: true, reason: "no_source_changes", sourceFiles, testFiles };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (testFiles.length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
reason: "source_changes_without_tests",
|
|
49
|
+
sourceFiles,
|
|
50
|
+
testFiles,
|
|
51
|
+
message:
|
|
52
|
+
"TDD policy violation: source code changed without test changes. Add/adjust tests first, then implementation."
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { ok: true, reason: "tests_present", sourceFiles, testFiles };
|
|
57
|
+
}
|