karajan-code 1.23.0 → 1.24.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
package/src/cli.js CHANGED
@@ -75,6 +75,7 @@ program
75
75
  .option("--enable-researcher")
76
76
  .option("--enable-tester")
77
77
  .option("--enable-security")
78
+ .option("--enable-impeccable")
78
79
  .option("--enable-triage")
79
80
  .option("--enable-discover")
80
81
  .option("--enable-architect")
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
- return {
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 = {};
@@ -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 }) {
@@ -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 });
@@ -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
+ }
@@ -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.