karajan-code 1.24.1 → 1.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/config.js +5 -1
- package/src/mcp/run-kj.js +2 -0
- package/src/mcp/tools.js +1 -0
- package/src/orchestrator/iteration-stages.js +124 -10
- package/src/orchestrator/solomon-escalation.js +11 -1
- package/src/orchestrator.js +67 -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
package/src/cli.js
CHANGED
|
@@ -103,6 +103,8 @@ program
|
|
|
103
103
|
.option("--checkpoint-interval <n>", "Minutes between interactive checkpoints (default: 5)")
|
|
104
104
|
.option("--pg-task <cardId>", "Planning Game card ID (e.g., KJC-TSK-0042)")
|
|
105
105
|
.option("--pg-project <projectId>", "Planning Game project ID")
|
|
106
|
+
.option("--auto-simplify", "Auto-simplify pipeline for simple tasks (disable reviewer/tester)")
|
|
107
|
+
.option("--no-auto-simplify", "Force full pipeline regardless of triage level")
|
|
106
108
|
.option("--smart-models", "Enable smart model selection based on triage complexity")
|
|
107
109
|
.option("--no-smart-models", "Disable smart model selection")
|
|
108
110
|
.option("--dry-run", "Show what would be executed without running anything")
|
package/src/config.js
CHANGED
|
@@ -31,7 +31,8 @@ const DEFAULTS = {
|
|
|
31
31
|
impeccable: { enabled: false },
|
|
32
32
|
triage: { enabled: true },
|
|
33
33
|
discover: { enabled: false },
|
|
34
|
-
architect: { enabled: false }
|
|
34
|
+
architect: { enabled: false },
|
|
35
|
+
auto_simplify: true
|
|
35
36
|
},
|
|
36
37
|
review_mode: "standard",
|
|
37
38
|
max_iterations: 5,
|
|
@@ -283,6 +284,8 @@ const PIPELINE_ENABLE_FLAGS = [
|
|
|
283
284
|
["enableArchitect", "architect"]
|
|
284
285
|
];
|
|
285
286
|
|
|
287
|
+
const AUTO_SIMPLIFY_FLAG = "autoSimplify";
|
|
288
|
+
|
|
286
289
|
// Scalar flags: [flagName, setter] — truthy check
|
|
287
290
|
const SCALAR_FLAGS = [
|
|
288
291
|
["mode", (out, v) => { out.review_mode = v; }],
|
|
@@ -361,6 +364,7 @@ function applyBecariaOverride(out, flags) {
|
|
|
361
364
|
}
|
|
362
365
|
|
|
363
366
|
function applyMiscOverrides(out, flags) {
|
|
367
|
+
if (flags[AUTO_SIMPLIFY_FLAG] !== undefined) out.pipeline.auto_simplify = Boolean(flags[AUTO_SIMPLIFY_FLAG]);
|
|
364
368
|
if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
|
|
365
369
|
out.sonarcloud = out.sonarcloud || {};
|
|
366
370
|
if (flags.enableSonarcloud === true) out.sonarcloud.enabled = true;
|
package/src/mcp/run-kj.js
CHANGED
|
@@ -53,6 +53,8 @@ export async function runKjCommand({ command, commandArgs = [], options = {}, en
|
|
|
53
53
|
if (options.autoRebase === false) args.push("--no-auto-rebase");
|
|
54
54
|
addOptionalValue(args, "--task-type", options.taskType);
|
|
55
55
|
normalizeBoolFlag(options.noSonar, "--no-sonar", args);
|
|
56
|
+
if (options.autoSimplify === true) args.push("--auto-simplify");
|
|
57
|
+
if (options.autoSimplify === false) args.push("--no-auto-simplify");
|
|
56
58
|
if (options.smartModels === true) args.push("--smart-models");
|
|
57
59
|
if (options.smartModels === false) args.push("--no-smart-models");
|
|
58
60
|
addOptionalValue(args, "--checkpoint-interval", options.checkpointInterval);
|
package/src/mcp/tools.js
CHANGED
|
@@ -90,6 +90,7 @@ export const tools = [
|
|
|
90
90
|
autoPr: { type: "boolean" },
|
|
91
91
|
autoRebase: { type: "boolean" },
|
|
92
92
|
branchPrefix: { type: "string" },
|
|
93
|
+
autoSimplify: { type: "boolean", description: "Auto-simplify pipeline for simple tasks (level trivial/simple). Disable with false to force full pipeline." },
|
|
93
94
|
smartModels: { type: "boolean", description: "Enable/disable smart model selection based on triage complexity" },
|
|
94
95
|
checkpointInterval: { type: "number", description: "Minutes between interactive checkpoints (default: 5). Set 0 to disable." },
|
|
95
96
|
taskType: { type: "string", enum: ["sw", "infra", "doc", "add-tests", "refactor"], description: "Explicit task type for policy resolution. Overrides triage classification." },
|
|
@@ -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) ---
|
|
@@ -213,6 +214,29 @@ function applyTriageOverrides(pipelineFlags, roleOverrides) {
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
const SIMPLE_LEVELS = new Set(["trivial", "simple"]);
|
|
218
|
+
|
|
219
|
+
function applyAutoSimplify({ pipelineFlags, triageLevel, config, flags, logger, emitter, eventBase }) {
|
|
220
|
+
if (!config.pipeline?.auto_simplify) return false;
|
|
221
|
+
if (!triageLevel || !SIMPLE_LEVELS.has(triageLevel)) return false;
|
|
222
|
+
if (flags.mode) return false;
|
|
223
|
+
if (flags.enableReviewer !== undefined || flags.enableTester !== undefined) return false;
|
|
224
|
+
|
|
225
|
+
pipelineFlags.reviewerEnabled = false;
|
|
226
|
+
pipelineFlags.testerEnabled = false;
|
|
227
|
+
|
|
228
|
+
const disabledRoles = ["reviewer", "tester"];
|
|
229
|
+
logger.info(`Simple task (${triageLevel}) — lightweight pipeline (disabled: ${disabledRoles.join(", ")})`);
|
|
230
|
+
emitProgress(
|
|
231
|
+
emitter,
|
|
232
|
+
makeEvent("pipeline:simplify", { ...eventBase, stage: "triage" }, {
|
|
233
|
+
message: `Simple task (${triageLevel}) — lightweight pipeline`,
|
|
234
|
+
detail: { level: triageLevel, disabledRoles }
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
216
240
|
async function handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger }) {
|
|
217
241
|
const shouldDecompose = triageResult.stageResult?.shouldDecompose
|
|
218
242
|
&& triageResult.stageResult.subtasks?.length > 1
|
|
@@ -794,9 +818,37 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
794
818
|
applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
|
|
795
819
|
stageResults.triage = triageResult.stageResult;
|
|
796
820
|
|
|
821
|
+
// --- Auto-simplify pipeline for simple tasks (before explicit flag overrides) ---
|
|
822
|
+
const simplified = applyAutoSimplify({
|
|
823
|
+
pipelineFlags,
|
|
824
|
+
triageLevel: triageResult.stageResult?.level || null,
|
|
825
|
+
config, flags, logger, emitter, eventBase
|
|
826
|
+
});
|
|
827
|
+
if (simplified) stageResults.triage.autoSimplified = true;
|
|
828
|
+
|
|
797
829
|
await handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger });
|
|
798
830
|
|
|
799
831
|
applyFlagOverrides(pipelineFlags, flags);
|
|
832
|
+
|
|
833
|
+
// --- Auto-detect TDD applicability when methodology not explicitly set ---
|
|
834
|
+
if (!flags.methodology) {
|
|
835
|
+
const projectDir = config.projectDir || process.cwd();
|
|
836
|
+
const detection = await detectTestFramework(projectDir);
|
|
837
|
+
if (!detection.hasTests) {
|
|
838
|
+
config = { ...config, development: { ...config.development, methodology: "standard", require_test_changes: false } };
|
|
839
|
+
logger.info("No test framework detected — using standard methodology");
|
|
840
|
+
} else {
|
|
841
|
+
config = { ...config, development: { ...config.development, methodology: "tdd", require_test_changes: true } };
|
|
842
|
+
logger.info(`Test framework detected (${detection.framework}) — using TDD methodology`);
|
|
843
|
+
}
|
|
844
|
+
emitProgress(emitter, makeEvent("tdd:auto-detect", { ...eventBase, stage: "pre-loop" }, {
|
|
845
|
+
message: detection.hasTests
|
|
846
|
+
? `TDD auto-detected: ${detection.framework}`
|
|
847
|
+
: "TDD skipped: no test framework found",
|
|
848
|
+
detail: detection
|
|
849
|
+
}));
|
|
850
|
+
}
|
|
851
|
+
|
|
800
852
|
const updatedConfig = resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags });
|
|
801
853
|
|
|
802
854
|
// --- Researcher → Planner ---
|
|
@@ -900,7 +952,9 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
|
|
|
900
952
|
if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
|
|
901
953
|
if (tddResult.action === "continue") return { action: "continue" };
|
|
902
954
|
|
|
903
|
-
|
|
955
|
+
const skipSonarForTaskType = new Set(["infra", "doc"]);
|
|
956
|
+
const effectiveTaskType = session.resolved_policies?.taskType || null;
|
|
957
|
+
if (config.sonarqube.enabled && !skipSonarForTaskType.has(effectiveTaskType)) {
|
|
904
958
|
const sonarResult = await runSonarStage({
|
|
905
959
|
config, logger, emitter, eventBase, session, trackBudget, iteration: i,
|
|
906
960
|
repeatDetector, budgetSummary, sonarState, askQuestion, task
|
|
@@ -1110,8 +1164,18 @@ async function runSingleIteration(ctx) {
|
|
|
1110
1164
|
if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
|
|
1111
1165
|
}
|
|
1112
1166
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1167
|
+
// Solomon already evaluated the rejection in runReviewerStage → handleReviewerRejection
|
|
1168
|
+
// Only use retry counter as fallback if Solomon is disabled
|
|
1169
|
+
if (!config.pipeline?.solomon?.enabled) {
|
|
1170
|
+
const retryResult = await handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion });
|
|
1171
|
+
if (retryResult.action === "return") return retryResult;
|
|
1172
|
+
} else {
|
|
1173
|
+
// Solomon is enabled — feed back the blocking issues for the next coder iteration
|
|
1174
|
+
session.last_reviewer_feedback = review.blocking_issues
|
|
1175
|
+
.map((x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`)
|
|
1176
|
+
.join("\n");
|
|
1177
|
+
await saveSession(session);
|
|
1178
|
+
}
|
|
1115
1179
|
|
|
1116
1180
|
return { action: "next" };
|
|
1117
1181
|
}
|
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
|