karajan-code 1.59.1 → 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.
- package/README.md +2 -0
- package/package.json +1 -1
- package/scripts/install.js +18 -15
- package/src/agents/aider-agent.js +0 -7
- package/src/agents/claude-agent.js +1 -4
- package/src/agents/codex-agent.js +2 -6
- package/src/agents/gemini-agent.js +0 -4
- package/src/agents/opencode-agent.js +0 -4
- package/src/{becaria → ci}/dispatch.js +13 -13
- package/src/{becaria → ci}/pr-diff.js +1 -1
- package/src/cli.js +2 -2
- package/src/commands/agents.js +0 -1
- package/src/commands/doctor.js +22 -40
- package/src/commands/init.js +8 -8
- package/src/commands/report.js +0 -13
- package/src/commands/review.js +17 -17
- package/src/commands/run.js +1 -2
- package/src/commands/status.js +1 -7
- package/src/config.js +10 -52
- package/src/git/automation.js +9 -9
- package/src/guards/output-guard.js +0 -17
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +1 -2
- package/src/mcp/sovereignty-guard.js +1 -1
- package/src/mcp/tools.js +1 -1
- package/src/orchestrator/brain-coordinator.js +165 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +435 -0
- package/src/orchestrator/direct-actions.js +186 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +152 -0
- package/src/orchestrator/flow-control.js +159 -0
- package/src/orchestrator/post-loop-stages.js +20 -15
- package/src/orchestrator/preflight-checks.js +7 -3
- package/src/orchestrator/role-output-compressor.js +190 -0
- package/src/orchestrator/session-journal.js +344 -0
- package/src/orchestrator/solomon-escalation.js +38 -5
- package/src/orchestrator/solomon-rules.js +32 -33
- package/src/orchestrator/stages/reviewer-stage.js +16 -5
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +428 -664
- package/src/planning-game/client.js +2 -2
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/coder.js +17 -3
- package/src/prompts/reviewer.js +2 -2
- package/src/prompts/rtk-snippet.js +3 -5
- package/src/review/diff-generator.js +66 -17
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/tdd-policy.js +4 -2
- package/src/roles/agent-role.js +135 -0
- package/src/roles/architect-role.js +61 -108
- package/src/roles/audit-role.js +25 -71
- package/src/roles/coder-role.js +32 -57
- package/src/roles/discover-role.js +30 -71
- package/src/roles/hu-reviewer-role.js +51 -99
- package/src/roles/impeccable-role.js +33 -88
- package/src/roles/index.js +2 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/planner-role.js +40 -75
- package/src/roles/refactorer-role.js +22 -64
- package/src/roles/researcher-role.js +51 -115
- package/src/roles/reviewer-role.js +68 -125
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +69 -11
- package/src/roles/tester-role.js +121 -95
- package/src/roles/triage-role.js +50 -111
- package/src/session-store.js +1 -1
- package/src/skills/skill-detector.js +11 -0
- package/src/sonar/config-resolver.js +20 -1
- package/src/sonar/credentials.js +17 -0
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +1 -1
- package/src/utils/display.js +33 -14
- package/src/utils/injection-guard.js +23 -8
- package/src/utils/logger.js +6 -1
- package/src/utils/process.js +3 -4
- package/src/utils/role-assigner.js +131 -0
- package/src/utils/update-check.js +5 -1
- package/src/webperf/devtools-detect.js +70 -23
- package/templates/roles/coder.md +7 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/solomon.md +67 -106
- package/templates/workflows/automerge.yml +4 -4
- package/templates/workflows/houston-override.yml +4 -4
- package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
- package/src/proxy/adapters/anthropic.js +0 -131
- package/src/proxy/adapters/gemini.js +0 -106
- package/src/proxy/adapters/index.js +0 -51
- package/src/proxy/adapters/openai.js +0 -71
- package/src/proxy/compression/ai-compressor.js +0 -209
- package/src/proxy/compression/dedup-cache.js +0 -100
- package/src/proxy/compression/deterministic/bash-build.js +0 -60
- package/src/proxy/compression/deterministic/bash-git.js +0 -101
- package/src/proxy/compression/deterministic/bash-infra.js +0 -64
- package/src/proxy/compression/deterministic/bash-misc.js +0 -53
- package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
- package/src/proxy/compression/deterministic/bash-test.js +0 -61
- package/src/proxy/compression/deterministic/glob.js +0 -26
- package/src/proxy/compression/deterministic/grep.js +0 -53
- package/src/proxy/compression/deterministic/index.js +0 -58
- package/src/proxy/compression/deterministic/read.js +0 -22
- package/src/proxy/compression/deterministic/utils.js +0 -33
- package/src/proxy/compression/pipeline.js +0 -270
- package/src/proxy/middleware/prompt-injector.js +0 -165
- package/src/proxy/middleware/response-interceptor.js +0 -223
- package/src/proxy/progress-monitor.js +0 -98
- package/src/proxy/proxy-lifecycle.js +0 -201
- package/src/proxy/proxy-server.js +0 -239
- package/src/proxy/security.js +0 -45
- /package/src/{becaria → ci}/index.js +0 -0
- /package/src/{becaria → ci}/repo.js +0 -0
|
@@ -36,27 +36,51 @@ function buildPrompt({ conflict, task, instructions }) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
sections.push(
|
|
39
|
-
"You are Solomon, the
|
|
40
|
-
"You are activated
|
|
41
|
-
"
|
|
39
|
+
"You are Solomon, the autonomous pipeline boss in a multi-agent AI coding orchestrator.",
|
|
40
|
+
"You are activated whenever ANYTHING deviates from normal flow: rate limits, agent failures, conflicts, max iterations, quality gate issues.",
|
|
41
|
+
"",
|
|
42
|
+
"## Core principle",
|
|
43
|
+
"The pipeline NEVER skips a quality stage (review, tests, sonar, security) without exhausting alternatives first.",
|
|
44
|
+
"NEVER approve skipping a stage as your first option. Follow the recovery hierarchy below.",
|
|
45
|
+
"",
|
|
46
|
+
"## ABSOLUTE RULE: Security issues from reviewer are NEVER overridable",
|
|
47
|
+
"If the reviewer's blocking_issues contain security-related issues (categories: authentication, authorization, injection, XSS, CSRF, exposed secrets/credentials, cryptography, session management, input validation), you MUST:",
|
|
48
|
+
"- Set ruling to \"continue\" (NOT \"approve\")",
|
|
49
|
+
"- Return the reviewer feedback to the coder for fixing",
|
|
50
|
+
"- NEVER approve or skip security issues regardless of task complexity or deadline",
|
|
51
|
+
"This rule overrides all other rules including the recovery hierarchy.",
|
|
52
|
+
"",
|
|
53
|
+
"## Recovery hierarchy (STRICT ORDER — follow top to bottom)",
|
|
54
|
+
"When a stage fails or an agent is unavailable:",
|
|
55
|
+
"1. **wait** — If there is a known cooldown (cooldownUntil), and it is less than 10 minutes, WAIT. Set ruling to 'approve_with_conditions' with condition 'wait for cooldown' and set waitUntil.",
|
|
56
|
+
"2. **retry_with_alternative** — Try the same stage with a DIFFERENT agent. The pipeline has multiple agents (claude, codex, gemini). Set ruling to 'approve_with_conditions' with condition specifying the alternative agent.",
|
|
57
|
+
"3. **evaluate_risk** — Only if no alternatives are available, evaluate the risk of skipping. Consider: task complexity, what the stage would catch, security implications.",
|
|
58
|
+
" - If risk is LOW (trivial task, no security impact, style-only review): approve with a clear justification.",
|
|
59
|
+
" - If risk is HIGH (complex task, security changes, new dependencies): escalate_human.",
|
|
60
|
+
"4. **escalate_human** — When you cannot resolve it safely. Always prefer this over blindly approving.",
|
|
61
|
+
"",
|
|
62
|
+
"## Quality hierarchy",
|
|
42
63
|
"Security > Correctness > Tests > Architecture > Maintainability > Style",
|
|
43
64
|
"- Green tests are sacred. Never dismiss a failing test.",
|
|
44
65
|
"- Style preferences NEVER block approval.",
|
|
45
|
-
"- Hardcoded values that will come from DB later are acceptable (contextual false positive).",
|
|
46
66
|
"- Sonar INFO/MINOR issues are always dismissable.",
|
|
47
67
|
"- Sonar BLOCKER/CRITICAL must be fixed unless they are proven false positives.",
|
|
68
|
+
"",
|
|
48
69
|
"## Classification rules",
|
|
49
70
|
"For each issue, classify as:",
|
|
50
71
|
"1. **critical** (security, correctness, tests broken) — action: must_fix",
|
|
51
72
|
"2. **important** (architecture, maintainability) — action: should_fix",
|
|
52
73
|
"3. **style** (naming, formatting, preferences, false positives) — action: dismiss",
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
74
|
+
"",
|
|
75
|
+
"## Ruling options",
|
|
76
|
+
'1. **approve** — FORBIDDEN when a quality stage (review, tests, sonar, security) was skipped or failed. You MUST try alternatives first (wait for cooldown, use different agent). Only allowed when all stages completed successfully and remaining issues are style/false positives.',
|
|
77
|
+
'2. **approve_with_conditions** — Fixable issues or recovery actions needed. Include exact conditions (wait, retry with alternative agent, specific fix instructions). Set extraIterations and/or alternativeAgent.',
|
|
78
|
+
'3. **escalate_human** — Cannot resolve safely. Critical issues, ambiguous requirements, architecture decisions, high-risk skip.',
|
|
79
|
+
'4. **create_subtask** — A prerequisite task must be completed first.',
|
|
80
|
+
"",
|
|
58
81
|
"Return a single valid JSON object with your ruling and nothing else.",
|
|
59
|
-
'JSON schema: {"ruling":"approve"|"approve_with_conditions"|"escalate_human"|"create_subtask","classification":[{"issue":string,"category":"critical"|"important"|"style","action":"must_fix"|"should_fix"|"dismiss"}],"conditions":[string],"dismissed":[string],"escalate":boolean,"escalate_reason":string|null,"subtask":{"title":string,"description":string,"reason":string}|null}'
|
|
82
|
+
'JSON schema: {"ruling":"approve"|"approve_with_conditions"|"escalate_human"|"create_subtask","classification":[{"issue":string,"category":"critical"|"important"|"style","action":"must_fix"|"should_fix"|"dismiss"}],"conditions":[string],"dismissed":[string],"escalate":boolean,"escalate_reason":string|null,"extraIterations":number|null,"alternativeAgent":string|null,"waitUntil":string|null,"subtask":{"title":string,"description":string,"reason":string}|null}',
|
|
83
|
+
"When ruling is approve_with_conditions, set extraIterations (1-5) and/or alternativeAgent (claude|codex|gemini) as appropriate."
|
|
60
84
|
);
|
|
61
85
|
|
|
62
86
|
const stage = conflict?.stage || "unknown";
|
|
@@ -66,14 +90,48 @@ function buildPrompt({ conflict, task, instructions }) {
|
|
|
66
90
|
const isFirstRejection = conflict?.isFirstRejection ?? false;
|
|
67
91
|
const isRepeat = conflict?.isRepeat ?? false;
|
|
68
92
|
|
|
93
|
+
const budgetUsd = conflict?.budget_usd;
|
|
94
|
+
const budgetLine = typeof budgetUsd === "number" ? `Budget spent so far: $${budgetUsd.toFixed(2)}` : "";
|
|
95
|
+
|
|
69
96
|
sections.push(
|
|
70
97
|
`## Conflict context`,
|
|
71
98
|
`Stage: ${stage}`,
|
|
72
99
|
`Iterations exhausted: ${iterationCount}/${maxIterations}`,
|
|
73
100
|
`isFirstRejection: ${isFirstRejection}`,
|
|
74
|
-
`isRepeat: ${isRepeat}
|
|
101
|
+
`isRepeat: ${isRepeat}`,
|
|
102
|
+
budgetLine,
|
|
103
|
+
"",
|
|
104
|
+
"## Budget awareness",
|
|
105
|
+
"Consider the cost-benefit ratio when deciding. If significant budget has been spent with little progress,",
|
|
106
|
+
"prefer approving the current work or escalating to human. If progress is good and budget is low,",
|
|
107
|
+
"prefer continuing with conditions. Never waste budget on style-only iterations."
|
|
75
108
|
);
|
|
76
109
|
|
|
110
|
+
if (conflict?.previousSolomonRulings?.length > 0) {
|
|
111
|
+
const rulingLines = conflict.previousSolomonRulings.map((r, idx) =>
|
|
112
|
+
`${idx + 1}. Stage: ${r.conflictStage}, Ruling: ${r.ruling}${r.alternativeAgent ? `, agent: ${r.alternativeAgent}` : ""}${r.error ? ` (FAILED: ${r.error})` : ""}`
|
|
113
|
+
).join("\n");
|
|
114
|
+
sections.push(
|
|
115
|
+
"## Your previous rulings in this session (DO NOT repeat failed strategies)",
|
|
116
|
+
rulingLines,
|
|
117
|
+
"If a previous ruling failed (e.g., alternative agent also rate-limited), do NOT suggest the same strategy again. Escalate or try a different approach."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Rate limit specific instruction
|
|
122
|
+
if (stage?.includes("rate_limit")) {
|
|
123
|
+
const rateLimitedAgent = conflict?.history?.[0]?.feedback?.match(/Agent "(\w+)"/)?.[1] || "unknown";
|
|
124
|
+
sections.push(
|
|
125
|
+
"## CRITICAL: Agent rate-limited",
|
|
126
|
+
`Agent "${rateLimitedAgent}" is rate-limited. You MUST NOT approve skipping this stage.`,
|
|
127
|
+
"Your ONLY valid options are:",
|
|
128
|
+
`1. approve_with_conditions + alternativeAgent: pick a different agent (available: claude, codex, gemini — NOT "${rateLimitedAgent}")`,
|
|
129
|
+
"2. approve_with_conditions + waitUntil: if cooldown is known and <10min",
|
|
130
|
+
"3. escalate_human: if no alternatives work",
|
|
131
|
+
`DO NOT set ruling to "approve". The stage MUST be completed by some agent.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
77
135
|
if (conflict?.issueCategories) {
|
|
78
136
|
sections.push(`## Issue categories\n${JSON.stringify(conflict.issueCategories, null, 2)}`);
|
|
79
137
|
}
|
package/src/roles/tester-role.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
3
|
+
import { detectTestFramework } from "../utils/project-detect.js";
|
|
4
4
|
|
|
5
5
|
const SUBAGENT_PREAMBLE = [
|
|
6
6
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -8,108 +8,134 @@ const SUBAGENT_PREAMBLE = [
|
|
|
8
8
|
"Do NOT use any MCP tools. Focus only on evaluating test quality."
|
|
9
9
|
].join(" ");
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Map test framework to the command that runs tests with coverage.
|
|
13
|
+
*/
|
|
14
|
+
const COVERAGE_COMMANDS = {
|
|
15
|
+
vitest: "npx vitest run --coverage --reporter=verbose 2>&1",
|
|
16
|
+
jest: "npx jest --coverage --verbose 2>&1",
|
|
17
|
+
mocha: "npx c8 mocha 2>&1",
|
|
18
|
+
playwright: "npx playwright test 2>&1",
|
|
19
|
+
pytest: "pytest --cov --tb=short 2>&1",
|
|
20
|
+
"go-test": "go test -cover -v ./... 2>&1",
|
|
21
|
+
"cargo-test": "cargo test --verbose 2>&1",
|
|
22
|
+
junit: "mvn test 2>&1",
|
|
23
|
+
rspec: "bundle exec rspec 2>&1",
|
|
24
|
+
phpunit: "vendor/bin/phpunit --coverage-text 2>&1",
|
|
25
|
+
"dotnet-test": "dotnet test --collect:\"XPlat Code Coverage\" 2>&1",
|
|
26
|
+
"dart-test": "dart test 2>&1"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const TEST_COMMANDS = {
|
|
30
|
+
vitest: "npx vitest run --reporter=verbose 2>&1",
|
|
31
|
+
jest: "npx jest --verbose 2>&1",
|
|
32
|
+
mocha: "npx mocha 2>&1",
|
|
33
|
+
playwright: "npx playwright test 2>&1",
|
|
34
|
+
pytest: "pytest --tb=short 2>&1",
|
|
35
|
+
"go-test": "go test -v ./... 2>&1",
|
|
36
|
+
"cargo-test": "cargo test --verbose 2>&1",
|
|
37
|
+
junit: "mvn test 2>&1",
|
|
38
|
+
rspec: "bundle exec rspec 2>&1",
|
|
39
|
+
phpunit: "vendor/bin/phpunit 2>&1",
|
|
40
|
+
"dotnet-test": "dotnet test 2>&1",
|
|
41
|
+
"dart-test": "dart test 2>&1"
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export class TesterRole extends AgentRole {
|
|
45
|
+
constructor(opts) {
|
|
46
|
+
super({ ...opts, name: "tester" });
|
|
24
47
|
}
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (diff) {
|
|
35
|
-
sections.push(`## Git diff\n${diff}`);
|
|
49
|
+
extractInput(input) {
|
|
50
|
+
if (typeof input === "string") return { task: input, diff: null, sonarIssues: null };
|
|
51
|
+
return {
|
|
52
|
+
task: input?.task || this.context?.task || "",
|
|
53
|
+
diff: input?.diff || null,
|
|
54
|
+
sonarIssues: input?.sonarIssues || null,
|
|
55
|
+
onOutput: input?.onOutput || null
|
|
56
|
+
};
|
|
36
57
|
}
|
|
37
58
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
async buildPrompt({ task, diff, sonarIssues }) {
|
|
60
|
+
const projectDir = this.config?.projectDir || process.cwd();
|
|
61
|
+
const detection = await detectTestFramework(projectDir);
|
|
62
|
+
|
|
63
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
64
|
+
if (this.instructions) sections.push(this.instructions);
|
|
65
|
+
|
|
66
|
+
sections.push(
|
|
67
|
+
"You are a test quality gate. Your job is to EXECUTE the test suite, measure REAL coverage, and evaluate quality.",
|
|
68
|
+
"You MUST run the actual test command — do NOT guess or estimate results."
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (detection.hasTests && detection.framework) {
|
|
72
|
+
const coverageCmd = COVERAGE_COMMANDS[detection.framework];
|
|
73
|
+
const testCmd = TEST_COMMANDS[detection.framework];
|
|
74
|
+
sections.push(
|
|
75
|
+
`## Detected test framework: ${detection.framework} (${detection.language})`,
|
|
76
|
+
"**Step 0**: If node_modules/ does not exist, run `npm install` (or `pnpm install`) first.",
|
|
77
|
+
`**Step 1**: Run the test suite with coverage:`,
|
|
78
|
+
"```bash",
|
|
79
|
+
coverageCmd || testCmd,
|
|
80
|
+
"```",
|
|
81
|
+
"If the coverage command fails (missing dependency), fall back to:",
|
|
82
|
+
"```bash",
|
|
83
|
+
testCmd,
|
|
84
|
+
"```",
|
|
85
|
+
"**Step 2**: Parse the output to extract: pass/fail count, coverage percentages, any failures.",
|
|
86
|
+
"**Step 3**: Return a single JSON object with REAL numbers from the test output."
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
sections.push(
|
|
90
|
+
"## No test framework detected",
|
|
91
|
+
"**Step 0**: If package.json exists but node_modules/ does not, run `npm install` first.",
|
|
92
|
+
"**Step 1**: Look at package.json or project files to find test scripts.",
|
|
93
|
+
"**Step 2**: Try running `npm test` or detect the framework from config files.",
|
|
94
|
+
"**Step 3**: If tests exist, run them. If no tests exist, report tests_pass: false with verdict: 'fail'.",
|
|
95
|
+
"**Step 4**: Return a JSON object with your findings."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
48
98
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
99
|
+
sections.push(
|
|
100
|
+
"",
|
|
101
|
+
"Return ONLY a single valid JSON object:",
|
|
102
|
+
'{"tests_pass":boolean,"coverage":{"overall":number,"services":number,"utilities":number},"missing_scenarios":[string],"quality_issues":[string],"verdict":"pass"|"fail"}',
|
|
103
|
+
"",
|
|
104
|
+
"- coverage.overall MUST be a real number from the test runner output, NOT an estimate",
|
|
105
|
+
"- If coverage tool is not available, set coverage.overall to null (not 0, not a guess)",
|
|
106
|
+
"- tests_pass must reflect whether the actual test run succeeded",
|
|
107
|
+
`## Task\n${task}`
|
|
108
|
+
);
|
|
109
|
+
if (diff) sections.push(`## Git diff\n${diff}`);
|
|
110
|
+
if (sonarIssues) sections.push(`## Sonar test issues\n${sonarIssues}`);
|
|
111
|
+
return { prompt: sections.join("\n\n") };
|
|
53
112
|
}
|
|
54
113
|
|
|
55
|
-
|
|
56
|
-
const { task, diff, sonarIssues } = typeof input === "string"
|
|
57
|
-
? { task: input, diff: null, sonarIssues: null }
|
|
58
|
-
: { task: input?.task || this.context?.task || "", diff: input?.diff || null, sonarIssues: input?.sonarIssues || null };
|
|
59
|
-
|
|
60
|
-
const provider = resolveProvider(this.config);
|
|
61
|
-
const agent = this._createAgent(provider, this.config, this.logger);
|
|
62
|
-
|
|
63
|
-
const prompt = buildPrompt({ task, diff, sonarIssues, instructions: this.instructions });
|
|
64
|
-
const result = await agent.runTask({ prompt, role: "tester" });
|
|
65
|
-
|
|
66
|
-
if (!result.ok) {
|
|
67
|
-
return {
|
|
68
|
-
ok: false,
|
|
69
|
-
result: {
|
|
70
|
-
error: result.error || result.output || "Tester failed",
|
|
71
|
-
provider
|
|
72
|
-
},
|
|
73
|
-
summary: `Tester failed: ${result.error || "unknown error"}`
|
|
74
|
-
};
|
|
75
|
-
}
|
|
114
|
+
parseOutput(raw) { return extractFirstJson(raw); }
|
|
76
115
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
ok: false,
|
|
82
|
-
result: { error: "Failed to parse tester output: no JSON found", provider },
|
|
83
|
-
summary: "Tester output parse error: no JSON found"
|
|
84
|
-
};
|
|
85
|
-
}
|
|
116
|
+
isSuccessful(parsed) {
|
|
117
|
+
const verdict = parsed.verdict || (parsed.tests_pass ? "pass" : "fail");
|
|
118
|
+
return verdict === "pass";
|
|
119
|
+
}
|
|
86
120
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
buildSuccessResult(parsed, provider) {
|
|
122
|
+
const verdict = parsed.verdict || (parsed.tests_pass ? "pass" : "fail");
|
|
123
|
+
return {
|
|
124
|
+
tests_pass: Boolean(parsed.tests_pass),
|
|
125
|
+
coverage: parsed.coverage || {},
|
|
126
|
+
missing_scenarios: parsed.missing_scenarios || [],
|
|
127
|
+
quality_issues: parsed.quality_issues || [],
|
|
128
|
+
verdict,
|
|
129
|
+
provider
|
|
130
|
+
};
|
|
131
|
+
}
|
|
90
132
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
verdict,
|
|
99
|
-
provider
|
|
100
|
-
},
|
|
101
|
-
summary: (() => {
|
|
102
|
-
const missingPart = parsed.missing_scenarios?.length ? `; ${parsed.missing_scenarios.length} missing scenario(s)` : "";
|
|
103
|
-
const qualityPart = parsed.quality_issues?.length ? `; ${parsed.quality_issues.length} quality issue(s)` : "";
|
|
104
|
-
return `Verdict: ${verdict}; Coverage: ${coverage.overall ?? "?"}%${missingPart}${qualityPart}`;
|
|
105
|
-
})()
|
|
106
|
-
};
|
|
107
|
-
} catch (err) {
|
|
108
|
-
return {
|
|
109
|
-
ok: false,
|
|
110
|
-
result: { error: `Failed to parse tester output: ${err.message}`, provider },
|
|
111
|
-
summary: `Tester output parse error: ${err.message}`
|
|
112
|
-
};
|
|
113
|
-
}
|
|
133
|
+
buildSummary(parsed) {
|
|
134
|
+
const verdict = parsed.verdict || (parsed.tests_pass ? "pass" : "fail");
|
|
135
|
+
const coverage = parsed.coverage || {};
|
|
136
|
+
const coverageStr = coverage.overall != null ? `${coverage.overall}%` : "not measured";
|
|
137
|
+
const missingPart = parsed.missing_scenarios?.length ? `; ${parsed.missing_scenarios.length} missing scenario(s)` : "";
|
|
138
|
+
const qualityPart = parsed.quality_issues?.length ? `; ${parsed.quality_issues.length} quality issue(s)` : "";
|
|
139
|
+
return `Verdict: ${verdict}; Coverage: ${coverageStr}${missingPart}${qualityPart}`;
|
|
114
140
|
}
|
|
115
141
|
}
|
package/src/roles/triage-role.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
1
|
+
import { AgentRole } from "./agent-role.js";
|
|
3
2
|
import { buildTriagePrompt } from "../prompts/triage.js";
|
|
4
3
|
import { VALID_TASK_TYPES } from "../guards/policy-resolver.js";
|
|
5
4
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
@@ -7,18 +6,7 @@ import { extractFirstJson } from "../utils/json-extract.js";
|
|
|
7
6
|
const VALID_LEVELS = new Set(["trivial", "simple", "medium", "complex"]);
|
|
8
7
|
const VALID_ROLES = new Set(["planner", "researcher", "refactorer", "reviewer", "tester", "security", "impeccable"]);
|
|
9
8
|
const FALLBACK_TASK_TYPE = "sw";
|
|
10
|
-
|
|
11
|
-
function resolveProvider(config) {
|
|
12
|
-
return (
|
|
13
|
-
config?.roles?.triage?.provider ||
|
|
14
|
-
config?.roles?.coder?.provider ||
|
|
15
|
-
"claude"
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function parseTriageOutput(raw) {
|
|
20
|
-
return extractFirstJson(raw);
|
|
21
|
-
}
|
|
9
|
+
const FALLBACK_RESULT = { level: "medium", roles: ["reviewer"], taskType: FALLBACK_TASK_TYPE, shouldDecompose: false };
|
|
22
10
|
|
|
23
11
|
function normalizeRoles(roles) {
|
|
24
12
|
if (!Array.isArray(roles)) return [];
|
|
@@ -27,115 +15,66 @@ function normalizeRoles(roles) {
|
|
|
27
15
|
|
|
28
16
|
function normalizeSubtasks(subtasks) {
|
|
29
17
|
if (!Array.isArray(subtasks)) return [];
|
|
30
|
-
return subtasks
|
|
31
|
-
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
|
32
|
-
.filter(Boolean)
|
|
33
|
-
.slice(0, 5);
|
|
18
|
+
return subtasks.map((s) => (typeof s === "string" ? s.trim() : "")).filter(Boolean).slice(0, 5);
|
|
34
19
|
}
|
|
35
20
|
|
|
36
21
|
function normalizeDomainHints(hints) {
|
|
37
22
|
if (!Array.isArray(hints)) return [];
|
|
38
|
-
return hints
|
|
39
|
-
.map((h) => (typeof h === "string" ? h.trim().toLowerCase() : ""))
|
|
40
|
-
.filter(Boolean);
|
|
23
|
+
return hints.map((h) => (typeof h === "string" ? h.trim().toLowerCase() : "")).filter(Boolean);
|
|
41
24
|
}
|
|
42
25
|
|
|
43
|
-
export class TriageRole extends
|
|
44
|
-
constructor(
|
|
45
|
-
super({ name: "triage"
|
|
46
|
-
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
26
|
+
export class TriageRole extends AgentRole {
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
super({ ...opts, name: "triage" });
|
|
47
29
|
}
|
|
48
30
|
|
|
49
|
-
async
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
: input?.task || this.context?.task || "";
|
|
53
|
-
const onOutput = typeof input === "string" ? null : input?.onOutput || null;
|
|
54
|
-
|
|
55
|
-
const provider = resolveProvider(this.config);
|
|
56
|
-
const agent = this._createAgent(provider, this.config, this.logger);
|
|
57
|
-
|
|
58
|
-
const prompt = buildTriagePrompt({ task, instructions: this.instructions });
|
|
59
|
-
const runArgs = { prompt, role: "triage" };
|
|
60
|
-
if (onOutput) runArgs.onOutput = onOutput;
|
|
61
|
-
const result = await agent.runTask(runArgs);
|
|
31
|
+
async buildPrompt({ task }) {
|
|
32
|
+
return { prompt: buildTriagePrompt({ task, instructions: this.instructions }) };
|
|
33
|
+
}
|
|
62
34
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
35
|
+
parseOutput(raw) { return extractFirstJson(raw); }
|
|
36
|
+
|
|
37
|
+
buildSuccessResult(parsed, provider) {
|
|
38
|
+
const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
|
|
39
|
+
const roles = normalizeRoles(parsed.roles);
|
|
40
|
+
const reasoning = String(parsed.reasoning || "").trim() || "No reasoning provided.";
|
|
41
|
+
const shouldDecompose = Boolean(parsed.shouldDecompose);
|
|
42
|
+
const subtasks = normalizeSubtasks(parsed.subtasks);
|
|
43
|
+
const taskType = VALID_TASK_TYPES.has(parsed.taskType) ? parsed.taskType : FALLBACK_TASK_TYPE;
|
|
44
|
+
const domainHints = normalizeDomainHints(parsed.domainHints);
|
|
45
|
+
|
|
46
|
+
const result = { level, roles, reasoning, taskType, domainHints, provider };
|
|
47
|
+
if (shouldDecompose && subtasks.length > 0) {
|
|
48
|
+
result.shouldDecompose = true;
|
|
49
|
+
result.subtasks = subtasks;
|
|
50
|
+
} else {
|
|
51
|
+
result.shouldDecompose = false;
|
|
73
52
|
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
74
55
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
level: "medium",
|
|
82
|
-
roles: ["reviewer"],
|
|
83
|
-
reasoning: "Unstructured output, using safe defaults.",
|
|
84
|
-
taskType: FALLBACK_TASK_TYPE,
|
|
85
|
-
provider,
|
|
86
|
-
raw: result.output
|
|
87
|
-
},
|
|
88
|
-
summary: "Triage complete (fallback defaults)",
|
|
89
|
-
usage: result.usage
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
|
|
94
|
-
const roles = normalizeRoles(parsed.roles);
|
|
95
|
-
const reasoning = String(parsed.reasoning || "").trim() || "No reasoning provided.";
|
|
96
|
-
const shouldDecompose = Boolean(parsed.shouldDecompose);
|
|
97
|
-
const subtasks = normalizeSubtasks(parsed.subtasks);
|
|
98
|
-
const taskType = VALID_TASK_TYPES.has(parsed.taskType) ? parsed.taskType : FALLBACK_TASK_TYPE;
|
|
99
|
-
|
|
100
|
-
const domainHints = normalizeDomainHints(parsed.domainHints);
|
|
101
|
-
|
|
102
|
-
const triageResult = {
|
|
103
|
-
level,
|
|
104
|
-
roles,
|
|
105
|
-
reasoning,
|
|
106
|
-
taskType,
|
|
107
|
-
domainHints,
|
|
108
|
-
provider
|
|
109
|
-
};
|
|
56
|
+
buildSummary(parsed) {
|
|
57
|
+
const level = VALID_LEVELS.has(parsed.level) ? parsed.level : "medium";
|
|
58
|
+
const roles = normalizeRoles(parsed.roles);
|
|
59
|
+
const decomposeNote = parsed.shouldDecompose ? " — decomposition recommended" : "";
|
|
60
|
+
return `Triage: ${level} (${roles.length} role${roles.length === 1 ? "" : "s"})${decomposeNote}`;
|
|
61
|
+
}
|
|
110
62
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
63
|
+
handleParseNull(agentResult, provider) {
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
result: { ...FALLBACK_RESULT, reasoning: "Unstructured output, using safe defaults.", provider, raw: agentResult.output },
|
|
67
|
+
summary: "Triage complete (fallback defaults)",
|
|
68
|
+
usage: agentResult.usage
|
|
69
|
+
};
|
|
70
|
+
}
|
|
117
71
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} catch { /* agent output is not structured JSON */
|
|
126
|
-
return {
|
|
127
|
-
ok: true,
|
|
128
|
-
result: {
|
|
129
|
-
level: "medium",
|
|
130
|
-
roles: ["reviewer"],
|
|
131
|
-
reasoning: "Failed to parse triage output, using safe defaults.",
|
|
132
|
-
taskType: FALLBACK_TASK_TYPE,
|
|
133
|
-
provider,
|
|
134
|
-
raw: result.output
|
|
135
|
-
},
|
|
136
|
-
summary: "Triage complete (fallback defaults)",
|
|
137
|
-
usage: result.usage
|
|
138
|
-
};
|
|
139
|
-
}
|
|
72
|
+
handleParseError(_err, agentResult, provider) {
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
result: { ...FALLBACK_RESULT, reasoning: "Failed to parse triage output, using safe defaults.", provider, raw: agentResult.output },
|
|
76
|
+
summary: "Triage complete (fallback defaults)",
|
|
77
|
+
usage: agentResult.usage
|
|
78
|
+
};
|
|
140
79
|
}
|
|
141
80
|
}
|
package/src/session-store.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getSessionRoot } from "./utils/paths.js";
|
|
|
5
5
|
|
|
6
6
|
const SESSION_ROOT = getSessionRoot();
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function newSessionId() {
|
|
9
9
|
const now = new Date();
|
|
10
10
|
const stamp = now.toISOString().replaceAll(/[:.]/g, "-");
|
|
11
11
|
return `s_${stamp}`;
|
|
@@ -63,6 +63,8 @@ const TASK_TEXT_PATTERNS = [
|
|
|
63
63
|
{ pattern: /\bgo(?:lang)?\b/i, skill: "go" },
|
|
64
64
|
{ pattern: /\brust\b/i, skill: "rust" },
|
|
65
65
|
{ pattern: /\bpython\b/i, skill: "python" },
|
|
66
|
+
{ pattern: /\bdjango\b/i, skill: "python" },
|
|
67
|
+
{ pattern: /\bflask\b/i, skill: "python" },
|
|
66
68
|
{ pattern: /\bruby\b/i, skill: "ruby" },
|
|
67
69
|
{ pattern: /\bflutter\b/i, skill: "flutter" },
|
|
68
70
|
{ pattern: /\bswift\b/i, skill: "swift" },
|
|
@@ -76,6 +78,15 @@ const TASK_TEXT_PATTERNS = [
|
|
|
76
78
|
{ pattern: /\breport\b/i, skill: "data-report" },
|
|
77
79
|
{ pattern: /\banalysis\b/i, skill: "data-report" },
|
|
78
80
|
{ pattern: /\bfindings\b/i, skill: "data-report" },
|
|
81
|
+
// Frontend/backend broad categories
|
|
82
|
+
{ pattern: /\bfrontend\b/i, skill: "frontend-design" },
|
|
83
|
+
{ pattern: /\bCSS\b/i, skill: "frontend-design" },
|
|
84
|
+
{ pattern: /\bHTML\b/i, skill: "frontend-design" },
|
|
85
|
+
{ pattern: /\bbackend\b/i, skill: "express" },
|
|
86
|
+
{ pattern: /\bnode\.?js\b/i, skill: "express" },
|
|
87
|
+
{ pattern: /\bAPI\b/, skill: "express" },
|
|
88
|
+
{ pattern: /\bwebsocket\b/i, skill: "express" },
|
|
89
|
+
{ pattern: /\breal[- ]time\b/i, skill: "express" },
|
|
79
90
|
];
|
|
80
91
|
|
|
81
92
|
/**
|
|
@@ -27,6 +27,8 @@ export function resolveSonarHost(rawHost) {
|
|
|
27
27
|
* 2. config.sonarqube.token
|
|
28
28
|
* 3. SONAR_TOKEN env var
|
|
29
29
|
*
|
|
30
|
+
* For async resolution (including credentials file), use resolveSonarTokenAsync.
|
|
31
|
+
*
|
|
30
32
|
* @param {object} [config={}] - Karajan config object
|
|
31
33
|
* @returns {string|null} Token string or null if none found
|
|
32
34
|
*/
|
|
@@ -39,6 +41,22 @@ export function resolveSonarToken(config = {}) {
|
|
|
39
41
|
return token || null;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Async version of resolveSonarToken that also checks the credentials file
|
|
46
|
+
* for previously generated tokens.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} [config={}] - Karajan config object
|
|
49
|
+
* @returns {Promise<string|null>} Token string or null if none found
|
|
50
|
+
*/
|
|
51
|
+
export async function resolveSonarTokenAsync(config = {}) {
|
|
52
|
+
const syncToken = resolveSonarToken(config);
|
|
53
|
+
if (syncToken) return syncToken;
|
|
54
|
+
|
|
55
|
+
// Check credentials file for persisted token
|
|
56
|
+
const fileCreds = (await loadSonarCredentials()) || {};
|
|
57
|
+
return fileCreds.token || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
/**
|
|
43
61
|
* Resolve SonarQube admin credentials from (in priority order):
|
|
44
62
|
* 1. Env vars: KJ_SONAR_ADMIN_USER / KJ_SONAR_ADMIN_PASSWORD
|
|
@@ -57,12 +75,13 @@ export async function resolveSonarCredentials(config = {}) {
|
|
|
57
75
|
process.env.KJ_SONAR_ADMIN_USER ||
|
|
58
76
|
config.sonarqube?.admin_user ||
|
|
59
77
|
fileCreds.user ||
|
|
60
|
-
|
|
78
|
+
"admin"; // Default for fresh SonarQube installs
|
|
61
79
|
|
|
62
80
|
const candidates = [
|
|
63
81
|
process.env.KJ_SONAR_ADMIN_PASSWORD,
|
|
64
82
|
config.sonarqube?.admin_password,
|
|
65
83
|
fileCreds.password,
|
|
84
|
+
"admin", // Default password for fresh SonarQube installs (zero-config)
|
|
66
85
|
].filter(Boolean);
|
|
67
86
|
|
|
68
87
|
return { user, passwords: [...new Set(candidates)] };
|
package/src/sonar/credentials.js
CHANGED
|
@@ -33,3 +33,20 @@ export async function loadSonarCredentials() {
|
|
|
33
33
|
export function credentialsPath() {
|
|
34
34
|
return path.join(getKarajanHome(), CREDENTIALS_FILENAME);
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Save a generated token to ~/.karajan/sonar-credentials.json so it persists across sessions.
|
|
39
|
+
* Merges with existing credentials (preserves user/password if present).
|
|
40
|
+
*/
|
|
41
|
+
export async function saveSonarToken(token) {
|
|
42
|
+
const filePath = path.join(getKarajanHome(), CREDENTIALS_FILENAME);
|
|
43
|
+
let existing = {};
|
|
44
|
+
try {
|
|
45
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
46
|
+
existing = JSON.parse(raw);
|
|
47
|
+
} catch { /* file may not exist */ }
|
|
48
|
+
existing.token = token;
|
|
49
|
+
const dir = getKarajanHome();
|
|
50
|
+
await fs.mkdir(dir, { recursive: true });
|
|
51
|
+
await fs.writeFile(filePath, JSON.stringify(existing, null, 2), "utf8");
|
|
52
|
+
}
|
package/src/sonar/project-key.js
CHANGED
|
@@ -9,7 +9,7 @@ function slug(value) {
|
|
|
9
9
|
.replaceAll(/(^-+)|(-+$)/g, "");
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
function normalizeProjectKey(value) {
|
|
13
13
|
const out = slug(value);
|
|
14
14
|
if (!out) return "kj-default";
|
|
15
15
|
return /[a-z]/.test(out) ? out : `kj-${out}`;
|