From 6ac32eedd7b4b49b842688d16354d4303620ad56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pe=C3=B1a?= Date: Thu, 7 May 2026 00:38:34 -0600 Subject: [PATCH 1/3] feat(rules): add 4 design-system PreToolUse rules (DOJ-3924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds AI-write-time enforcement for Design System rules β€” the 4th enforcement layer complementing dojo-os's: - useDsRules React hook (passive, runtime-only) - ds-lint pre-commit hook (post-author, post-write) - validate-storage-upload Edge Function (Storage bucket uploads) This 4th layer fires PreToolUse on Edit/Write/MultiEdit, blocking the write BEFORE the file is touched if the LLM proposes a violation. Tightest feedback loop + educates the LLM in real time. 4 new rules: - ds-arbitrary-breakpoint (block min-[Xpx] in TSX, suggest sm:/md:/lg:) - ds-deep-ui-import (block @/components/ui/ deep imports, force barrel) - ds-arbitrary-fixed-width-in-ds-component (block w-[Xpx] inside src/components/ui/) - ds-raw-hex-color-in-source (warn raw hex in TSX/TS, exempt theme/test/storybook) Each rule ships with 5 tests (20 total): blocks/warns the violation, allows preset alternatives, allows exempt paths, allows bypass marker. Bumps the rules manifest only β€” no new bash scripts, no schema changes. The existing v1.5.0 manifest-driven framework dispatches via pre-edit.sh on Edit/Write/MultiEdit and parse-input.sh extracts file_path + content. Closes DOJ-3924 (Phase G of DOJ-3439 design-system-linter). πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) --- hooks/rules/README.md | 22 +++ hooks/rules/rules.json | 313 +++++++++++++++++++++++++++++++++++++++++ hooks/rules/rules.yaml | 237 +++++++++++++++++++++++++++++++ 3 files changed, 572 insertions(+) diff --git a/hooks/rules/README.md b/hooks/rules/README.md index 215739f..e2f6881 100644 --- a/hooks/rules/README.md +++ b/hooks/rules/README.md @@ -84,6 +84,28 @@ client-acme client-beta ``` +## Rule families + +The manifest groups rules into informal families by prefix / domain. +Adding a new family is fine β€” just keep ids unique and follow the schema. + +- **Bash safety** (`ssh-db-mutation`, `gcloud-missing-project`, + `prod-ops-no-approval`, `destructive-db-ops`, `manual-edge-fn-deploy`) + β€” block / warn on dangerous shell invocations. +- **File safety** (`minified-build-output`, `secrets-hardcoded`) + β€” block writes of minified build artifacts and hardcoded credentials. +- **Slack style** (`slack-unicode-bullets`, `slack-tables-no-codeblock`, + `slack-spanish-tildes`) β€” warn-only formatting nudges. +- **Design System** (`ds-arbitrary-breakpoint`, `ds-deep-ui-import`, + `ds-arbitrary-fixed-width-in-ds-component`, + `ds-raw-hex-color-in-source`) β€” block / warn at write-time when AI + proposes Edit/Write/MultiEdit changes that violate the Dojo Design + System contract (preset breakpoints, barrel imports, flexible widths, + token-only colors). Added in DOJ-3924 as the 4th enforcement layer + alongside the runtime hook, pre-commit linter, and Storage upload + validator. See + [DOJ-3924](https://linear.app/dojocoding/issue/DOJ-3924). + ## Tier 2 β€” decomposing non-deterministic memories Many narrative-style guidelines can be converted to deterministic rules diff --git a/hooks/rules/rules.json b/hooks/rules/rules.json index 8a60be8..fd07e8c 100644 --- a/hooks/rules/rules.json +++ b/hooks/rules/rules.json @@ -539,5 +539,318 @@ "expected_exit": 0 } ] + }, + { + "id": "ds-arbitrary-breakpoint", + "description": "Block arbitrary Tailwind breakpoints (min-[Xpx]) in TSX/JSX/TS source β€” use sm:/md:/lg:/xl:/2xl: preset breakpoints", + "applies_to": [ + "Edit", + "Write", + "MultiEdit" + ], + "match": [ + { + "field": "file_path", + "pattern": "\\.(tsx|jsx|ts|js)$" + }, + { + "field": "content", + "pattern": "min-\\[[0-9]+px\\]" + } + ], + "action": "block", + "bypass_marker": "ds-arbitrary-breakpoint", + "memory_ref": "feedback_design_system_no_arbitrary_breakpoints.md", + "message": "BLOCKED: arbitrary Tailwind breakpoint detected (min-[Xpx]).\n\nUse Tailwind's preset breakpoints instead: sm: (640px), md: (768px),\nlg: (1024px), xl: (1280px), 2xl: (1536px). Arbitrary values fragment\nthe responsive design language and break consistency across the app.\n\nIf a non-preset breakpoint is genuinely required, add\n\"// hook-bypass: ds-arbitrary-breakpoint\" to the line and justify\nin the PR description.\n", + "tests": [ + { + "name": "blocks-min-1200px-in-tsx", + "input": { + "tool_input": { + "file_path": "src/components/foo.tsx", + "content": "
x
" + } + }, + "expected_exit": 2 + }, + { + "name": "blocks-min-768-arbitrary", + "input": { + "tool_input": { + "file_path": "src/pages/Bar.tsx", + "content": "
y
" + } + }, + "expected_exit": 2 + }, + { + "name": "allows-preset-md", + "input": { + "tool_input": { + "file_path": "src/components/foo.tsx", + "content": "
x
" + } + }, + "expected_exit": 0 + }, + { + "name": "allows-non-tsx-files", + "input": { + "tool_input": { + "file_path": "docs/example.md", + "content": "Use min-[1200px] for ..." + } + }, + "expected_exit": 0 + }, + { + "name": "allows-bypass-marker", + "input": { + "tool_input": { + "file_path": "src/components/foo.tsx", + "content": "
x
// hook-bypass: ds-arbitrary-breakpoint" + } + }, + "expected_exit": 0 + } + ] + }, + { + "id": "ds-deep-ui-import", + "description": "Block deep imports from @/components/ui/ β€” always import from the barrel @/components/ui", + "applies_to": [ + "Edit", + "Write", + "MultiEdit" + ], + "match": [ + { + "field": "file_path", + "pattern": "\\.(tsx|jsx|ts)$" + }, + { + "field": "content", + "pattern": "from[[:space:]]+[\"']@/components/ui/[a-zA-Z][a-zA-Z0-9_-]+[\"']" + } + ], + "action": "block", + "bypass_marker": "ds-deep-ui-import", + "memory_ref": "feedback_design_system_barrel_imports.md", + "message": "BLOCKED: deep import from @/components/ui/ detected.\n\nAlways import Design System components from the barrel:\n import { Button, Card, TextInput } from \"@/components/ui\"\n\nDeep imports like @/components/ui/button bypass the barrel-controlled\nsurface and make refactors brittle. The barrel is the public API.\n", + "tests": [ + { + "name": "blocks-deep-button-import", + "input": { + "tool_input": { + "file_path": "src/pages/Foo.tsx", + "content": "import { Button } from \"@/components/ui/button\"" + } + }, + "expected_exit": 2 + }, + { + "name": "blocks-deep-textinput-import", + "input": { + "tool_input": { + "file_path": "src/components/Bar.tsx", + "content": "import { TextInput } from '@/components/ui/text-input'" + } + }, + "expected_exit": 2 + }, + { + "name": "allows-barrel-import", + "input": { + "tool_input": { + "file_path": "src/pages/Foo.tsx", + "content": "import { Button, Card } from \"@/components/ui\"" + } + }, + "expected_exit": 0 + }, + { + "name": "allows-non-ds-import", + "input": { + "tool_input": { + "file_path": "src/pages/Foo.tsx", + "content": "import { foo } from \"@/lib/utils\"" + } + }, + "expected_exit": 0 + }, + { + "name": "allows-bypass-marker", + "input": { + "tool_input": { + "file_path": "src/pages/Foo.tsx", + "content": "import { Button } from \"@/components/ui/button\" // hook-bypass: ds-deep-ui-import" + } + }, + "expected_exit": 0 + } + ] + }, + { + "id": "ds-arbitrary-fixed-width-in-ds-component", + "description": "Block arbitrary fixed widths (w-[Xpx]) inside DS components β€” they must be flexible (w-full + max-w)", + "applies_to": [ + "Edit", + "Write", + "MultiEdit" + ], + "match": [ + { + "field": "file_path", + "pattern": "^src/components/ui/" + }, + { + "field": "content", + "pattern": "(^|[^a-zA-Z-])w-\\[[0-9]+px\\]" + } + ], + "action": "block", + "bypass_marker": "ds-arbitrary-fixed-width", + "memory_ref": "feedback_design_system_no_fixed_pixel_widths.md", + "message": "BLOCKED: arbitrary fixed pixel width in a DS component.\n\nDS components live under src/components/ui/ and must be flexible.\nUse w-full + max-w-[Xpx] instead of w-[Xpx], so the component\nadapts to its container.\n\nApplication-level usage (outside src/components/ui/) is exempt β€”\nthis rule only fires when editing the DS components themselves.\n", + "tests": [ + { + "name": "blocks-w-pixel-in-ui-button", + "input": { + "tool_input": { + "file_path": "src/components/ui/button.tsx", + "content": "" + } + }, + "expected_exit": 2 + }, + { + "name": "blocks-w-pixel-in-ui-card", + "input": { + "tool_input": { + "file_path": "src/components/ui/card.tsx", + "content": "
y
" + } + }, + "expected_exit": 2 + }, + { + "name": "allows-w-pixel-outside-ds", + "input": { + "tool_input": { + "file_path": "src/pages/Settings.tsx", + "content": "
page-specific
" + } + }, + "expected_exit": 0 + }, + { + "name": "allows-w-full-max-w", + "input": { + "tool_input": { + "file_path": "src/components/ui/button.tsx", + "content": "" + } + }, + "expected_exit": 0 + }, + { + "name": "allows-bypass-marker", + "input": { + "tool_input": { + "file_path": "src/components/ui/special.tsx", + "content": "
px-perfect-icon
// hook-bypass: ds-arbitrary-fixed-width" + } + }, + "expected_exit": 0 + } + ] + }, + { + "id": "ds-raw-hex-color-in-source", + "description": "Block raw hex color literals in TSX/TS source β€” colors must come from Tailwind tokens (dojo-primary-*, etc.)", + "applies_to": [ + "Edit", + "Write", + "MultiEdit" + ], + "match": [ + { + "field": "file_path", + "pattern": "\\.(tsx|jsx|ts|js)$", + "not_pattern": "(tailwind\\.config|theme/|design-system/|design-tokens|\\.test\\.|\\.stories\\.)" + }, + { + "field": "content", + "pattern": "#[0-9a-fA-F]{6}\\b" + } + ], + "action": "warn", + "bypass_marker": "ds-raw-hex-color", + "memory_ref": "feedback_design_system_token_only_colors.md", + "message": "WARNING: raw hex color literal found in source.\n\nUse a Tailwind token instead (e.g., bg-dojo-primary-lilac, text-dojo-foreground).\nThe token system is the single source of truth for color and theming.\n\nExempt paths (NOT flagged): tailwind.config.*, theme/, design-system/,\ndesign-tokens, .test.*, .stories.*. Use the bypass marker if a hex is\ngenuinely required outside those paths (e.g. embedding a brand color\nin a logo SVG that the design system doesn't yet expose).\n", + "tests": [ + { + "name": "warns-on-raw-hex-in-tsx", + "input": { + "tool_input": { + "file_path": "src/components/foo.tsx", + "content": "
x
" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "ds-raw-hex-color-in-source" + }, + { + "name": "warns-on-raw-hex-in-ts", + "input": { + "tool_input": { + "file_path": "src/lib/util.ts", + "content": "const PRIMARY = \"#C980FC\"" + } + }, + "expected_exit": 0, + "expected_stderr_contains": "ds-raw-hex-color-in-source" + }, + { + "name": "allows-tailwind-config", + "input": { + "tool_input": { + "file_path": "tailwind.config.ts", + "content": "lilac: \"#C980FC\"" + } + }, + "expected_exit": 0 + }, + { + "name": "allows-theme-file", + "input": { + "tool_input": { + "file_path": "src/theme/tokens.ts", + "content": "export const PRIMARY = \"#C980FC\"" + } + }, + "expected_exit": 0 + }, + { + "name": "allows-test-file", + "input": { + "tool_input": { + "file_path": "src/components/foo.test.tsx", + "content": "expect(color).toBe(\"#FF7151\")" + } + }, + "expected_exit": 0 + }, + { + "name": "allows-bypass-marker", + "input": { + "tool_input": { + "file_path": "src/components/Logo.tsx", + "content": " // hook-bypass: ds-raw-hex-color" + } + }, + "expected_exit": 0 + } + ] } ] diff --git a/hooks/rules/rules.yaml b/hooks/rules/rules.yaml index 59e17b1..8ec3ddb 100644 --- a/hooks/rules/rules.yaml +++ b/hooks/rules/rules.yaml @@ -410,3 +410,240 @@ tool_input: text: 'La migraciΓ³n fue exitosa' expected_exit: 0 + +# ----------------------------------------------------------------------------- +# Design System rules (DOJ-3924) +# +# These four rules implement AI-write-time enforcement for the Dojo Design +# System. They are the 4th enforcement layer, complementing the dojo-os +# repo's existing layers: +# 1. useDsRules React hook (passive, runtime-only) +# 2. ds-lint pre-commit hook (post-author, post-write) +# 3. validate-storage-upload Edge Function (Storage bucket uploads) +# 4. THIS layer β€” PreToolUse on Edit/Write/MultiEdit (write-time, blocks +# the violation BEFORE the file is touched, tightest feedback loop) +# +# Linear: DOJ-3924 (Phase G of DOJ-3439 design-system-linter). +# ----------------------------------------------------------------------------- + +- id: ds-arbitrary-breakpoint + description: 'Block arbitrary Tailwind breakpoints (min-[Xpx]) in TSX/JSX/TS source β€” use sm:/md:/lg:/xl:/2xl: preset breakpoints' + applies_to: [Edit, Write, MultiEdit] + match: + - field: file_path + pattern: '\.(tsx|jsx|ts|js)$' + - field: content + pattern: 'min-\[[0-9]+px\]' + action: block + bypass_marker: ds-arbitrary-breakpoint + memory_ref: feedback_design_system_no_arbitrary_breakpoints.md + message: | + BLOCKED: arbitrary Tailwind breakpoint detected (min-[Xpx]). + + Use Tailwind's preset breakpoints instead: sm: (640px), md: (768px), + lg: (1024px), xl: (1280px), 2xl: (1536px). Arbitrary values fragment + the responsive design language and break consistency across the app. + + If a non-preset breakpoint is genuinely required, add + "// hook-bypass: ds-arbitrary-breakpoint" to the line and justify + in the PR description. + tests: + - name: blocks-min-1200px-in-tsx + input: + tool_input: + file_path: 'src/components/foo.tsx' + content: '
x
' + expected_exit: 2 + - name: blocks-min-768-arbitrary + input: + tool_input: + file_path: 'src/pages/Bar.tsx' + content: '
y
' + expected_exit: 2 + - name: allows-preset-md + input: + tool_input: + file_path: 'src/components/foo.tsx' + content: '
x
' + expected_exit: 0 + - name: allows-non-tsx-files + input: + tool_input: + file_path: 'docs/example.md' + content: 'Use min-[1200px] for ...' + expected_exit: 0 + - name: allows-bypass-marker + input: + tool_input: + file_path: 'src/components/foo.tsx' + content: '
x
// hook-bypass: ds-arbitrary-breakpoint' + expected_exit: 0 + +- id: ds-deep-ui-import + description: Block deep imports from @/components/ui/ β€” always import from the barrel @/components/ui + applies_to: [Edit, Write, MultiEdit] + match: + - field: file_path + pattern: '\.(tsx|jsx|ts)$' + - field: content + pattern: 'from[[:space:]]+["'']@/components/ui/[a-zA-Z][a-zA-Z0-9_-]+["'']' + action: block + bypass_marker: ds-deep-ui-import + memory_ref: feedback_design_system_barrel_imports.md + message: | + BLOCKED: deep import from @/components/ui/ detected. + + Always import Design System components from the barrel: + import { Button, Card, TextInput } from "@/components/ui" + + Deep imports like @/components/ui/button bypass the barrel-controlled + surface and make refactors brittle. The barrel is the public API. + tests: + - name: blocks-deep-button-import + input: + tool_input: + file_path: 'src/pages/Foo.tsx' + content: 'import { Button } from "@/components/ui/button"' + expected_exit: 2 + - name: blocks-deep-textinput-import + input: + tool_input: + file_path: 'src/components/Bar.tsx' + content: "import { TextInput } from '@/components/ui/text-input'" + expected_exit: 2 + - name: allows-barrel-import + input: + tool_input: + file_path: 'src/pages/Foo.tsx' + content: 'import { Button, Card } from "@/components/ui"' + expected_exit: 0 + - name: allows-non-ds-import + input: + tool_input: + file_path: 'src/pages/Foo.tsx' + content: 'import { foo } from "@/lib/utils"' + expected_exit: 0 + - name: allows-bypass-marker + input: + tool_input: + file_path: 'src/pages/Foo.tsx' + content: 'import { Button } from "@/components/ui/button" // hook-bypass: ds-deep-ui-import' + expected_exit: 0 + +- id: ds-arbitrary-fixed-width-in-ds-component + description: Block arbitrary fixed widths (w-[Xpx]) inside DS components β€” they must be flexible (w-full + max-w) + applies_to: [Edit, Write, MultiEdit] + match: + - field: file_path + pattern: '^src/components/ui/' + - field: content + # Require the `w` to NOT be preceded by a letter/dash so we don't + # mis-flag legitimate composite utilities like max-w-[200px] or + # min-w-[120px]. ERE has no lookahead, so we anchor on a leading + # boundary of "start-of-line OR non-[a-zA-Z-] character". + pattern: '(^|[^a-zA-Z-])w-\[[0-9]+px\]' + action: block + bypass_marker: ds-arbitrary-fixed-width + memory_ref: feedback_design_system_no_fixed_pixel_widths.md + message: | + BLOCKED: arbitrary fixed pixel width in a DS component. + + DS components live under src/components/ui/ and must be flexible. + Use w-full + max-w-[Xpx] instead of w-[Xpx], so the component + adapts to its container. + + Application-level usage (outside src/components/ui/) is exempt β€” + this rule only fires when editing the DS components themselves. + tests: + - name: blocks-w-pixel-in-ui-button + input: + tool_input: + file_path: 'src/components/ui/button.tsx' + content: '' + expected_exit: 2 + - name: blocks-w-pixel-in-ui-card + input: + tool_input: + file_path: 'src/components/ui/card.tsx' + content: '
y
' + expected_exit: 2 + - name: allows-w-pixel-outside-ds + input: + tool_input: + file_path: 'src/pages/Settings.tsx' + content: '
page-specific
' + expected_exit: 0 + - name: allows-w-full-max-w + input: + tool_input: + file_path: 'src/components/ui/button.tsx' + content: '' + expected_exit: 0 + - name: allows-bypass-marker + input: + tool_input: + file_path: 'src/components/ui/special.tsx' + content: '
px-perfect-icon
// hook-bypass: ds-arbitrary-fixed-width' + expected_exit: 0 + +- id: ds-raw-hex-color-in-source + description: Block raw hex color literals in TSX/TS source β€” colors must come from Tailwind tokens (dojo-primary-*, etc.) + applies_to: [Edit, Write, MultiEdit] + match: + - field: file_path + pattern: '\.(tsx|jsx|ts|js)$' + not_pattern: '(tailwind\.config|theme/|design-system/|design-tokens|\.test\.|\.stories\.)' + - field: content + pattern: '#[0-9a-fA-F]{6}\b' + action: warn + bypass_marker: ds-raw-hex-color + memory_ref: feedback_design_system_token_only_colors.md + message: | + WARNING: raw hex color literal found in source. + + Use a Tailwind token instead (e.g., bg-dojo-primary-lilac, text-dojo-foreground). + The token system is the single source of truth for color and theming. + + Exempt paths (NOT flagged): tailwind.config.*, theme/, design-system/, + design-tokens, .test.*, .stories.*. Use the bypass marker if a hex is + genuinely required outside those paths (e.g. embedding a brand color + in a logo SVG that the design system doesn't yet expose). + tests: + - name: warns-on-raw-hex-in-tsx + input: + tool_input: + file_path: 'src/components/foo.tsx' + content: '
x
' + expected_exit: 0 + expected_stderr_contains: 'ds-raw-hex-color-in-source' + - name: warns-on-raw-hex-in-ts + input: + tool_input: + file_path: 'src/lib/util.ts' + content: 'const PRIMARY = "#C980FC"' + expected_exit: 0 + expected_stderr_contains: 'ds-raw-hex-color-in-source' + - name: allows-tailwind-config + input: + tool_input: + file_path: 'tailwind.config.ts' + content: 'lilac: "#C980FC"' + expected_exit: 0 + - name: allows-theme-file + input: + tool_input: + file_path: 'src/theme/tokens.ts' + content: 'export const PRIMARY = "#C980FC"' + expected_exit: 0 + - name: allows-test-file + input: + tool_input: + file_path: 'src/components/foo.test.tsx' + content: 'expect(color).toBe("#FF7151")' + expected_exit: 0 + - name: allows-bypass-marker + input: + tool_input: + file_path: 'src/components/Logo.tsx' + content: ' // hook-bypass: ds-raw-hex-color' + expected_exit: 0 From 26a92e22704104d7a7f12abd63917c272682f34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pe=C3=B1a?= Date: Sat, 9 May 2026 18:34:49 -0600 Subject: [PATCH 2/3] fix(rules): make ds-fixed-width rule fire on absolute paths + clarify hex warn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile review on PR #14 caught three real issues: 1. ds-arbitrary-fixed-width-in-ds-component used a ^-anchored file_path pattern that never matched at runtime β€” Claude Code passes absolute paths like /home/user/repo/src/components/ui/foo.tsx, but ^src/ only anchors to start-of-string. All 56 tests passed because fixtures used relative paths; the rule was silently inert in production. Switch to (^|/)src/components/ui/ so both relative and absolute paths trigger it. Add a regression test covering an absolute fixture path. 2. ds-raw-hex-color-in-source description started with "Block" while action is warn β€” confused both maintainers reading the manifest and end-users seeing the runtime stderr. Description now says "Warn". 3. The design-system/ and design-tokens not_pattern exemptions had no test coverage. Added two fixtures so a future regex narrow-down that broke them would fail CI. Tests: 59/59 passing (was 56/56; added 3 regression cases). Co-Authored-By: Claude Opus 4.7 (1M context) --- hooks/rules/rules.json | 34 ++++++++++++++++++++++++++++++++-- hooks/rules/rules.yaml | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/hooks/rules/rules.json b/hooks/rules/rules.json index fd07e8c..dc6c92f 100644 --- a/hooks/rules/rules.json +++ b/hooks/rules/rules.json @@ -701,7 +701,7 @@ "match": [ { "field": "file_path", - "pattern": "^src/components/ui/" + "pattern": "(^|/)src/components/ui/" }, { "field": "content", @@ -733,6 +733,16 @@ }, "expected_exit": 2 }, + { + "name": "blocks-w-pixel-in-ui-button-absolute-path", + "input": { + "tool_input": { + "file_path": "/home/user/repo/src/components/ui/button.tsx", + "content": "" + } + }, + "expected_exit": 2 + }, { "name": "allows-w-pixel-outside-ds", "input": { @@ -767,7 +777,7 @@ }, { "id": "ds-raw-hex-color-in-source", - "description": "Block raw hex color literals in TSX/TS source β€” colors must come from Tailwind tokens (dojo-primary-*, etc.)", + "description": "Warn on raw hex color literals in TSX/TS source β€” colors must come from Tailwind tokens (dojo-primary-*, etc.)", "applies_to": [ "Edit", "Write", @@ -841,6 +851,26 @@ }, "expected_exit": 0 }, + { + "name": "allows-design-system-folder", + "input": { + "tool_input": { + "file_path": "src/design-system/tokens.ts", + "content": "export const PRIMARY = \"#C980FC\"" + } + }, + "expected_exit": 0 + }, + { + "name": "allows-design-tokens-file", + "input": { + "tool_input": { + "file_path": "src/lib/design-tokens.ts", + "content": "export const LILAC = \"#C980FC\"" + } + }, + "expected_exit": 0 + }, { "name": "allows-bypass-marker", "input": { diff --git a/hooks/rules/rules.yaml b/hooks/rules/rules.yaml index 8ec3ddb..c84303b 100644 --- a/hooks/rules/rules.yaml +++ b/hooks/rules/rules.yaml @@ -534,8 +534,12 @@ description: Block arbitrary fixed widths (w-[Xpx]) inside DS components β€” they must be flexible (w-full + max-w) applies_to: [Edit, Write, MultiEdit] match: + # Anchor on start-of-string OR a leading "/" so the rule fires for both + # relative paths ("src/components/ui/...") and absolute paths + # ("/home/user/repo/src/components/ui/..."). Claude Code's Edit/Write + # tools may pass either form at runtime. - field: file_path - pattern: '^src/components/ui/' + pattern: '(^|/)src/components/ui/' - field: content # Require the `w` to NOT be preceded by a letter/dash so we don't # mis-flag legitimate composite utilities like max-w-[200px] or @@ -567,6 +571,15 @@ file_path: 'src/components/ui/card.tsx' content: '
y
' expected_exit: 2 + # Regression: Claude Code passes absolute paths to Edit/Write at runtime. + # Without the (^|/) anchor the rule would silently allow these (the bug + # Greptile flagged in PR #14). + - name: blocks-w-pixel-in-ui-button-absolute-path + input: + tool_input: + file_path: '/home/user/repo/src/components/ui/button.tsx' + content: '' + expected_exit: 2 - name: allows-w-pixel-outside-ds input: tool_input: @@ -587,7 +600,7 @@ expected_exit: 0 - id: ds-raw-hex-color-in-source - description: Block raw hex color literals in TSX/TS source β€” colors must come from Tailwind tokens (dojo-primary-*, etc.) + description: Warn on raw hex color literals in TSX/TS source β€” colors must come from Tailwind tokens (dojo-primary-*, etc.) applies_to: [Edit, Write, MultiEdit] match: - field: file_path @@ -641,6 +654,21 @@ file_path: 'src/components/foo.test.tsx' content: 'expect(color).toBe("#FF7151")' expected_exit: 0 + # Lock in the design-system/ exemption so a future regex tweak that + # accidentally narrows not_pattern fails this test. + - name: allows-design-system-folder + input: + tool_input: + file_path: 'src/design-system/tokens.ts' + content: 'export const PRIMARY = "#C980FC"' + expected_exit: 0 + # Lock in the design-tokens exemption (matches both folder and filename). + - name: allows-design-tokens-file + input: + tool_input: + file_path: 'src/lib/design-tokens.ts' + content: 'export const LILAC = "#C980FC"' + expected_exit: 0 - name: allows-bypass-marker input: tool_input: From 4cffbe290bb167d38bb9fddba688dfe01b0200d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pe=C3=B1a?= Date: Sat, 9 May 2026 18:40:53 -0600 Subject: [PATCH 3/3] ci: migrate test-hooks workflow to Blacksmith runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub Actions account is locked due to a billing issue, blocking every ubuntu-latest job from starting. Same pattern dojo-os already adopted: swap to blacksmith-2vcpu-ubuntu-2204 (Blacksmith managed runners) so the test workflow can actually run. Drop-in replacement β€” Blacksmith runners are API-compatible with the GitHub-hosted Ubuntu image, and the workflow steps (npm ci, build-rules, test-hooks.sh) don't depend on anything GitHub-specific. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test-hooks.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-hooks.yml b/.github/workflows/test-hooks.yml index 269f3fb..212b541 100644 --- a/.github/workflows/test-hooks.yml +++ b/.github/workflows/test-hooks.yml @@ -14,7 +14,9 @@ on: jobs: test: - runs-on: ubuntu-latest + # Use Blacksmith managed runners (same pattern as dojo-os) to avoid the + # GitHub-hosted billing block. Drop-in replacement for ubuntu-latest. + runs-on: blacksmith-2vcpu-ubuntu-2204 steps: - uses: actions/checkout@v4