@bastani/atomic 0.5.25 → 0.5.26-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.
Files changed (51) hide show
  1. package/.agents/skills/ado-commit/SKILL.md +92 -0
  2. package/.agents/skills/ado-create-pr/SKILL.md +209 -0
  3. package/.claude/settings.json +1 -0
  4. package/.mcp.json +5 -0
  5. package/.opencode/opencode.json +7 -1
  6. package/README.md +150 -116
  7. package/assets/settings.schema.json +2 -2
  8. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  9. package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts +46 -0
  10. package/dist/sdk/workflows/builtin/open-claude-design/claude/index.d.ts.map +1 -0
  11. package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts +34 -0
  12. package/dist/sdk/workflows/builtin/open-claude-design/copilot/index.d.ts.map +1 -0
  13. package/dist/sdk/workflows/builtin/open-claude-design/helpers/constants.d.ts +72 -0
  14. package/dist/sdk/workflows/builtin/open-claude-design/helpers/constants.d.ts.map +1 -0
  15. package/dist/sdk/workflows/builtin/open-claude-design/helpers/design-system.d.ts +46 -0
  16. package/dist/sdk/workflows/builtin/open-claude-design/helpers/design-system.d.ts.map +1 -0
  17. package/dist/sdk/workflows/builtin/open-claude-design/helpers/export.d.ts +32 -0
  18. package/dist/sdk/workflows/builtin/open-claude-design/helpers/export.d.ts.map +1 -0
  19. package/dist/sdk/workflows/builtin/open-claude-design/helpers/import.d.ts +33 -0
  20. package/dist/sdk/workflows/builtin/open-claude-design/helpers/import.d.ts.map +1 -0
  21. package/dist/sdk/workflows/builtin/open-claude-design/helpers/prompts.d.ts +106 -0
  22. package/dist/sdk/workflows/builtin/open-claude-design/helpers/prompts.d.ts.map +1 -0
  23. package/dist/sdk/workflows/builtin/open-claude-design/helpers/scan.d.ts +50 -0
  24. package/dist/sdk/workflows/builtin/open-claude-design/helpers/scan.d.ts.map +1 -0
  25. package/dist/sdk/workflows/builtin/open-claude-design/helpers/validation.d.ts +12 -0
  26. package/dist/sdk/workflows/builtin/open-claude-design/helpers/validation.d.ts.map +1 -0
  27. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts +36 -0
  28. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -0
  29. package/dist/services/config/atomic-config.d.ts +6 -0
  30. package/dist/services/config/atomic-config.d.ts.map +1 -1
  31. package/dist/services/config/scm-sync.d.ts +37 -0
  32. package/dist/services/config/scm-sync.d.ts.map +1 -0
  33. package/package.json +7 -7
  34. package/src/cli.ts +2 -2
  35. package/src/commands/cli/chat/index.ts +8 -1
  36. package/src/commands/cli/config.ts +34 -10
  37. package/src/commands/cli/init/index.ts +9 -0
  38. package/src/sdk/runtime/executor.ts +13 -0
  39. package/src/sdk/workflows/builtin/open-claude-design/claude/index.ts +499 -0
  40. package/src/sdk/workflows/builtin/open-claude-design/copilot/index.ts +507 -0
  41. package/src/sdk/workflows/builtin/open-claude-design/helpers/constants.ts +159 -0
  42. package/src/sdk/workflows/builtin/open-claude-design/helpers/design-system.ts +88 -0
  43. package/src/sdk/workflows/builtin/open-claude-design/helpers/export.ts +193 -0
  44. package/src/sdk/workflows/builtin/open-claude-design/helpers/import.ts +52 -0
  45. package/src/sdk/workflows/builtin/open-claude-design/helpers/prompts.ts +1110 -0
  46. package/src/sdk/workflows/builtin/open-claude-design/helpers/scan.ts +117 -0
  47. package/src/sdk/workflows/builtin/open-claude-design/helpers/validation.ts +38 -0
  48. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +570 -0
  49. package/src/services/config/atomic-config.ts +12 -0
  50. package/src/services/config/scm-sync.ts +174 -0
  51. package/src/services/config/settings.ts +19 -0
@@ -0,0 +1,507 @@
1
+ /**
2
+ * open-claude-design / copilot
3
+ *
4
+ * Copilot replica of the Claude open-claude-design workflow. Orchestrates
5
+ * the design skill ecosystem (impeccable, critique, shape, polish, audit,
6
+ * etc.) into a deterministic 5-phase pipeline:
7
+ *
8
+ * Phase 1: Design System Onboarding — extract tokens, build Design.md (HIL)
9
+ * Phase 2: Import — aggregate text/URL/file references
10
+ * Phase 3: Generation — produce first design version
11
+ * Phase 4: Refinement Loop — bounded iterate with critique + user feedback
12
+ * Phase 5: Export/Handoff — HTML export + Claude Code handoff bundle
13
+ *
14
+ * Copilot-specific concerns (see references/failure-modes.md):
15
+ *
16
+ * • F5 — every `ctx.stage()` is a FRESH session. Every specialist receives
17
+ * its required context verbatim in its first prompt.
18
+ * • F1 — Copilot's last assistant turn is often empty when the agent ends
19
+ * on a tool call. Use `getAssistantText()` (concatenation of every
20
+ * top-level non-empty assistant turn, ignoring sub-agent
21
+ * `parentToolCallId` traffic) instead of `.at(-1).data.content`.
22
+ * • F9 — `s.save()` receives `SessionEvent[]` from `s.session.getMessages()`.
23
+ *
24
+ * Sub-agents are dispatched via `sessionOpts.agent` (the Copilot-native way
25
+ * to bind a session to a single named sub-agent). Permissions default to
26
+ * `approveAll` so headless stages run unattended.
27
+ *
28
+ * See claude/index.ts for the full topology diagram and design rationale.
29
+ *
30
+ * Run: atomic workflow -n open-claude-design -a copilot "design a dashboard"
31
+ */
32
+
33
+ import { defineWorkflow } from "../../../index.ts";
34
+ import type { SessionEvent } from "@github/copilot-sdk";
35
+ import { mkdir } from "node:fs/promises";
36
+ import path from "node:path";
37
+
38
+ import { MAX_REFINEMENTS, DESIGNS_DIR } from "../helpers/constants.ts";
39
+ import {
40
+ loadDesignSystem,
41
+ persistDesignSystem,
42
+ readImpeccableMd,
43
+ slugifyPrompt,
44
+ ensureScratchDir,
45
+ } from "../helpers/design-system.ts";
46
+ import {
47
+ isUrl,
48
+ isFilePath,
49
+ aggregateImportResults,
50
+ } from "../helpers/import.ts";
51
+ import { isRefinementComplete } from "../helpers/validation.ts";
52
+ import { writeHandoffBundle } from "../helpers/export.ts";
53
+ import {
54
+ hasBlockingFindings,
55
+ renderScanFindings,
56
+ runImpeccableScan,
57
+ } from "../helpers/scan.ts";
58
+ import {
59
+ buildDesignLocatorPrompt,
60
+ buildDesignAnalyzerPrompt,
61
+ buildDesignPatternPrompt,
62
+ buildDesignSystemBuilderPrompt,
63
+ buildWebCapturePrompt,
64
+ buildFileParserPrompt,
65
+ buildGeneratorPrompt,
66
+ buildRefineFeedbackPrompt,
67
+ buildCritiquePrompt,
68
+ buildScreenshotValidationPrompt,
69
+ buildApplyChangesPrompt,
70
+ buildForcedFixPrompt,
71
+ buildExportPrompt,
72
+ } from "../helpers/prompts.ts";
73
+
74
+ /**
75
+ * Concatenate every top-level assistant turn's non-empty content. The final
76
+ * `assistant.message` of a Copilot turn is often empty when the agent ends
77
+ * on a tool call (F1), and sub-agent traffic is signalled by `parentToolCallId`.
78
+ */
79
+ function getAssistantText(messages: SessionEvent[]): string {
80
+ return messages
81
+ .filter(
82
+ (m): m is Extract<SessionEvent, { type: "assistant.message" }> =>
83
+ m.type === "assistant.message" && !m.data.parentToolCallId,
84
+ )
85
+ .map((m) => m.data.content)
86
+ .filter((c) => c.length > 0)
87
+ .join("\n\n");
88
+ }
89
+
90
+ export default defineWorkflow({
91
+ name: "open-claude-design",
92
+ description:
93
+ "AI-powered design workflow: design system onboarding → import → generate → refine → export/handoff",
94
+ inputs: [
95
+ {
96
+ name: "prompt",
97
+ type: "text",
98
+ required: true,
99
+ description:
100
+ "What to design (e.g., 'a dashboard for monitoring API latency')",
101
+ },
102
+ {
103
+ name: "reference",
104
+ type: "text",
105
+ required: false,
106
+ description:
107
+ "URL, file path, or codebase path to import as design reference",
108
+ },
109
+ {
110
+ name: "output-type",
111
+ type: "enum",
112
+ required: false,
113
+ values: ["prototype", "wireframe", "page", "component"],
114
+ default: "prototype",
115
+ description: "Type of design output to generate",
116
+ },
117
+ {
118
+ name: "design-system",
119
+ type: "text",
120
+ required: false,
121
+ description: "Path to existing Design.md (skips onboarding if provided)",
122
+ },
123
+ ],
124
+ })
125
+ .for<"copilot">()
126
+ .run(async (ctx) => {
127
+ const prompt = ctx.inputs.prompt ?? "";
128
+ const reference = ctx.inputs.reference ?? "";
129
+ const outputType = ctx.inputs["output-type"] ?? "prototype";
130
+ const designSystemPath = ctx.inputs["design-system"] ?? "";
131
+
132
+ const root = process.cwd();
133
+ const slug = slugifyPrompt(prompt);
134
+ const isoDate = new Date().toISOString().slice(0, 10);
135
+ const scratchDir = await ensureScratchDir(root);
136
+ const designDir = path.join(scratchDir, slug);
137
+ await mkdir(designDir, { recursive: true });
138
+
139
+ // ══════════════════════════════════════════════════════════════════════
140
+ // PHASE 1: Design System Onboarding
141
+ // ══════════════════════════════════════════════════════════════════════
142
+
143
+ let designSystem;
144
+
145
+ if (designSystemPath.trim()) {
146
+ // Skip onboarding — user provided an existing Design.md
147
+ designSystem = await loadDesignSystem(designSystemPath);
148
+ } else {
149
+ // Layer 1: Parallel headless codebase analysis
150
+ const [locator, analyzer, patterns] = await Promise.all([
151
+ ctx.stage(
152
+ {
153
+ name: "ds-locator",
154
+ headless: true,
155
+ description: "Locate design files and tokens",
156
+ },
157
+ {},
158
+ { agent: "codebase-locator" },
159
+ async (s) => {
160
+ await s.session.send({
161
+ prompt: buildDesignLocatorPrompt({ root }),
162
+ });
163
+ const messages = await s.session.getMessages();
164
+ s.save(messages);
165
+ return getAssistantText(messages);
166
+ },
167
+ ),
168
+ ctx.stage(
169
+ {
170
+ name: "ds-analyzer",
171
+ headless: true,
172
+ description: "Analyze design tokens and patterns",
173
+ },
174
+ {},
175
+ { agent: "codebase-analyzer" },
176
+ async (s) => {
177
+ await s.session.send({
178
+ prompt: buildDesignAnalyzerPrompt({ root }),
179
+ });
180
+ const messages = await s.session.getMessages();
181
+ s.save(messages);
182
+ return getAssistantText(messages);
183
+ },
184
+ ),
185
+ ctx.stage(
186
+ {
187
+ name: "ds-patterns",
188
+ headless: true,
189
+ description: "Find existing design patterns",
190
+ },
191
+ {},
192
+ { agent: "codebase-pattern-finder" },
193
+ async (s) => {
194
+ await s.session.send({
195
+ prompt: buildDesignPatternPrompt({ root }),
196
+ });
197
+ const messages = await s.session.getMessages();
198
+ s.save(messages);
199
+ return getAssistantText(messages);
200
+ },
201
+ ),
202
+ ]);
203
+
204
+ // Layer 2: Visible stage with HIL — presents findings, asks user to
205
+ // approve/modify each design element category
206
+ await ctx.stage(
207
+ {
208
+ name: "design-system-builder",
209
+ description: "Build design system with user approval (HIL)",
210
+ },
211
+ {},
212
+ {},
213
+ async (s) => {
214
+ await s.session.send({
215
+ prompt: buildDesignSystemBuilderPrompt({
216
+ root,
217
+ locatorOutput: locator.result,
218
+ analyzerOutput: analyzer.result,
219
+ patternsOutput: patterns.result,
220
+ existingImpeccable: await readImpeccableMd(root),
221
+ }),
222
+ });
223
+ s.save(await s.session.getMessages());
224
+ },
225
+ );
226
+
227
+ // Deterministic: read back the Design.md the agent wrote
228
+ designSystem = await persistDesignSystem(root);
229
+ }
230
+
231
+ // ══════════════════════════════════════════════════════════════════════
232
+ // PHASE 2: Import
233
+ // ══════════════════════════════════════════════════════════════════════
234
+
235
+ const importResults = await Promise.all([
236
+ // Web capture (only if reference is a URL)
237
+ isUrl(reference)
238
+ ? ctx.stage(
239
+ {
240
+ name: "web-capture",
241
+ headless: true,
242
+ description: "Capture web reference via playwright",
243
+ },
244
+ {},
245
+ { agent: "codebase-online-researcher" },
246
+ async (s) => {
247
+ await s.session.send({
248
+ prompt: buildWebCapturePrompt({
249
+ url: reference,
250
+ screenshotDir: scratchDir,
251
+ }),
252
+ });
253
+ const messages = await s.session.getMessages();
254
+ s.save(messages);
255
+ return getAssistantText(messages);
256
+ },
257
+ )
258
+ : null,
259
+
260
+ // File parser (only if reference is a file path)
261
+ isFilePath(reference)
262
+ ? ctx.stage(
263
+ {
264
+ name: "file-parser",
265
+ headless: true,
266
+ description: "Parse reference document",
267
+ },
268
+ {},
269
+ { agent: "codebase-analyzer" },
270
+ async (s) => {
271
+ await s.session.send({
272
+ prompt: buildFileParserPrompt({ filePath: reference }),
273
+ });
274
+ const messages = await s.session.getMessages();
275
+ s.save(messages);
276
+ return getAssistantText(messages);
277
+ },
278
+ )
279
+ : null,
280
+ ]);
281
+
282
+ // Deterministic aggregation
283
+ const importContext = aggregateImportResults({
284
+ prompt,
285
+ reference,
286
+ webCapture: importResults[0]?.result ?? null,
287
+ fileParse: importResults[1]?.result ?? null,
288
+ });
289
+
290
+ // ══════════════════════════════════════════════════════════════════════
291
+ // PHASE 3: Generation
292
+ // ══════════════════════════════════════════════════════════════════════
293
+
294
+ await ctx.stage(
295
+ { name: "generator", description: "Generate first design version" },
296
+ {},
297
+ {},
298
+ async (s) => {
299
+ await s.session.send({
300
+ prompt: buildGeneratorPrompt({
301
+ prompt,
302
+ outputType,
303
+ designSystem,
304
+ importContext,
305
+ root,
306
+ outputDir: designDir,
307
+ }),
308
+ });
309
+ s.save(await s.session.getMessages());
310
+ },
311
+ );
312
+
313
+ // ══════════════════════════════════════════════════════════════════════
314
+ // PHASE 4: Refinement Loop
315
+ // ══════════════════════════════════════════════════════════════════════
316
+
317
+ for (let iteration = 1; iteration <= MAX_REFINEMENTS; iteration++) {
318
+ // Step 1: Collect user feedback via HIL
319
+ const feedback = await ctx.stage(
320
+ {
321
+ name: `user-feedback-${iteration}`,
322
+ description: `Collect refinement feedback (iteration ${iteration})`,
323
+ },
324
+ {},
325
+ {},
326
+ async (s) => {
327
+ await s.session.send({
328
+ prompt: buildRefineFeedbackPrompt({
329
+ prompt,
330
+ designDir,
331
+ iteration,
332
+ maxIterations: MAX_REFINEMENTS,
333
+ }),
334
+ });
335
+ const messages = await s.session.getMessages();
336
+ s.save(messages);
337
+ return getAssistantText(messages);
338
+ },
339
+ );
340
+
341
+ // Check if user signaled "done" via HIL response
342
+ if (isRefinementComplete(feedback.result)) break;
343
+
344
+ // Step 2: Parallel validation — critique + screenshot
345
+ const [critiqueResult, screenshotResult] = await Promise.all([
346
+ ctx.stage(
347
+ {
348
+ name: `critique-${iteration}`,
349
+ headless: true,
350
+ description: `Design critique (iteration ${iteration})`,
351
+ },
352
+ {},
353
+ { agent: "reviewer" },
354
+ async (s) => {
355
+ await s.session.send({
356
+ prompt: buildCritiquePrompt({
357
+ designDir,
358
+ designSystem,
359
+ userFeedback: feedback.result,
360
+ }),
361
+ });
362
+ const messages = await s.session.getMessages();
363
+ s.save(messages);
364
+ return getAssistantText(messages);
365
+ },
366
+ ),
367
+ ctx.stage(
368
+ {
369
+ name: `screenshot-${iteration}`,
370
+ headless: true,
371
+ description: `Visual validation (iteration ${iteration})`,
372
+ },
373
+ {},
374
+ { agent: "codebase-analyzer" },
375
+ async (s) => {
376
+ await s.session.send({
377
+ prompt: buildScreenshotValidationPrompt({
378
+ designDir,
379
+ scratchDir,
380
+ }),
381
+ });
382
+ const messages = await s.session.getMessages();
383
+ s.save(messages);
384
+ return getAssistantText(messages);
385
+ },
386
+ ),
387
+ ]);
388
+
389
+ // Step 3: Deterministic scan — surface banned anti-patterns so the
390
+ // apply-changes stage can fix them alongside user feedback. No LLM
391
+ // call; runs the `impeccable detect` CLI directly.
392
+ const scan = await runImpeccableScan(designDir);
393
+ const scanFindings =
394
+ scan.available && scan.findings.length > 0
395
+ ? renderScanFindings(scan.findings)
396
+ : scan.available
397
+ ? ""
398
+ : `(scanner unavailable: ${scan.reason} — proceed without scan input)`;
399
+
400
+ // Step 4: Apply changes based on feedback + critique + scanner findings
401
+ await ctx.stage(
402
+ {
403
+ name: `apply-changes-${iteration}`,
404
+ description: `Apply refinements (iteration ${iteration})`,
405
+ },
406
+ {},
407
+ {},
408
+ async (s) => {
409
+ await s.session.send({
410
+ prompt: buildApplyChangesPrompt({
411
+ prompt,
412
+ designDir,
413
+ designSystem,
414
+ userFeedback: feedback.result,
415
+ critiqueOutput: critiqueResult.result,
416
+ screenshotOutput: screenshotResult.result,
417
+ scanFindings,
418
+ iteration,
419
+ }),
420
+ });
421
+ s.save(await s.session.getMessages());
422
+ },
423
+ );
424
+ }
425
+
426
+ // ══════════════════════════════════════════════════════════════════════
427
+ // Hard enforcement gate — runs before export, independent of the
428
+ // refinement loop's exit condition. Guarantees no design ships with
429
+ // scanner findings even if the user approved early or MAX_REFINEMENTS
430
+ // was reached with the agent still introducing banned patterns.
431
+ // ══════════════════════════════════════════════════════════════════════
432
+
433
+ const preExportScan = await runImpeccableScan(designDir);
434
+ if (hasBlockingFindings(preExportScan)) {
435
+ const findings = (
436
+ preExportScan as Extract<typeof preExportScan, { available: true }>
437
+ ).findings;
438
+ const findingsText = renderScanFindings(findings);
439
+
440
+ await ctx.stage(
441
+ {
442
+ name: "forced-fix",
443
+ description: "Remove banned anti-patterns before export",
444
+ },
445
+ {},
446
+ {},
447
+ async (s) => {
448
+ await s.session.send({
449
+ prompt: buildForcedFixPrompt({
450
+ designDir,
451
+ designSystem,
452
+ scanFindings: findingsText,
453
+ }),
454
+ });
455
+ s.save(await s.session.getMessages());
456
+ },
457
+ );
458
+
459
+ const rescan = await runImpeccableScan(designDir);
460
+ if (hasBlockingFindings(rescan)) {
461
+ const remaining = (
462
+ rescan as Extract<typeof rescan, { available: true }>
463
+ ).findings;
464
+ throw new Error(
465
+ `open-claude-design: export blocked — ${remaining.length} ` +
466
+ `banned anti-pattern(s) remain after forced fix:\n` +
467
+ renderScanFindings(remaining),
468
+ );
469
+ }
470
+ }
471
+
472
+ // ══════════════════════════════════════════════════════════════════════
473
+ // PHASE 5: Export / Handoff
474
+ // ══════════════════════════════════════════════════════════════════════
475
+
476
+ const finalDesignDir = path.join(root, DESIGNS_DIR, `${isoDate}-${slug}`);
477
+
478
+ await ctx.stage(
479
+ {
480
+ name: "exporter",
481
+ description: "Export design and create handoff bundle",
482
+ },
483
+ {},
484
+ {},
485
+ async (s) => {
486
+ await s.session.send({
487
+ prompt: buildExportPrompt({
488
+ prompt,
489
+ designDir,
490
+ finalDesignDir,
491
+ designSystem,
492
+ outputType,
493
+ }),
494
+ });
495
+ s.save(await s.session.getMessages());
496
+ },
497
+ );
498
+
499
+ // Deterministic: package handoff bundle (copies Design.md, writes
500
+ // handoff-prompt.md and README.md — no LLM call)
501
+ await writeHandoffBundle(finalDesignDir, {
502
+ designSystem,
503
+ prompt,
504
+ outputType,
505
+ });
506
+ })
507
+ .compile();
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Constants for the open-claude-design workflow.
3
+ *
4
+ * Design-rule constants (REFLEX_FONTS, IMPECCABLE_BANS, DESIGN_DONTS) mirror
5
+ * the canonical impeccable skill shipped at
6
+ * `~/.claude/skills/impeccable/SKILL.md`. The skill itself is only loaded
7
+ * opportunistically by the agent — so we inline the load-bearing rules here
8
+ * to guarantee every generation/refinement/critique turn sees them even if
9
+ * the skill is missing or fails to load. Re-sync when the skill updates.
10
+ */
11
+
12
+ /** Maximum refinement iterations before the loop exits unconditionally. */
13
+ export const MAX_REFINEMENTS = 5;
14
+
15
+ /**
16
+ * Headless stages: structured analysis, tool orchestration, rubric-following.
17
+ * Uses Sonnet for cost efficiency. Bypasses permissions for unattended operation.
18
+ */
19
+ export const HEADLESS_OPTS = {
20
+ permissionMode: "bypassPermissions",
21
+ allowDangerouslySkipPermissions: true,
22
+ model: "sonnet",
23
+ } as const;
24
+
25
+ /**
26
+ * Visible/creative stages: inherit orchestrator model (Opus).
27
+ * No model override — inherits from the parent session.
28
+ */
29
+ export const VISIBLE_OPTS = {
30
+ permissionMode: "bypassPermissions",
31
+ allowDangerouslySkipPermissions: true,
32
+ } as const;
33
+
34
+ /** Name of the design system file written to the project root. */
35
+ export const DESIGN_SYSTEM_FILENAME = "Design.md";
36
+
37
+ /** Directory under project root where final design outputs are stored. */
38
+ export const DESIGNS_DIR = "research/designs";
39
+
40
+ /** Name of the existing impeccable brand context file. */
41
+ export const IMPECCABLE_FILENAME = ".impeccable.md";
42
+
43
+ /**
44
+ * Structured ban — rendered into generation/refinement/critique prompts as a
45
+ * `rule` (what is forbidden) plus `detail` (the CSS pattern + the rewrite).
46
+ */
47
+ export interface ImpeccableBan {
48
+ readonly id: string;
49
+ readonly rule: string;
50
+ readonly detail: string;
51
+ }
52
+
53
+ /**
54
+ * Absolute CSS bans from the canonical impeccable skill's `<absolute_bans>`
55
+ * section. These are the two patterns that the skill flags as "NEVER
56
+ * acceptable" regardless of color, radius, or CSS variable naming. If you
57
+ * change this list, re-sync with `~/.claude/skills/impeccable/SKILL.md`.
58
+ */
59
+ export const IMPECCABLE_BANS: readonly ImpeccableBan[] = [
60
+ {
61
+ id: "side-stripe-borders",
62
+ rule: "BAN 1 — Side-stripe borders on cards/list items/callouts/alerts",
63
+ detail:
64
+ "PATTERN: `border-left:` or `border-right:` with width greater than 1px. " +
65
+ "Forbidden for any color, any CSS variable name (including `--color-warning`, " +
66
+ "`--color-accent`, `--color-primary`). REWRITE: use a different element " +
67
+ "structure entirely — full borders, background tints, leading icons, numbers, " +
68
+ "or no visual indicator at all. Do NOT just swap to `box-shadow: inset`.",
69
+ },
70
+ {
71
+ id: "gradient-text",
72
+ rule: "BAN 2 — Gradient text",
73
+ detail:
74
+ "PATTERN: `background-clip: text` (or `-webkit-background-clip: text`) combined " +
75
+ "with a `linear-gradient`, `radial-gradient`, or `conic-gradient` background. " +
76
+ "Forbidden on any text element — headings, metrics, or inline spans. " +
77
+ "REWRITE: solid colors only for text.",
78
+ },
79
+ ] as const;
80
+
81
+ /**
82
+ * Canonical reflex fonts from the impeccable skill's
83
+ * `<reflex_fonts_to_reject>` list. These are the training-data defaults that
84
+ * create monoculture across projects; every one of them is banned.
85
+ */
86
+ export const REFLEX_FONTS: readonly string[] = [
87
+ "Fraunces",
88
+ "Newsreader",
89
+ "Lora",
90
+ "Crimson",
91
+ "Crimson Pro",
92
+ "Crimson Text",
93
+ "Playfair Display",
94
+ "Cormorant",
95
+ "Cormorant Garamond",
96
+ "Syne",
97
+ "IBM Plex Mono",
98
+ "IBM Plex Sans",
99
+ "IBM Plex Serif",
100
+ "Space Mono",
101
+ "Space Grotesk",
102
+ "Inter",
103
+ "DM Sans",
104
+ "DM Serif Display",
105
+ "DM Serif Text",
106
+ "Outfit",
107
+ "Plus Jakarta Sans",
108
+ "Instrument Sans",
109
+ "Instrument Serif",
110
+ ] as const;
111
+
112
+ /**
113
+ * Non-ban DON'Ts distilled from the impeccable skill sections on Typography,
114
+ * Color, Layout/Space, Motion, UX Writing, and Responsive. These are not
115
+ * absolute bans, but they are the recognizable AI fingerprints from
116
+ * 2024-2025 and must be avoided in generated designs.
117
+ */
118
+ export const DESIGN_DONTS: readonly string[] = [
119
+ // Typography
120
+ "Do not use monospace typography as lazy shorthand for 'technical/developer' vibes.",
121
+ "Do not put large icons with rounded corners above every heading.",
122
+ "Do not use only one font family for the entire page — pair a distinctive display font with a refined body font.",
123
+ "Do not use a flat type hierarchy where sizes are too close together — aim for at least a 1.25 ratio between steps.",
124
+ "Do not set long body passages in uppercase — reserve all-caps for short labels and headings.",
125
+ // Color & theme
126
+ "Do not use pure black (#000) or pure white (#fff) — always tint toward the brand hue.",
127
+ "Do not use the AI color palette (cyan-on-dark, purple-to-blue gradients, neon accents on dark backgrounds).",
128
+ "Do not default to dark mode with glowing accents, or to light mode 'to be safe' — derive theme from audience and viewing context.",
129
+ "Do not use gray text on colored backgrounds — use a shade of the background color instead.",
130
+ // Layout & space
131
+ "Do not wrap everything in cards — not everything needs a container.",
132
+ "Do not nest cards inside cards.",
133
+ "Do not use identical card grids (same-sized cards with icon + heading + text, repeated endlessly).",
134
+ "Do not use the hero metric layout template (big number, small label, supporting stats, gradient accent).",
135
+ "Do not center everything — left-aligned text with asymmetric layouts feels more designed.",
136
+ "Do not use the same spacing everywhere — create rhythm with tight groupings and generous separations.",
137
+ "Do not let body text wrap beyond ~80 characters per line — use a max-width like 65–75ch.",
138
+ // Motion
139
+ "Do not animate layout properties (width, height, padding, margin) — use transform and opacity only.",
140
+ "Do not use bounce or elastic easing — real objects decelerate smoothly.",
141
+ // UX writing
142
+ "Do not repeat information (redundant headers, intros that restate the heading).",
143
+ "Do not make every button primary — use ghost buttons, text links, and secondary styles for hierarchy.",
144
+ // Responsive
145
+ "Do not hide critical functionality on mobile — adapt the interface, don't amputate it.",
146
+ ] as const;
147
+
148
+ /**
149
+ * Command invocation for the impeccable scanner. The correct subcommand is
150
+ * `detect`; a bare `impeccable --json` just prints the top-level help.
151
+ * Exit code is always 0 — callers must parse the JSON array and check length
152
+ * to know whether findings exist.
153
+ */
154
+ export const IMPECCABLE_SCAN_CMD = [
155
+ "bunx",
156
+ "impeccable",
157
+ "detect",
158
+ "--json",
159
+ ] as const;