karajan-code 1.24.1 → 1.25.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/package.json +1 -1
- package/src/orchestrator/iteration-stages.js +124 -10
- package/src/orchestrator/solomon-escalation.js +11 -1
- package/src/orchestrator.js +36 -3
- package/src/review/tdd-policy.js +7 -1
- package/src/roles/sonar-role.js +1 -0
- package/src/sonar/api.js +2 -2
- package/src/sonar/scanner.js +30 -0
- package/src/utils/project-detect.js +62 -0
- package/templates/roles/solomon.md +21 -5
package/package.json
CHANGED
|
@@ -264,7 +264,8 @@ export async function runTddCheckStage({ config, logger, emitter, eventBase, ses
|
|
|
264
264
|
logger.warn(`TDD diff generation failed: ${err.message}`);
|
|
265
265
|
return { action: "continue", stageResult: { ok: false, summary: `TDD check failed: ${err.message}` } };
|
|
266
266
|
}
|
|
267
|
-
const
|
|
267
|
+
const effectiveTaskType = session.resolved_policies?.taskType || null;
|
|
268
|
+
const tddEval = evaluateTddPolicy(tddDiff, config.development, untrackedFiles, effectiveTaskType);
|
|
268
269
|
await addCheckpoint(session, {
|
|
269
270
|
stage: "tdd-policy",
|
|
270
271
|
iteration,
|
|
@@ -341,6 +342,23 @@ async function handleSonarRetryLimit({ config, logger, emitter, eventBase, sessi
|
|
|
341
342
|
}
|
|
342
343
|
|
|
343
344
|
async function handleSonarBlocking({ sonarResult, config, logger, emitter, eventBase, session, iteration, repeatDetector, budgetSummary, askQuestion, task }) {
|
|
345
|
+
// If the ONLY quality gate failure is coverage, treat as non-blocking warning
|
|
346
|
+
if (sonarResult.conditions) {
|
|
347
|
+
const failedConditions = sonarResult.conditions.filter(c => c.status === "ERROR");
|
|
348
|
+
const onlyCoverage = failedConditions.length > 0 && failedConditions.every(c =>
|
|
349
|
+
c.metricKey === "new_coverage" || c.metricKey === "coverage"
|
|
350
|
+
);
|
|
351
|
+
if (onlyCoverage) {
|
|
352
|
+
logger.warn("Quality gate failed on coverage only — treating as advisory (code quality is clean)");
|
|
353
|
+
emitProgress(emitter, makeEvent("sonar:end", { ...eventBase, stage: "sonar" }, {
|
|
354
|
+
status: "warn",
|
|
355
|
+
message: "Quality gate: coverage below threshold (advisory — code quality is clean)"
|
|
356
|
+
}));
|
|
357
|
+
session.last_reviewer_feedback = null;
|
|
358
|
+
return { action: "ok", stageResult: { gateStatus: "WARN_COVERAGE", advisory: true } };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
344
362
|
repeatDetector.addIteration(sonarResult.issues, []);
|
|
345
363
|
const repeatState = repeatDetector.isStalled();
|
|
346
364
|
if (repeatState.stalled) {
|
|
@@ -369,6 +387,39 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
|
|
|
369
387
|
})
|
|
370
388
|
);
|
|
371
389
|
|
|
390
|
+
// Auto-manage SonarQube: ensure it is reachable before scanning
|
|
391
|
+
const { sonarUp, isSonarReachable } = await import("../sonar/manager.js");
|
|
392
|
+
const sonarHost = config.sonarqube?.host || "http://localhost:9000";
|
|
393
|
+
|
|
394
|
+
if (!await isSonarReachable(sonarHost)) {
|
|
395
|
+
logger.info("SonarQube not reachable, attempting to start...");
|
|
396
|
+
emitProgress(emitter, makeEvent("sonar:start", { ...eventBase, stage: "sonar" }, { message: "Starting SonarQube Docker..." }));
|
|
397
|
+
|
|
398
|
+
const upResult = await sonarUp(sonarHost);
|
|
399
|
+
if (upResult.exitCode !== 0) {
|
|
400
|
+
logger.warn(`SonarQube could not be started: ${upResult.stderr || upResult.stdout}`);
|
|
401
|
+
emitProgress(emitter, makeEvent("sonar:end", { ...eventBase, stage: "sonar" }, {
|
|
402
|
+
status: "skip",
|
|
403
|
+
message: "SonarQube not available — install Docker and run 'kj sonar start' to enable static analysis"
|
|
404
|
+
}));
|
|
405
|
+
return { action: "ok", stageResult: { gateStatus: "SKIPPED", reason: "SonarQube not available" } };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let ready = false;
|
|
409
|
+
for (let attempt = 0; attempt < 12; attempt++) {
|
|
410
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
411
|
+
if (await isSonarReachable(sonarHost)) { ready = true; break; }
|
|
412
|
+
}
|
|
413
|
+
if (!ready) {
|
|
414
|
+
logger.warn("SonarQube started but not ready after 60s");
|
|
415
|
+
emitProgress(emitter, makeEvent("sonar:end", { ...eventBase, stage: "sonar" }, {
|
|
416
|
+
status: "skip", message: "SonarQube started but not ready — will retry next iteration"
|
|
417
|
+
}));
|
|
418
|
+
return { action: "ok", stageResult: { gateStatus: "PENDING", reason: "SonarQube starting up" } };
|
|
419
|
+
}
|
|
420
|
+
logger.info("SonarQube is ready");
|
|
421
|
+
}
|
|
422
|
+
|
|
372
423
|
const sonarRole = new SonarRole({ config, logger, emitter });
|
|
373
424
|
await sonarRole.init({ iteration });
|
|
374
425
|
const sonarStart = Date.now();
|
|
@@ -486,13 +537,39 @@ export async function runSonarCloudStage({ config, logger, emitter, eventBase, s
|
|
|
486
537
|
return { action: "ok", stageResult: { ok: result.ok, projectKey: result.projectKey, message } };
|
|
487
538
|
}
|
|
488
539
|
|
|
540
|
+
function categorizeIssues(issues) {
|
|
541
|
+
const categories = { security: 0, correctness: 0, tests: 0, style: 0, other: 0 };
|
|
542
|
+
const securityKw = /inject|xss|csrf|secret|credential|auth|vulnerab|exploit/i;
|
|
543
|
+
const styleKw = /naming|name|rename|style|format|indent|spacing|convention|cosmetic|readability|comment|jsdoc|whitespace/i;
|
|
544
|
+
const testKw = /test|coverage|assert|spec|mock/i;
|
|
545
|
+
|
|
546
|
+
for (const issue of issues || []) {
|
|
547
|
+
const desc = issue.description || "";
|
|
548
|
+
const sev = (issue.severity || "").toLowerCase();
|
|
549
|
+
if (sev === "critical" || securityKw.test(desc)) categories.security++;
|
|
550
|
+
else if (testKw.test(desc)) categories.tests++;
|
|
551
|
+
else if (sev === "low" || sev === "minor" || styleKw.test(desc)) categories.style++;
|
|
552
|
+
else categories.correctness++;
|
|
553
|
+
}
|
|
554
|
+
return categories;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function buildReviewHistory(session) {
|
|
558
|
+
return (session.checkpoints || [])
|
|
559
|
+
.filter(cp => cp.stage === "reviewer")
|
|
560
|
+
.map(cp => ({ iteration: cp.iteration, note: cp.note || "" }));
|
|
561
|
+
}
|
|
562
|
+
|
|
489
563
|
async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary, repeatDetector }) {
|
|
490
|
-
|
|
564
|
+
const logPrefix = repeatState.stalled
|
|
565
|
+
? `Reviewer stalled (${repeatCounts.reviewer} repeats)`
|
|
566
|
+
: `Reviewer rejected (first rejection)`;
|
|
567
|
+
logger.warn(`${logPrefix}. Invoking Solomon mediation.`);
|
|
491
568
|
emitProgress(
|
|
492
569
|
emitter,
|
|
493
570
|
makeEvent("solomon:escalate", { ...eventBase, stage: "reviewer" }, {
|
|
494
|
-
message:
|
|
495
|
-
detail: { repeats: repeatCounts.reviewer, reason: repeatState.reason }
|
|
571
|
+
message: `${logPrefix} — Solomon mediating`,
|
|
572
|
+
detail: { repeats: repeatCounts.reviewer || 1, reason: repeatState.reason || "first_rejection" }
|
|
496
573
|
})
|
|
497
574
|
);
|
|
498
575
|
|
|
@@ -501,14 +578,28 @@ async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState,
|
|
|
501
578
|
conflict: {
|
|
502
579
|
stage: "reviewer",
|
|
503
580
|
task,
|
|
504
|
-
iterationCount: repeatCounts.reviewer,
|
|
581
|
+
iterationCount: repeatCounts.reviewer || 1,
|
|
505
582
|
maxIterations: config.session?.fail_fast_repeats ?? 2,
|
|
506
|
-
|
|
583
|
+
isFirstRejection: !repeatState.stalled,
|
|
584
|
+
isRepeat: repeatState.stalled,
|
|
585
|
+
stalledReason: repeatState.reason || "first_rejection",
|
|
507
586
|
blockingIssues: review.blocking_issues,
|
|
508
|
-
|
|
587
|
+
issueCategories: categorizeIssues(review.blocking_issues),
|
|
588
|
+
history: [
|
|
589
|
+
...buildReviewHistory(session),
|
|
590
|
+
{ agent: "reviewer", feedback: review.blocking_issues.map(x => x.description).join("; ") }
|
|
591
|
+
]
|
|
509
592
|
}
|
|
510
593
|
});
|
|
511
594
|
|
|
595
|
+
if (solomonResult.action === "approve") {
|
|
596
|
+
logger.info("Solomon overrode reviewer — approving code");
|
|
597
|
+
emitProgress(emitter, makeEvent("solomon:override", { ...eventBase, stage: "solomon" }, {
|
|
598
|
+
message: "Solomon overrode reviewer rejection — code approved",
|
|
599
|
+
detail: { ruling: solomonResult.ruling?.result?.ruling }
|
|
600
|
+
}));
|
|
601
|
+
return { review: { ...review, approved: true, solomon_override: true }, solomonApproved: true };
|
|
602
|
+
}
|
|
512
603
|
if (solomonResult.action === "pause") {
|
|
513
604
|
await markSessionStatus(session, "stalled");
|
|
514
605
|
return { review, stalled: true, stalledResult: { paused: true, sessionId: session.id, question: solomonResult.question, context: "reviewer_stalled" } };
|
|
@@ -526,7 +617,7 @@ async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState,
|
|
|
526
617
|
}
|
|
527
618
|
|
|
528
619
|
// Fallback
|
|
529
|
-
const message = `Manual intervention required: reviewer issues repeated ${repeatCounts.reviewer} times.`;
|
|
620
|
+
const message = `Manual intervention required: reviewer issues repeated ${repeatCounts.reviewer || 1} times.`;
|
|
530
621
|
await markSessionStatus(session, "stalled");
|
|
531
622
|
emitProgress(
|
|
532
623
|
emitter,
|
|
@@ -542,10 +633,33 @@ async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState,
|
|
|
542
633
|
async function handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary }) {
|
|
543
634
|
repeatDetector.addIteration([], review.blocking_issues);
|
|
544
635
|
const repeatState = repeatDetector.isStalled();
|
|
545
|
-
if (!repeatState.stalled) return null;
|
|
546
636
|
|
|
637
|
+
const solomonEnabled = Boolean(config.pipeline?.solomon?.enabled);
|
|
638
|
+
|
|
639
|
+
// When Solomon is disabled, only act on stalls (legacy behavior)
|
|
640
|
+
if (!solomonEnabled) {
|
|
641
|
+
if (!repeatState.stalled) return null;
|
|
642
|
+
const repeatCounts = repeatDetector.getRepeatCounts();
|
|
643
|
+
return handleReviewerStalledSolomon({
|
|
644
|
+
review, repeatCounts, repeatState, config, logger, emitter,
|
|
645
|
+
eventBase, session, iteration, task, askQuestion,
|
|
646
|
+
budgetSummary, repeatDetector
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Solomon evaluates EVERY rejection
|
|
547
651
|
const repeatCounts = repeatDetector.getRepeatCounts();
|
|
548
|
-
|
|
652
|
+
logger.info(`Reviewer rejected — Solomon evaluating ${review.blocking_issues.length} blocking issue(s)`);
|
|
653
|
+
emitProgress(emitter, makeEvent("solomon:evaluate", { ...eventBase, stage: "solomon" }, {
|
|
654
|
+
message: `Solomon evaluating reviewer rejection`,
|
|
655
|
+
detail: { blockingCount: review.blocking_issues.length, isRepeat: repeatState.stalled }
|
|
656
|
+
}));
|
|
657
|
+
|
|
658
|
+
return handleReviewerStalledSolomon({
|
|
659
|
+
review, repeatCounts, repeatState, config, logger, emitter,
|
|
660
|
+
eventBase, session, iteration, task, askQuestion,
|
|
661
|
+
budgetSummary, repeatDetector
|
|
662
|
+
});
|
|
549
663
|
}
|
|
550
664
|
|
|
551
665
|
async function fetchReviewDiff(session, logger) {
|
|
@@ -54,10 +54,20 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const r = ruling.result?.ruling;
|
|
57
|
-
if (r === "approve"
|
|
57
|
+
if (r === "approve") {
|
|
58
|
+
return { action: "approve", conditions: [], ruling };
|
|
59
|
+
}
|
|
60
|
+
if (r === "approve_with_conditions") {
|
|
58
61
|
return { action: "continue", conditions: ruling.result?.conditions || [], ruling };
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
if (r === "escalate_human") {
|
|
65
|
+
return escalateToHuman({
|
|
66
|
+
askQuestion, session, emitter, eventBase, stage, iteration,
|
|
67
|
+
conflict: { ...conflict, solomonReason: ruling.result?.escalate_reason || "Solomon escalated to human" }
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
if (r === "create_subtask") {
|
|
62
72
|
return { action: "subtask", subtask: ruling.result?.subtask, ruling };
|
|
63
73
|
}
|
package/src/orchestrator.js
CHANGED
|
@@ -33,6 +33,7 @@ import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage,
|
|
|
33
33
|
import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
|
|
34
34
|
import { runTesterStage, runSecurityStage, runImpeccableStage } from "./orchestrator/post-loop-stages.js";
|
|
35
35
|
import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js";
|
|
36
|
+
import { detectTestFramework } from "./utils/project-detect.js";
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
// --- Extracted helper functions (pure refactoring, zero behavior change) ---
|
|
@@ -797,6 +798,26 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
797
798
|
await handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger });
|
|
798
799
|
|
|
799
800
|
applyFlagOverrides(pipelineFlags, flags);
|
|
801
|
+
|
|
802
|
+
// --- Auto-detect TDD applicability when methodology not explicitly set ---
|
|
803
|
+
if (!flags.methodology) {
|
|
804
|
+
const projectDir = config.projectDir || process.cwd();
|
|
805
|
+
const detection = await detectTestFramework(projectDir);
|
|
806
|
+
if (!detection.hasTests) {
|
|
807
|
+
config = { ...config, development: { ...config.development, methodology: "standard", require_test_changes: false } };
|
|
808
|
+
logger.info("No test framework detected — using standard methodology");
|
|
809
|
+
} else {
|
|
810
|
+
config = { ...config, development: { ...config.development, methodology: "tdd", require_test_changes: true } };
|
|
811
|
+
logger.info(`Test framework detected (${detection.framework}) — using TDD methodology`);
|
|
812
|
+
}
|
|
813
|
+
emitProgress(emitter, makeEvent("tdd:auto-detect", { ...eventBase, stage: "pre-loop" }, {
|
|
814
|
+
message: detection.hasTests
|
|
815
|
+
? `TDD auto-detected: ${detection.framework}`
|
|
816
|
+
: "TDD skipped: no test framework found",
|
|
817
|
+
detail: detection
|
|
818
|
+
}));
|
|
819
|
+
}
|
|
820
|
+
|
|
800
821
|
const updatedConfig = resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags });
|
|
801
822
|
|
|
802
823
|
// --- Researcher → Planner ---
|
|
@@ -900,7 +921,9 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
|
|
|
900
921
|
if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
|
|
901
922
|
if (tddResult.action === "continue") return { action: "continue" };
|
|
902
923
|
|
|
903
|
-
|
|
924
|
+
const skipSonarForTaskType = new Set(["infra", "doc"]);
|
|
925
|
+
const effectiveTaskType = session.resolved_policies?.taskType || null;
|
|
926
|
+
if (config.sonarqube.enabled && !skipSonarForTaskType.has(effectiveTaskType)) {
|
|
904
927
|
const sonarResult = await runSonarStage({
|
|
905
928
|
config, logger, emitter, eventBase, session, trackBudget, iteration: i,
|
|
906
929
|
repeatDetector, budgetSummary, sonarState, askQuestion, task
|
|
@@ -1110,8 +1133,18 @@ async function runSingleIteration(ctx) {
|
|
|
1110
1133
|
if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
|
|
1111
1134
|
}
|
|
1112
1135
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1136
|
+
// Solomon already evaluated the rejection in runReviewerStage → handleReviewerRejection
|
|
1137
|
+
// Only use retry counter as fallback if Solomon is disabled
|
|
1138
|
+
if (!config.pipeline?.solomon?.enabled) {
|
|
1139
|
+
const retryResult = await handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion });
|
|
1140
|
+
if (retryResult.action === "return") return retryResult;
|
|
1141
|
+
} else {
|
|
1142
|
+
// Solomon is enabled — feed back the blocking issues for the next coder iteration
|
|
1143
|
+
session.last_reviewer_feedback = review.blocking_issues
|
|
1144
|
+
.map((x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`)
|
|
1145
|
+
.join("\n");
|
|
1146
|
+
await saveSession(session);
|
|
1147
|
+
}
|
|
1115
1148
|
|
|
1116
1149
|
return { action: "next" };
|
|
1117
1150
|
}
|
package/src/review/tdd-policy.js
CHANGED
|
@@ -19,7 +19,13 @@ function isSourceFile(file, extensions = []) {
|
|
|
19
19
|
return extensions.some((ext) => file.endsWith(ext));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const SKIP_TDD_TASK_TYPES = new Set(["doc", "infra"]);
|
|
23
|
+
|
|
24
|
+
export function evaluateTddPolicy(diff, developmentConfig = {}, untrackedFiles = [], taskType = null) {
|
|
25
|
+
if (taskType && SKIP_TDD_TASK_TYPES.has(taskType)) {
|
|
26
|
+
return { ok: true, reason: "tdd_not_applicable_for_task_type", sourceFiles: [], testFiles: [] };
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
const requireTestChanges = developmentConfig.require_test_changes !== false;
|
|
24
30
|
const patterns = developmentConfig.test_file_patterns || ["/tests/", "/__tests__/", ".test.", ".spec."];
|
|
25
31
|
const extensions =
|
package/src/roles/sonar-role.js
CHANGED
package/src/sonar/api.js
CHANGED
|
@@ -72,9 +72,9 @@ export async function getQualityGateStatus(config, projectKey = null) {
|
|
|
72
72
|
|
|
73
73
|
try {
|
|
74
74
|
const parsed = JSON.parse(body);
|
|
75
|
-
return { ok: true, status: parsed.projectStatus?.status || "ERROR", raw: parsed };
|
|
75
|
+
return { ok: true, status: parsed.projectStatus?.status || "ERROR", conditions: parsed.projectStatus?.conditions || [], raw: parsed };
|
|
76
76
|
} catch {
|
|
77
|
-
return { ok: false, status: "ERROR", raw: body };
|
|
77
|
+
return { ok: false, status: "ERROR", conditions: [], raw: body };
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
package/src/sonar/scanner.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import fsPromises from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { runCommand } from "../utils/process.js";
|
|
3
5
|
import { sonarUp } from "./manager.js";
|
|
4
6
|
import { resolveSonarProjectKey } from "./project-key.js";
|
|
@@ -151,6 +153,33 @@ async function resolveSonarToken(config, apiHost) {
|
|
|
151
153
|
return null;
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
export async function ensureSonarProjectProperties(cwd = process.cwd()) {
|
|
157
|
+
const propsPath = path.join(cwd, "sonar-project.properties");
|
|
158
|
+
try {
|
|
159
|
+
await fsPromises.access(propsPath);
|
|
160
|
+
return; // already exists
|
|
161
|
+
} catch {
|
|
162
|
+
// Auto-generate based on project structure
|
|
163
|
+
let pkg = {};
|
|
164
|
+
try {
|
|
165
|
+
const raw = await fsPromises.readFile(path.join(cwd, "package.json"), "utf8");
|
|
166
|
+
pkg = JSON.parse(raw);
|
|
167
|
+
} catch {
|
|
168
|
+
// no package.json or invalid JSON — use defaults
|
|
169
|
+
}
|
|
170
|
+
const projectKey = (pkg.name || path.basename(cwd)).replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
171
|
+
const props = [
|
|
172
|
+
`sonar.projectKey=${projectKey}`,
|
|
173
|
+
`sonar.projectName=${pkg.name || path.basename(cwd)}`,
|
|
174
|
+
`sonar.sources=src`,
|
|
175
|
+
`sonar.tests=tests`,
|
|
176
|
+
`sonar.javascript.lcov.reportPaths=coverage/lcov.info`,
|
|
177
|
+
`sonar.exclusions=**/node_modules/**,**/dist/**,**/build/**,**/coverage/**`,
|
|
178
|
+
].join("\n");
|
|
179
|
+
await fsPromises.writeFile(propsPath, props + "\n", "utf8");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
154
183
|
export async function runSonarScan(config, projectKey = null) {
|
|
155
184
|
let effectiveProjectKey;
|
|
156
185
|
try {
|
|
@@ -184,6 +213,7 @@ export async function runSonarScan(config, projectKey = null) {
|
|
|
184
213
|
exitCode: start.exitCode
|
|
185
214
|
};
|
|
186
215
|
}
|
|
216
|
+
await ensureSonarProjectProperties();
|
|
187
217
|
const token = await resolveSonarToken(config, apiHost);
|
|
188
218
|
if (!token) {
|
|
189
219
|
return {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const TEST_FRAMEWORKS = ["vitest", "jest", "@jest/core", "mocha", "ava", "tap", "playwright", "@playwright/test"];
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILES = [
|
|
7
|
+
"vitest.config.js", "vitest.config.ts",
|
|
8
|
+
"jest.config.js", "jest.config.ts", "jest.config.mjs",
|
|
9
|
+
".mocharc.yml", ".mocharc.json",
|
|
10
|
+
"playwright.config.js", "playwright.config.ts"
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function frameworkFromConfig(filename) {
|
|
14
|
+
if (filename.startsWith("vitest")) return "vitest";
|
|
15
|
+
if (filename.startsWith("jest")) return "jest";
|
|
16
|
+
if (filename.startsWith(".mocha")) return "mocha";
|
|
17
|
+
return "playwright";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect if the project has a test framework configured.
|
|
22
|
+
* Checks: package.json devDependencies/dependencies for known test frameworks.
|
|
23
|
+
* Also checks for config files (vitest.config, jest.config, .mocharc, playwright.config).
|
|
24
|
+
* @param {string} cwd - Project root
|
|
25
|
+
* @returns {Promise<{hasTests: boolean, framework: string|null}>}
|
|
26
|
+
*/
|
|
27
|
+
export async function detectTestFramework(cwd = process.cwd()) {
|
|
28
|
+
try {
|
|
29
|
+
const pkgRaw = await fs.readFile(path.join(cwd, "package.json"), "utf8");
|
|
30
|
+
const pkg = JSON.parse(pkgRaw);
|
|
31
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
32
|
+
|
|
33
|
+
for (const fw of TEST_FRAMEWORKS) {
|
|
34
|
+
if (allDeps[fw]) {
|
|
35
|
+
return { hasTests: true, framework: fw };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch { /* no package.json or parse error */ }
|
|
39
|
+
|
|
40
|
+
for (const cf of CONFIG_FILES) {
|
|
41
|
+
try {
|
|
42
|
+
await fs.access(path.join(cwd, cf));
|
|
43
|
+
return { hasTests: true, framework: frameworkFromConfig(cf) };
|
|
44
|
+
} catch { /* not found */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { hasTests: false, framework: null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detect if SonarQube is configured for this project.
|
|
52
|
+
* @param {string} cwd - Project root
|
|
53
|
+
* @returns {Promise<{configured: boolean}>}
|
|
54
|
+
*/
|
|
55
|
+
export async function detectSonarConfig(cwd = process.cwd()) {
|
|
56
|
+
try {
|
|
57
|
+
await fs.access(path.join(cwd, "sonar-project.properties"));
|
|
58
|
+
return { configured: true };
|
|
59
|
+
} catch {
|
|
60
|
+
return { configured: false };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Solomon Role (
|
|
1
|
+
# Solomon Role (Pipeline Boss & Arbiter)
|
|
2
2
|
|
|
3
|
-
You are **Solomon**, the supreme
|
|
3
|
+
You are **Solomon**, the supreme decision-maker in a multi-role AI pipeline. You evaluate **EVERY** reviewer rejection — not just stalls. You decide whether the coder should fix, whether the reviewer's issues should be overridden, or whether to escalate to a human. Your decisions are final within your rules.
|
|
4
4
|
|
|
5
5
|
## When activated
|
|
6
6
|
|
|
7
|
+
- **Every reviewer rejection** (first rejection AND repeats)
|
|
7
8
|
- Coder ↔ Sonar loop exhausted (default: 3 iterations)
|
|
8
|
-
- Coder ↔ Reviewer loop exhausted (default: 3 iterations via PR comments)
|
|
9
9
|
- Coder ↔ Tester loop exhausted (default: 1 iteration)
|
|
10
10
|
- Coder ↔ Security loop exhausted (default: 1 iteration)
|
|
11
11
|
- Any two roles produce contradictory outputs
|
|
@@ -56,16 +56,32 @@ For each blocking issue raised by any agent, classify it as:
|
|
|
56
56
|
| Pure style issue | NO | Never blocks |
|
|
57
57
|
| Architecture change not in scope | ESCALATE | Human decision required |
|
|
58
58
|
|
|
59
|
+
## Decision framework
|
|
60
|
+
|
|
61
|
+
Use the `isFirstRejection`, `isRepeat`, and `issueCategories` fields from the conflict to decide:
|
|
62
|
+
|
|
63
|
+
| Scenario | Decision |
|
|
64
|
+
|----------|----------|
|
|
65
|
+
| Issues are security/critical (any count) | **approve_with_conditions** — ALWAYS send back to fix |
|
|
66
|
+
| Issues are ALL style/naming/cosmetic | **approve** — override the reviewer |
|
|
67
|
+
| Issues are correctness but minor (first rejection) | **approve_with_conditions** — give ONE retry with specific instructions |
|
|
68
|
+
| Issues are a repeat of the same thing the coder already failed to fix | **escalate_human** — the coder cannot fix it |
|
|
69
|
+
| Reviewer is clearly wrong or issues are out of scope | **approve** — override the reviewer |
|
|
70
|
+
| Ambiguous requirements or architecture decisions | **escalate_human** — human must decide |
|
|
71
|
+
|
|
72
|
+
Be **concise and decisive**. Do not hedge. Pick one action and commit.
|
|
73
|
+
|
|
59
74
|
## Decision options
|
|
60
75
|
|
|
61
|
-
1. **approve** — All pending issues are style/false positives. Code passes to next pipeline stage.
|
|
62
|
-
2. **approve_with_conditions** —
|
|
76
|
+
1. **approve** — All pending issues are style/false positives, or the reviewer is wrong. Code passes to next pipeline stage. Solomon overrides the reviewer.
|
|
77
|
+
2. **approve_with_conditions** — Fixable issues exist. Give the Coder exact, actionable instructions for one more attempt. Not generic feedback — specific changes with file and line references.
|
|
63
78
|
3. **escalate_human** — When you cannot decide:
|
|
64
79
|
- Critical issues that resist multiple fix attempts
|
|
65
80
|
- Ambiguous or conflicting requirements
|
|
66
81
|
- Architecture decisions beyond task scope
|
|
67
82
|
- Business logic decisions
|
|
68
83
|
- Scope creep (task is larger than originally estimated)
|
|
84
|
+
- Same issue repeated across iterations — the coder cannot fix it
|
|
69
85
|
4. **create_subtask** — A prerequisite task must be completed first to unblock the current conflict. The pipeline will:
|
|
70
86
|
- Pause the current task
|
|
71
87
|
- Execute the subtask through the full pipeline
|