karajan-code 1.23.0 → 1.24.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 +1 -0
- package/src/config.js +6 -4
- package/src/guards/intent-guard.js +13 -1
- package/src/mcp/run-kj.js +1 -0
- package/src/mcp/server-handlers.js +2 -1
- package/src/mcp/tools.js +1 -0
- package/src/orchestrator/post-loop-stages.js +51 -0
- package/src/orchestrator/pre-loop-stages.js +1 -1
- package/src/orchestrator/solomon-rules.js +24 -3
- package/src/orchestrator.js +22 -8
- package/src/roles/impeccable-role.js +121 -0
- package/src/roles/triage-role.js +1 -1
- package/templates/roles/impeccable.md +125 -0
- package/templates/roles/triage.md +8 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
package/src/config.js
CHANGED
|
@@ -16,6 +16,7 @@ const DEFAULTS = {
|
|
|
16
16
|
researcher: { provider: null, model: null },
|
|
17
17
|
tester: { provider: null, model: null },
|
|
18
18
|
security: { provider: null, model: null },
|
|
19
|
+
impeccable: { provider: null, model: null },
|
|
19
20
|
triage: { provider: null, model: null },
|
|
20
21
|
discover: { provider: null, model: null },
|
|
21
22
|
architect: { provider: null, model: null }
|
|
@@ -27,6 +28,7 @@ const DEFAULTS = {
|
|
|
27
28
|
researcher: { enabled: false },
|
|
28
29
|
tester: { enabled: true },
|
|
29
30
|
security: { enabled: true },
|
|
31
|
+
impeccable: { enabled: false },
|
|
30
32
|
triage: { enabled: true },
|
|
31
33
|
discover: { enabled: false },
|
|
32
34
|
architect: { enabled: false }
|
|
@@ -276,7 +278,7 @@ const ROLE_MODEL_FLAGS = [
|
|
|
276
278
|
const PIPELINE_ENABLE_FLAGS = [
|
|
277
279
|
["enablePlanner", "planner"], ["enableRefactorer", "refactorer"],
|
|
278
280
|
["enableSolomon", "solomon"], ["enableResearcher", "researcher"],
|
|
279
|
-
["enableTester", "tester"], ["enableSecurity", "security"],
|
|
281
|
+
["enableTester", "tester"], ["enableSecurity", "security"], ["enableImpeccable", "impeccable"],
|
|
280
282
|
["enableTriage", "triage"], ["enableDiscover", "discover"],
|
|
281
283
|
["enableArchitect", "architect"]
|
|
282
284
|
];
|
|
@@ -408,14 +410,14 @@ export function resolveRole(config, role) {
|
|
|
408
410
|
let provider = roleConfig.provider ?? null;
|
|
409
411
|
if (!provider && role === "coder") provider = legacyCoder;
|
|
410
412
|
if (!provider && role === "reviewer") provider = legacyReviewer;
|
|
411
|
-
if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "triage" || role === "discover" || role === "architect")) {
|
|
413
|
+
if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect")) {
|
|
412
414
|
provider = roles.coder?.provider || legacyCoder;
|
|
413
415
|
}
|
|
414
416
|
|
|
415
417
|
let model = roleConfig.model ?? null;
|
|
416
418
|
if (!model && role === "coder") model = config?.coder_options?.model ?? null;
|
|
417
419
|
if (!model && role === "reviewer") model = config?.reviewer_options?.model ?? null;
|
|
418
|
-
if (!model && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "triage" || role === "discover" || role === "architect")) {
|
|
420
|
+
if (!model && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect")) {
|
|
419
421
|
model = config?.coder_options?.model ?? null;
|
|
420
422
|
}
|
|
421
423
|
|
|
@@ -426,7 +428,7 @@ export function resolveRole(config, role) {
|
|
|
426
428
|
const RUN_PIPELINE_ROLES = [
|
|
427
429
|
["reviewer", "reviewer"], ["triage", "triage"], ["planner", "planner"],
|
|
428
430
|
["refactorer", "refactorer"], ["researcher", "researcher"],
|
|
429
|
-
["tester", "tester"], ["security", "security"]
|
|
431
|
+
["tester", "tester"], ["security", "security"], ["impeccable", "impeccable"]
|
|
430
432
|
];
|
|
431
433
|
|
|
432
434
|
// Direct command-to-role mapping for non-"run" commands
|
|
@@ -51,6 +51,16 @@ const INTENT_PATTERNS = [
|
|
|
51
51
|
confidence: 0.9,
|
|
52
52
|
message: "Trivial fix detected",
|
|
53
53
|
},
|
|
54
|
+
// Frontend / UI tasks (sets hasFrontend flag for impeccable role activation)
|
|
55
|
+
{
|
|
56
|
+
id: "frontend-ui",
|
|
57
|
+
keywords: ["html", "css", "ui", "landing", "component", "responsive", "accessibility", "a11y", "frontend", "design", "layout", "styling", "dark mode", "animation"],
|
|
58
|
+
taskType: "sw",
|
|
59
|
+
level: "simple",
|
|
60
|
+
confidence: 0.8,
|
|
61
|
+
message: "Frontend/UI task detected",
|
|
62
|
+
hasFrontend: true,
|
|
63
|
+
},
|
|
54
64
|
];
|
|
55
65
|
|
|
56
66
|
/**
|
|
@@ -106,7 +116,7 @@ export function classifyIntent(task, config = {}) {
|
|
|
106
116
|
if (!matchesKeywords(task, pattern.keywords)) continue;
|
|
107
117
|
|
|
108
118
|
if (pattern.confidence >= threshold) {
|
|
109
|
-
|
|
119
|
+
const result = {
|
|
110
120
|
classified: true,
|
|
111
121
|
taskType: pattern.taskType,
|
|
112
122
|
level: pattern.level,
|
|
@@ -114,6 +124,8 @@ export function classifyIntent(task, config = {}) {
|
|
|
114
124
|
patternId: pattern.id,
|
|
115
125
|
message: pattern.message,
|
|
116
126
|
};
|
|
127
|
+
if (pattern.hasFrontend) result.hasFrontend = true;
|
|
128
|
+
return result;
|
|
117
129
|
}
|
|
118
130
|
}
|
|
119
131
|
|
package/src/mcp/run-kj.js
CHANGED
|
@@ -42,6 +42,7 @@ export async function runKjCommand({ command, commandArgs = [], options = {}, en
|
|
|
42
42
|
normalizeBoolFlag(options.enableResearcher, "--enable-researcher", args);
|
|
43
43
|
normalizeBoolFlag(options.enableTester, "--enable-tester", args);
|
|
44
44
|
normalizeBoolFlag(options.enableSecurity, "--enable-security", args);
|
|
45
|
+
normalizeBoolFlag(options.enableImpeccable, "--enable-impeccable", args);
|
|
45
46
|
normalizeBoolFlag(options.enableTriage, "--enable-triage", args);
|
|
46
47
|
normalizeBoolFlag(options.enableDiscover, "--enable-discover", args);
|
|
47
48
|
normalizeBoolFlag(options.enableArchitect, "--enable-architect", args);
|
|
@@ -827,6 +827,7 @@ async function handleResume(a, server, extra) {
|
|
|
827
827
|
if (!a.sessionId) {
|
|
828
828
|
return failPayload("Missing required field: sessionId");
|
|
829
829
|
}
|
|
830
|
+
applySessionOverrides(a, ["coder", "reviewer", "tester", "security", "solomon", "enableTester", "enableSecurity", "enableImpeccable"]);
|
|
830
831
|
return handleResumeDirect(a, server, extra);
|
|
831
832
|
}
|
|
832
833
|
|
|
@@ -843,7 +844,7 @@ async function handleRun(a, server, extra) {
|
|
|
843
844
|
if (!isPreflightAcked()) {
|
|
844
845
|
return buildPreflightRequiredResponse("kj_run");
|
|
845
846
|
}
|
|
846
|
-
applySessionOverrides(a, ["coder", "reviewer", "tester", "security", "solomon", "enableTester", "enableSecurity"]);
|
|
847
|
+
applySessionOverrides(a, ["coder", "reviewer", "tester", "security", "solomon", "enableTester", "enableSecurity", "enableImpeccable"]);
|
|
847
848
|
return handleRunDirect(a, server, extra);
|
|
848
849
|
}
|
|
849
850
|
|
package/src/mcp/tools.js
CHANGED
|
@@ -69,6 +69,7 @@ export const tools = [
|
|
|
69
69
|
enableResearcher: { type: "boolean" },
|
|
70
70
|
enableTester: { type: "boolean" },
|
|
71
71
|
enableSecurity: { type: "boolean" },
|
|
72
|
+
enableImpeccable: { type: "boolean" },
|
|
72
73
|
enableTriage: { type: "boolean" },
|
|
73
74
|
enableDiscover: { type: "boolean" },
|
|
74
75
|
enableArchitect: { type: "boolean" },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TesterRole } from "../roles/tester-role.js";
|
|
2
2
|
import { SecurityRole } from "../roles/security-role.js";
|
|
3
|
+
import { ImpeccableRole } from "../roles/impeccable-role.js";
|
|
3
4
|
import { addCheckpoint, saveSession } from "../session-store.js";
|
|
4
5
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
5
6
|
import { invokeSolomon } from "./solomon-escalation.js";
|
|
@@ -163,3 +164,53 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
|
|
|
163
164
|
session.security_retry_count = 0;
|
|
164
165
|
return { action: "ok", stageResult: { ok: true, summary: securityOutput.summary || "No vulnerabilities found" } };
|
|
165
166
|
}
|
|
167
|
+
|
|
168
|
+
export async function runImpeccableStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, iteration, task, diff }) {
|
|
169
|
+
logger.setContext({ iteration, stage: "impeccable" });
|
|
170
|
+
emitProgress(
|
|
171
|
+
emitter,
|
|
172
|
+
makeEvent("impeccable:start", { ...eventBase, stage: "impeccable" }, {
|
|
173
|
+
message: "Impeccable auditing frontend design quality"
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const impeccable = new ImpeccableRole({ config, logger, emitter });
|
|
178
|
+
await impeccable.init({ task, iteration });
|
|
179
|
+
const impeccableStart = Date.now();
|
|
180
|
+
let impeccableOutput;
|
|
181
|
+
try {
|
|
182
|
+
impeccableOutput = await impeccable.run({ task, diff });
|
|
183
|
+
} catch (err) {
|
|
184
|
+
logger.warn(`Impeccable threw: ${err.message}`);
|
|
185
|
+
impeccableOutput = { ok: false, summary: `Impeccable error: ${err.message}`, result: { error: err.message } };
|
|
186
|
+
}
|
|
187
|
+
trackBudget({
|
|
188
|
+
role: "impeccable",
|
|
189
|
+
provider: config?.roles?.impeccable?.provider || coderRole.provider,
|
|
190
|
+
model: config?.roles?.impeccable?.model || coderRole.model,
|
|
191
|
+
result: impeccableOutput,
|
|
192
|
+
duration_ms: Date.now() - impeccableStart
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await addCheckpoint(session, {
|
|
196
|
+
stage: "impeccable",
|
|
197
|
+
iteration,
|
|
198
|
+
ok: impeccableOutput.ok,
|
|
199
|
+
provider: config?.roles?.impeccable?.provider || coderRole.provider,
|
|
200
|
+
model: config?.roles?.impeccable?.model || coderRole.model || null
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const verdict = impeccableOutput.result?.verdict || "APPROVED";
|
|
204
|
+
emitProgress(
|
|
205
|
+
emitter,
|
|
206
|
+
makeEvent("impeccable:end", { ...eventBase, stage: "impeccable" }, {
|
|
207
|
+
status: impeccableOutput.ok ? "ok" : "fail",
|
|
208
|
+
message: impeccableOutput.ok
|
|
209
|
+
? (verdict === "IMPROVED" ? "Impeccable applied design fixes" : "Impeccable audit passed")
|
|
210
|
+
: `Impeccable: ${impeccableOutput.summary}`
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Impeccable is advisory — failures do not block the pipeline
|
|
215
|
+
return { action: "ok", stageResult: { ok: impeccableOutput.ok, verdict, summary: impeccableOutput.summary || "No frontend design issues found" } };
|
|
216
|
+
}
|
|
@@ -11,7 +11,7 @@ import { parsePlannerOutput } from "../prompts/planner.js";
|
|
|
11
11
|
import { selectModelsForRoles } from "../utils/model-selector.js";
|
|
12
12
|
import { createStallDetector } from "../utils/stall-detector.js";
|
|
13
13
|
|
|
14
|
-
const ROLE_NAMES = ["planner", "researcher", "architect", "refactorer", "reviewer", "tester", "security"];
|
|
14
|
+
const ROLE_NAMES = ["planner", "researcher", "architect", "refactorer", "reviewer", "tester", "security", "impeccable"];
|
|
15
15
|
|
|
16
16
|
function buildRoleOverrides(recommendedRoles, pipelineConfig) {
|
|
17
17
|
const overrides = {};
|
|
@@ -8,7 +8,8 @@ const DEFAULT_RULES = {
|
|
|
8
8
|
max_stale_iterations: 3,
|
|
9
9
|
no_new_dependencies_without_task: true,
|
|
10
10
|
scope_guard: true,
|
|
11
|
-
reviewer_overreach: true
|
|
11
|
+
reviewer_overreach: true,
|
|
12
|
+
reviewer_style_block: true
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export function evaluateRules(context, rulesConfig = {}) {
|
|
@@ -71,6 +72,25 @@ export function evaluateRules(context, rulesConfig = {}) {
|
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
// Rule 6: Reviewer style-only block — all blocking issues are style/naming/formatting, not security/correctness
|
|
76
|
+
if (rules.reviewer_style_block && context.blockingIssues?.length > 0) {
|
|
77
|
+
const styleKeywords = /\b(naming|name|rename|style|format|formatting|indent|spacing|camelCase|snake_case|convention|cosmetic|readability|comment|jsdoc|documentation|whitespace|semicolon|quotes|trailing)\b/i;
|
|
78
|
+
const styleSeverities = new Set(["low", "minor"]);
|
|
79
|
+
const allStyle = context.blockingIssues.every(issue => {
|
|
80
|
+
const desc = issue.description || "";
|
|
81
|
+
const sev = (issue.severity || "").toLowerCase();
|
|
82
|
+
return styleSeverities.has(sev) || styleKeywords.test(desc);
|
|
83
|
+
});
|
|
84
|
+
if (allStyle) {
|
|
85
|
+
alerts.push({
|
|
86
|
+
rule: "reviewer_style_block",
|
|
87
|
+
severity: "critical",
|
|
88
|
+
message: `Reviewer blocked on ${context.blockingIssues.length} style-only issue(s). Style preferences should not block approval — escalating to Solomon mediation.`,
|
|
89
|
+
detail: { issueCount: context.blockingIssues.length, issues: context.blockingIssues.map(i => i.description) }
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
return {
|
|
75
95
|
alerts,
|
|
76
96
|
hasCritical: alerts.some(a => a.severity === "critical"),
|
|
@@ -81,7 +101,7 @@ export function evaluateRules(context, rulesConfig = {}) {
|
|
|
81
101
|
/**
|
|
82
102
|
* Build context for rules evaluation from git diff and session state.
|
|
83
103
|
*/
|
|
84
|
-
export async function buildRulesContext({ session, task, iteration }) {
|
|
104
|
+
export async function buildRulesContext({ session, task, iteration, blockingIssues }) {
|
|
85
105
|
const context = {
|
|
86
106
|
task,
|
|
87
107
|
iteration,
|
|
@@ -90,7 +110,8 @@ export async function buildRulesContext({ session, task, iteration }) {
|
|
|
90
110
|
newDependencies: [],
|
|
91
111
|
outOfScopeFiles: [],
|
|
92
112
|
reviewerDemotedCount: 0,
|
|
93
|
-
reviewerAutoApproved: false
|
|
113
|
+
reviewerAutoApproved: false,
|
|
114
|
+
blockingIssues: blockingIssues || []
|
|
94
115
|
};
|
|
95
116
|
|
|
96
117
|
// Count reviewer scope-filter demotions from session checkpoints
|
package/src/orchestrator.js
CHANGED
|
@@ -31,7 +31,7 @@ import { CoderRole } from "./roles/coder-role.js";
|
|
|
31
31
|
import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
|
|
32
32
|
import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage } from "./orchestrator/pre-loop-stages.js";
|
|
33
33
|
import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
|
|
34
|
-
import { runTesterStage, runSecurityStage } from "./orchestrator/post-loop-stages.js";
|
|
34
|
+
import { runTesterStage, runSecurityStage, runImpeccableStage } from "./orchestrator/post-loop-stages.js";
|
|
35
35
|
import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js";
|
|
36
36
|
|
|
37
37
|
|
|
@@ -44,6 +44,7 @@ function resolvePipelineFlags(config) {
|
|
|
44
44
|
researcherEnabled: Boolean(config.pipeline?.researcher?.enabled),
|
|
45
45
|
testerEnabled: Boolean(config.pipeline?.tester?.enabled),
|
|
46
46
|
securityEnabled: Boolean(config.pipeline?.security?.enabled),
|
|
47
|
+
impeccableEnabled: Boolean(config.pipeline?.impeccable?.enabled),
|
|
47
48
|
reviewerEnabled: config.pipeline?.reviewer?.enabled !== false,
|
|
48
49
|
discoverEnabled: Boolean(config.pipeline?.discover?.enabled),
|
|
49
50
|
architectEnabled: Boolean(config.pipeline?.architect?.enabled),
|
|
@@ -51,7 +52,7 @@ function resolvePipelineFlags(config) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
|
|
54
|
-
const { plannerEnabled, refactorerEnabled, researcherEnabled, testerEnabled, securityEnabled, reviewerEnabled, discoverEnabled, architectEnabled } = pipelineFlags;
|
|
55
|
+
const { plannerEnabled, refactorerEnabled, researcherEnabled, testerEnabled, securityEnabled, impeccableEnabled, reviewerEnabled, discoverEnabled, architectEnabled } = pipelineFlags;
|
|
55
56
|
const plannerRole = resolveRole(config, "planner");
|
|
56
57
|
const coderRole = resolveRole(config, "coder");
|
|
57
58
|
const reviewerRole = resolveRole(config, "reviewer");
|
|
@@ -84,6 +85,7 @@ async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
|
|
|
84
85
|
researcher_enabled: researcherEnabled,
|
|
85
86
|
tester_enabled: testerEnabled,
|
|
86
87
|
security_enabled: securityEnabled,
|
|
88
|
+
impeccable_enabled: impeccableEnabled,
|
|
87
89
|
solomon_enabled: Boolean(config.pipeline?.solomon?.enabled)
|
|
88
90
|
},
|
|
89
91
|
limits: {
|
|
@@ -203,7 +205,7 @@ async function markPgCardInProgress({ pgTaskId, pgProject, config, logger }) {
|
|
|
203
205
|
}
|
|
204
206
|
|
|
205
207
|
function applyTriageOverrides(pipelineFlags, roleOverrides) {
|
|
206
|
-
const keys = ["plannerEnabled", "researcherEnabled", "architectEnabled", "refactorerEnabled", "reviewerEnabled", "testerEnabled", "securityEnabled"];
|
|
208
|
+
const keys = ["plannerEnabled", "researcherEnabled", "architectEnabled", "refactorerEnabled", "reviewerEnabled", "testerEnabled", "securityEnabled", "impeccableEnabled"];
|
|
207
209
|
for (const key of keys) {
|
|
208
210
|
if (roleOverrides[key] !== undefined) {
|
|
209
211
|
pipelineFlags[key] = roleOverrides[key];
|
|
@@ -271,6 +273,7 @@ function applyFlagOverrides(pipelineFlags, flags) {
|
|
|
271
273
|
if (flags.enableReviewer !== undefined) pipelineFlags.reviewerEnabled = Boolean(flags.enableReviewer);
|
|
272
274
|
if (flags.enableTester !== undefined) pipelineFlags.testerEnabled = Boolean(flags.enableTester);
|
|
273
275
|
if (flags.enableSecurity !== undefined) pipelineFlags.securityEnabled = Boolean(flags.enableSecurity);
|
|
276
|
+
if (flags.enableImpeccable !== undefined) pipelineFlags.impeccableEnabled = Boolean(flags.enableImpeccable);
|
|
274
277
|
}
|
|
275
278
|
|
|
276
279
|
function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags }) {
|
|
@@ -531,12 +534,12 @@ async function handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emi
|
|
|
531
534
|
}
|
|
532
535
|
}
|
|
533
536
|
|
|
534
|
-
async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, becariaEnabled }) {
|
|
537
|
+
async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, becariaEnabled, blockingIssues }) {
|
|
535
538
|
if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
|
|
536
539
|
|
|
537
540
|
try {
|
|
538
541
|
const { evaluateRules, buildRulesContext } = await import("./orchestrator/solomon-rules.js");
|
|
539
|
-
const rulesContext = await buildRulesContext({ session, task, iteration: i });
|
|
542
|
+
const rulesContext = await buildRulesContext({ session, task, iteration: i, blockingIssues });
|
|
540
543
|
const rulesResult = evaluateRules(rulesContext, config.solomon?.rules);
|
|
541
544
|
|
|
542
545
|
if (rulesResult.alerts.length > 0) {
|
|
@@ -892,7 +895,7 @@ async function runGuardStages({ config, logger, emitter, eventBase, session, ite
|
|
|
892
895
|
return { action: "ok" };
|
|
893
896
|
}
|
|
894
897
|
|
|
895
|
-
async function runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults }) {
|
|
898
|
+
async function runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults, coderRole, pipelineFlags }) {
|
|
896
899
|
const tddResult = await runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration: i, askQuestion });
|
|
897
900
|
if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
|
|
898
901
|
if (tddResult.action === "continue") return { action: "continue" };
|
|
@@ -919,6 +922,17 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
|
|
|
919
922
|
}
|
|
920
923
|
}
|
|
921
924
|
|
|
925
|
+
if (pipelineFlags?.impeccableEnabled) {
|
|
926
|
+
const diff = await generateDiff({ baseRef: session.session_start_sha });
|
|
927
|
+
const impeccableResult = await runImpeccableStage({
|
|
928
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
929
|
+
iteration: i, task, diff
|
|
930
|
+
});
|
|
931
|
+
if (impeccableResult.stageResult) {
|
|
932
|
+
stageResults.impeccable = impeccableResult.stageResult;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
922
936
|
return { action: "ok" };
|
|
923
937
|
}
|
|
924
938
|
|
|
@@ -1071,7 +1085,7 @@ async function runSingleIteration(ctx) {
|
|
|
1071
1085
|
const guardResult = await runGuardStages({ config, logger, emitter, eventBase, session, iteration: i });
|
|
1072
1086
|
if (guardResult.action === "return") return guardResult;
|
|
1073
1087
|
|
|
1074
|
-
const qgResult = await runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults });
|
|
1088
|
+
const qgResult = await runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults, coderRole, pipelineFlags });
|
|
1075
1089
|
if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
|
|
1076
1090
|
|
|
1077
1091
|
await handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i });
|
|
@@ -1086,7 +1100,7 @@ async function runSingleIteration(ctx) {
|
|
|
1086
1100
|
}));
|
|
1087
1101
|
session.standby_retry_count = 0;
|
|
1088
1102
|
|
|
1089
|
-
const solomonResult = await handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, becariaEnabled });
|
|
1103
|
+
const solomonResult = await handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, becariaEnabled, blockingIssues: review?.blocking_issues });
|
|
1090
1104
|
if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
|
|
1091
1105
|
|
|
1092
1106
|
await handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger });
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { BaseRole } from "./base-role.js";
|
|
2
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
|
|
4
|
+
const SUBAGENT_PREAMBLE = [
|
|
5
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
6
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
7
|
+
"Do NOT use any MCP tools. Focus only on auditing frontend/UI code for design quality."
|
|
8
|
+
].join(" ");
|
|
9
|
+
|
|
10
|
+
function resolveProvider(config) {
|
|
11
|
+
return (
|
|
12
|
+
config?.roles?.impeccable?.provider ||
|
|
13
|
+
config?.roles?.coder?.provider ||
|
|
14
|
+
"claude"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildPrompt({ task, diff, instructions }) {
|
|
19
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
20
|
+
|
|
21
|
+
if (instructions) {
|
|
22
|
+
sections.push(instructions);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
sections.push(
|
|
26
|
+
`## Task\n${task}`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (diff) {
|
|
30
|
+
sections.push(`## Git diff to audit\n${diff}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return sections.join("\n\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseImpeccableOutput(raw) {
|
|
37
|
+
const text = raw?.trim() || "";
|
|
38
|
+
const jsonMatch = /\{[\s\S]*\}/.exec(text);
|
|
39
|
+
if (!jsonMatch) return null;
|
|
40
|
+
return JSON.parse(jsonMatch[0]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildSummary(parsed) {
|
|
44
|
+
const verdict = parsed.verdict || "APPROVED";
|
|
45
|
+
const found = parsed.issuesFound || 0;
|
|
46
|
+
const fixed = parsed.issuesFixed || 0;
|
|
47
|
+
|
|
48
|
+
if (verdict === "APPROVED" || found === 0) {
|
|
49
|
+
return `Verdict: APPROVED; No frontend design issues found`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const cats = parsed.categories || {};
|
|
53
|
+
const parts = Object.entries(cats)
|
|
54
|
+
.filter(([, count]) => count > 0)
|
|
55
|
+
.map(([cat, count]) => `${count} ${cat}`);
|
|
56
|
+
|
|
57
|
+
return `Verdict: ${verdict}; ${found} issue(s) found, ${fixed} fixed (${parts.join(", ")})`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ImpeccableRole extends BaseRole {
|
|
61
|
+
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
62
|
+
super({ name: "impeccable", config, logger, emitter });
|
|
63
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async execute(input) {
|
|
67
|
+
const { task, diff } = typeof input === "string"
|
|
68
|
+
? { task: input, diff: null }
|
|
69
|
+
: { task: input?.task || this.context?.task || "", diff: input?.diff || null };
|
|
70
|
+
|
|
71
|
+
const provider = resolveProvider(this.config);
|
|
72
|
+
const agent = this._createAgent(provider, this.config, this.logger);
|
|
73
|
+
|
|
74
|
+
const prompt = buildPrompt({ task, diff, instructions: this.instructions });
|
|
75
|
+
const result = await agent.runTask({ prompt, role: "impeccable" });
|
|
76
|
+
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
result: {
|
|
81
|
+
error: result.error || result.output || "Impeccable audit failed",
|
|
82
|
+
provider
|
|
83
|
+
},
|
|
84
|
+
summary: `Impeccable audit failed: ${result.error || "unknown error"}`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const parsed = parseImpeccableOutput(result.output);
|
|
90
|
+
if (!parsed) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
result: { error: "Failed to parse impeccable output: no JSON found", provider },
|
|
94
|
+
summary: "Impeccable output parse error: no JSON found"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const verdict = parsed.verdict || (parsed.issuesFound > 0 ? "IMPROVED" : "APPROVED");
|
|
99
|
+
const ok = verdict === "APPROVED" || verdict === "IMPROVED";
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
ok,
|
|
103
|
+
result: {
|
|
104
|
+
verdict,
|
|
105
|
+
issuesFound: parsed.issuesFound || 0,
|
|
106
|
+
issuesFixed: parsed.issuesFixed || 0,
|
|
107
|
+
categories: parsed.categories || {},
|
|
108
|
+
changes: parsed.changes || [],
|
|
109
|
+
provider
|
|
110
|
+
},
|
|
111
|
+
summary: buildSummary({ ...parsed, verdict })
|
|
112
|
+
};
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
result: { error: `Failed to parse impeccable output: ${err.message}`, provider },
|
|
117
|
+
summary: `Impeccable output parse error: ${err.message}`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/roles/triage-role.js
CHANGED
|
@@ -4,7 +4,7 @@ import { buildTriagePrompt } from "../prompts/triage.js";
|
|
|
4
4
|
import { VALID_TASK_TYPES } from "../guards/policy-resolver.js";
|
|
5
5
|
|
|
6
6
|
const VALID_LEVELS = new Set(["trivial", "simple", "medium", "complex"]);
|
|
7
|
-
const VALID_ROLES = new Set(["planner", "researcher", "refactorer", "reviewer", "tester", "security"]);
|
|
7
|
+
const VALID_ROLES = new Set(["planner", "researcher", "refactorer", "reviewer", "tester", "security", "impeccable"]);
|
|
8
8
|
const FALLBACK_TASK_TYPE = "sw";
|
|
9
9
|
|
|
10
10
|
function resolveProvider(config) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Impeccable Role
|
|
2
|
+
|
|
3
|
+
You are the **Impeccable Design Auditor** in a multi-role AI pipeline. You run after SonarQube and before the reviewer. Your job is to audit changed UI/frontend files for design quality issues and apply fixes automatically.
|
|
4
|
+
|
|
5
|
+
## Scope constraint
|
|
6
|
+
|
|
7
|
+
- **ONLY audit and fix files present in the diff.** Do not touch files that were not changed.
|
|
8
|
+
- If no frontend files (.html, .css, .astro, .jsx, .tsx, .vue, .svelte, .lit, .js with DOM manipulation) are in the diff, report APPROVED immediately with 0 issues.
|
|
9
|
+
|
|
10
|
+
## Input
|
|
11
|
+
|
|
12
|
+
- **Task**: {{task}}
|
|
13
|
+
- **Diff**: {{diff}}
|
|
14
|
+
- **Context**: {{context}}
|
|
15
|
+
|
|
16
|
+
## Phase 1 — Audit
|
|
17
|
+
|
|
18
|
+
Analyze all changed files in the diff that are frontend-related. Run these checks systematically:
|
|
19
|
+
|
|
20
|
+
### 1. Accessibility (a11y)
|
|
21
|
+
- Missing ARIA labels on interactive elements
|
|
22
|
+
- No `focus-visible` styles on focusable elements
|
|
23
|
+
- Missing `alt` text on images
|
|
24
|
+
- Non-semantic HTML (e.g. `<div>` used as buttons instead of `<button>`)
|
|
25
|
+
- Missing skip links for navigation
|
|
26
|
+
- Keyboard traps (focus cannot leave a component)
|
|
27
|
+
- Insufficient color contrast
|
|
28
|
+
|
|
29
|
+
### 2. Performance
|
|
30
|
+
- Render-blocking resources (synchronous scripts in `<head>`)
|
|
31
|
+
- Missing `loading="lazy"` on below-fold images
|
|
32
|
+
- Animating layout properties (`width`, `height`, `top`, `left`) instead of `transform`/`opacity`
|
|
33
|
+
- Missing image dimensions (`width`/`height` attributes) causing CLS
|
|
34
|
+
- No `prefers-reduced-motion` support for animations
|
|
35
|
+
|
|
36
|
+
### 3. Theming
|
|
37
|
+
- Hard-coded colors not using design tokens or CSS custom properties
|
|
38
|
+
- Broken dark mode (elements invisible or unreadable in dark theme)
|
|
39
|
+
- Inconsistent token usage across the same component
|
|
40
|
+
|
|
41
|
+
### 4. Responsive
|
|
42
|
+
- Fixed widths (`width: 500px`) that break on mobile viewports
|
|
43
|
+
- Touch targets smaller than 44×44px
|
|
44
|
+
- Horizontal scroll on narrow viewports (< 375px)
|
|
45
|
+
- Text that does not scale with user font-size preferences
|
|
46
|
+
|
|
47
|
+
### 5. Anti-patterns
|
|
48
|
+
- AI slop tells: gratuitous gradient text, excessive card grids, bounce animations, glassmorphism overuse
|
|
49
|
+
- Gray text on colored backgrounds (poor readability)
|
|
50
|
+
- Deeply nested cards (card inside card inside card)
|
|
51
|
+
- Generic fallback fonts without a proper font stack
|
|
52
|
+
|
|
53
|
+
## Phase 2 — Fix
|
|
54
|
+
|
|
55
|
+
For each issue found in Phase 1, apply the fix directly. Use the **Edit** tool for targeted changes — never use Write to overwrite entire files.
|
|
56
|
+
|
|
57
|
+
### Priority order
|
|
58
|
+
1. **Critical a11y** — keyboard accessibility, ARIA attributes, semantic HTML
|
|
59
|
+
2. **Performance** — CLS fixes, render-blocking resources
|
|
60
|
+
3. **Theming** — design token consistency, dark mode
|
|
61
|
+
4. **Responsive** — viewport, touch targets, scaling
|
|
62
|
+
5. **Anti-pattern cleanup** — slop removal, readability
|
|
63
|
+
|
|
64
|
+
### Rules
|
|
65
|
+
- Each fix must be minimal and targeted (Edit, not Write)
|
|
66
|
+
- Only use Read, Edit, Grep, Glob, and Bash tools
|
|
67
|
+
- Verify each fix with `git diff` to confirm only intended lines changed
|
|
68
|
+
- If a fix would require changes outside the diff, skip it and note it in the report
|
|
69
|
+
|
|
70
|
+
## Phase 3 — Report
|
|
71
|
+
|
|
72
|
+
Output a strict JSON object:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"ok": true,
|
|
77
|
+
"result": {
|
|
78
|
+
"verdict": "APPROVED",
|
|
79
|
+
"issuesFound": 0,
|
|
80
|
+
"issuesFixed": 0,
|
|
81
|
+
"categories": {
|
|
82
|
+
"a11y": 0,
|
|
83
|
+
"performance": 0,
|
|
84
|
+
"theming": 0,
|
|
85
|
+
"responsive": 0,
|
|
86
|
+
"antiPatterns": 0
|
|
87
|
+
},
|
|
88
|
+
"changes": []
|
|
89
|
+
},
|
|
90
|
+
"summary": "No frontend design issues found"
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
When issues are found and fixed:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"ok": true,
|
|
99
|
+
"result": {
|
|
100
|
+
"verdict": "IMPROVED",
|
|
101
|
+
"issuesFound": 3,
|
|
102
|
+
"issuesFixed": 3,
|
|
103
|
+
"categories": {
|
|
104
|
+
"a11y": 2,
|
|
105
|
+
"performance": 1,
|
|
106
|
+
"theming": 0,
|
|
107
|
+
"responsive": 0,
|
|
108
|
+
"antiPatterns": 0
|
|
109
|
+
},
|
|
110
|
+
"changes": [
|
|
111
|
+
{
|
|
112
|
+
"file": "src/components/Button.astro",
|
|
113
|
+
"issue": "Non-semantic div used as button",
|
|
114
|
+
"fix": "Replaced <div onclick> with <button>",
|
|
115
|
+
"category": "a11y"
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"summary": "3 design issues found and fixed (2 a11y, 1 performance)"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Verdict rules
|
|
124
|
+
- **APPROVED** — No frontend design issues found (issuesFound === 0)
|
|
125
|
+
- **IMPROVED** — Issues were found and fixes were applied (issuesFixed > 0)
|
|
@@ -9,7 +9,7 @@ Return a single valid JSON object and nothing else:
|
|
|
9
9
|
{
|
|
10
10
|
"level": "trivial|simple|medium|complex",
|
|
11
11
|
"taskType": "sw|infra|doc|add-tests|refactor",
|
|
12
|
-
"roles": ["planner", "researcher", "refactorer", "reviewer", "tester", "security"],
|
|
12
|
+
"roles": ["planner", "researcher", "refactorer", "reviewer", "tester", "security", "impeccable"],
|
|
13
13
|
"reasoning": "brief practical justification",
|
|
14
14
|
"shouldDecompose": false,
|
|
15
15
|
"subtasks": []
|
|
@@ -41,6 +41,13 @@ When `shouldDecompose` is true, provide `subtasks`: an array of 2-5 short string
|
|
|
41
41
|
|
|
42
42
|
When `shouldDecompose` is false, `subtasks` must be an empty array.
|
|
43
43
|
|
|
44
|
+
## Frontend detection
|
|
45
|
+
If the task involves frontend/UI work, include `"impeccable"` in `roles`. Detect frontend tasks by:
|
|
46
|
+
- **File extensions**: .html, .css, .astro, .jsx, .tsx, .vue, .svelte
|
|
47
|
+
- **Keywords in description**: UI, landing, component, responsive, accessibility, a11y, frontend, design, layout, styling, dark mode, animation, CSS, HTML
|
|
48
|
+
|
|
49
|
+
The `impeccable` role audits and fixes frontend design quality (a11y, performance, theming, responsive, anti-patterns).
|
|
50
|
+
|
|
44
51
|
## Rules
|
|
45
52
|
- Keep `reasoning` short.
|
|
46
53
|
- Recommend only roles that add clear value.
|