diff --git a/.ai/README.md b/.ai/README.md index 5ab883e731d..101a2e82192 100644 --- a/.ai/README.md +++ b/.ai/README.md @@ -1,423 +1,158 @@ -# AI and agent documentation +# AI and agent configuration -Coding agents should start with [`AGENTS.md`](../AGENTS.md) at the repository root. It summarizes how to use this directory as the canonical source for rules and skills. +`.ai/` is the single, tool-agnostic source for this repository's AI rules, skills, and lessons. Coding agents start from [`AGENTS.md`](../AGENTS.md) at the repository root. GitHub Copilot (CLI, app, VS Code, cloud agent, and code review), Claude Code, and Cursor read `.ai/` through native discovery paths, symlinks, and generated files, so nobody has to load anything by hand. -This directory contains rules, skills, and accumulated memory that coding agents use to enforce consistent formatting and structure in our codebase. +## How it works -## Why `.ai/` - -All rules and skills now live in **`.ai/`** — a tool-agnostic, plain-markdown directory that any agent or tool can read. IDE-specific directories (`.cursor/`, `.claude/`) become thin adapters that point back to `.ai/` via symlinks: - -- Edit once in `.ai/` → all tools see the update automatically -- No sync step, no duplication, no drift between tools -- New contributors or tools start from `AGENTS.md` at the repo root, which bootstraps everything - -### Rules carry `paths:` frontmatter; tool copies are generated - -Every rule in `.ai/rules/` has a `description` and a quoted `paths:` list of globs. `yarn ai:sync` (`.ai/scripts/sync.js`) generates the tool-specific copies from that one source: - -- `.github/instructions/.instructions.md` with `applyTo` for GitHub Copilot (CLI, app, VS Code, cloud agent, and code review) -- `.cursor/rules/.mdc` with `globs` for Cursor -- Claude Code reads `.ai/rules/` directly through the `.claude/rules` symlink, because `paths:` is its own key - -Glob semantics follow GitHub's `applyTo`: `*` stays within one directory and `**` crosses directories, so `*.css` matches only root-level files and `**/*.css` matches CSS anywhere. `yarn lint:ai` fails when a glob matches no tracked file, and when a generated file doesn't match its source. The pre-commit hook runs `yarn ai:sync` whenever `.ai/` or an `AGENTS.md` file is staged. It skips the sync with a warning if `.ai/rules/` or `.ai/memory/` has unstaged changes, and the pre-push hook regenerates any out-of-date files and blocks the push until you commit them. - -### Rules vs. skills: how to choose - -- **Path-scoped rule** — the guidance is tied to a specific set of file paths (e.g. "when editing a `.stories.ts` file" or "when editing a component README"). Give it a `paths:` list so it loads deterministically whenever a matching file is in context, in every tool — no risk of it going unread just because a task's intent wasn't explicit. -- **Skill** — the guidance is tied to a task or intent, not a file path (e.g. "draft a Jira ticket", "run a consistency pass"). There's no glob to scope it by, so it's invoked on demand: the agent matches the task to the skill's description, or the user names it explicitly. - -Getting this wrong in either direction has a real cost: forcing task-scoped guidance into a rule with no natural `paths:` value means it's either always-inlined (wasting tokens) or never triggers; forcing file-scoped guidance into a skill loses the deterministic trigger a path-scoped rule gives you and depends on the agent guessing intent. - -## CI integration - -- `yarn lint:ai` runs `.ai/scripts/validate.js`, which checks story tags, links, branch and commit types against commitlint, instruction and skill frontmatter, symlinks, generated files, and per-unit MDX docs pages. The header of `validate.js` lists each check. -- `yarn lint:docs-pages` runs the per-unit MDX docs-page check in isolation. Use during authoring to catch missing `` references, unknown `##` section headings, or out-of-order sections in a single component / pattern / controller MDX -- Pre-commit hook runs the contributor docs nav script to keep breadcrumbs and TOCs in sync automatically - -## Rules - -Rules are narrative, per-topic guidance in `.ai/rules/` that an agent reads when a matching file is in context. Structured data that used to live in a separate config file now lives with the guidance or tool that uses it: - -- Commit and branch types come from commitlint (`commitlint.config.cjs`); `yarn lint:ai` fails if the `branch-naming` or `conventional-commits` skill disagrees with it -- Jira labels, issue types, and templates are in the `jira-ticket` skill -- Editor and formatter settings are in `.editorconfig`, `.prettierrc.yaml`, and `.vscode/settings.json` - -### Available rules - -Every rule is a **path-scoped rule**: it carries a `paths:` list so it loads only when a matching file is in context, in every tool. Guidance with no natural file-path scope is a skill instead — see [Available skills](#available-skills). `branch-naming` and `storybook-mdx-conversion` were rules and are now skills. - -#### Path-scoped rules - -##### Styles - -- **stylelint_compliance**: Auto-fixes based on `stylelint.config.js` unless rewriting more than 30% of the line -- **copyrights**: Must reflect the current year -- **comments**: Always use sentence case, never title case -- **custom_properties**: Never rename without prompting for approval first -- **media_queries**: Sort high-contrast and other media queries to the bottom of the file -- **duplicate_properties**: Warn about or suggest fixes; keep the definition that honors the CSS cascade -- Applies to: `**/*.css` - -##### Text formatting - -- **heading_case**: Enforces sentence case in headings with specific exceptions -- Applies to: `**/*.md`, `**/*.txt`, `**/*.mdx` - -##### Storybook stories (documentation + format) - -These two rules work as a pair: `stories-documentation` defines _what_ to document, `stories-format` defines _how_ to structure the file. Each rule keeps the constraints that apply to every matching file and points to a skill of the same name for the full procedure (`.ai/skills/stories-documentation/SKILL.md` and `.ai/skills/stories-format/SKILL.md`), so the always-loaded part stays under 12 KB. - -- **stories-documentation**: Content patterns for each documentation section - - Sections: overview, anatomy, options, states, behaviors, accessibility - - 1st-gen to gen2 comparison guidance - - Verification process to prevent hallucinated attributes, slots, or ARIA claims - - Applies to: `gen2/packages/swc/components/*/*.mdx`, `gen2/packages/swc/patterns/*/*/*.mdx`, `gen2/packages/core/controllers/*/*.mdx` -- **stories-format**: File structure and technical conventions - - Visual separators, meta configuration, required tags, layout parameters - - `render` vs `args` patterns, `flexLayout` usage - - Static color single-story pattern, image asset conventions - - Applies to: `gen2/packages/swc/components/*/stories/**`, `gen2/packages/swc/patterns/*/*/stories/**`, `gen2/packages/core/controllers/*/stories/**` - -##### Component README - -- **document_structure**: Required sections for 1st-gen component READMEs - - Sections: overview, usage, anatomy, options, states, behaviors, accessibility - - Starts with `## Overview`, not `# Component Name` -- **code_examples**: All examples must include accessible labels and unique IDs -- **sp_tabs**: Must include `selected`, `auto`, and `label` attributes -- Applies to: `1st-gen/packages/*/README.md` - -##### Contributor docs - -- **nav_update**: Run the nav script when adding, removing, renaming, or moving files under `CONTRIBUTOR-DOCS/` -- **link_validation**: Fix broken links automatically when the fix is clear; ask when intent is unclear -- Applies to: `CONTRIBUTOR-DOCS/**` -- Points to the `contributor-docs-nav` skill for the full Operator/Maintainer workflow - -##### Pointer rules - -- **accessibility-migration-analysis**: Loads for `CONTRIBUTOR-DOCS/**/accessibility-migration-analysis.md` and points to the skill of the same name -- **consumer-migration-guide**: Loads for `gen2/packages/swc/components/*/migration-guide.mdx` and points to the skill of the same name - -##### Lessons (memory) - -- Files in `.ai/memory/` use the same frontmatter as rules and are generated as `memory-` instructions. `agnostic-lessons` applies to all files (`**`) and is excluded from code review; `css-styling-lessons` applies to `**/*.css`. - -### When rules and skills are activated - -**Path-scoped rules:** `styles`, `text-formatting`, `stories-documentation`, `stories-format`, `component-readme`, `contributor-doc-update`, `accessibility-migration-analysis`, `consumer-migration-guide`, and the two lessons files in `.ai/memory/` carry a `paths:` list — loaded only when a matching file is in context, deterministically, in every tool. Always-on guidance belongs in `AGENTS.md`, not in a rule. -**Skills:** Guidance with no natural file-path scope — `branch-naming`, `storybook-mdx-conversion`, `jira-ticket`, `github-description`, `code-conformance`, `consistency-pass`, `deep-understanding`, `migration-phase-awareness`, `contributor-docs-nav`, and the rest of the [Available skills](#available-skills) catalog — invoked on demand by the agent matching task intent, or by explicit request. - -| Rule/skill | Path-scoped rule | Skill (on-demand) | Glob / paths | -| -------------------------------- | :--------------: | :---------------: | --------------------------------------------------------- | -| branch-naming | | x | — | -| styles | x | | `**/*.css` | -| text-formatting | x | | `**/*.md`, `**/*.txt`, `**/*.mdx` | -| stories-documentation | x | x | `gen2/packages/…/*.mdx` (3 globs) | -| stories-format | x | x | `gen2/packages/…/stories/**` (3) | -| component-readme | x | | `1st-gen/packages/*/README.md` | -| contributor-doc-update | x | | `CONTRIBUTOR-DOCS/**` | -| accessibility-migration-analysis | x | x | `CONTRIBUTOR-DOCS/**/accessibility-migration-analysis.md` | -| consumer-migration-guide | x | x | `gen2/packages/swc/components/*/migration-guide.mdx` | -| memory: agnostic-lessons | x | | `**` | -| memory: css-styling-lessons | x | | `**/*.css` | -| storybook-mdx-conversion | | x | — | -| contributor-docs-nav | | x | — | -| deep-understanding | | x | — | -| code-conformance | | x | — | -| consistency-pass | | x | — | -| migration-phase-awareness | | x | — | -| github-description | | x | — | -| jira-ticket | | x | — | - -### Usage - -1. Rules load automatically while your coding agent works on matching files. To use a rule outside that trigger, mention it by name in the chat. -2. Skills load when a task matches their description, or when you name them. - -### Updating rules - -To modify these rules: - -1. Edit the file in `.ai/rules/` (never a generated copy) -2. Try to follow the existing structure and format where possible -3. Run `yarn ai:sync` and `yarn lint:ai` before committing - -## Skills - -Skills are used on-demand. When a task matches a skill’s purpose, the agent reads the skill file for workflows, patterns, and guidance. Skills live in the `skills` directory; each has a `SKILL.md` and may include references or scripts. - -### Available skills - -#### Accessibility migration analysis - -- **purpose**: Create accessibility migration analysis docs for the "analyze accessibility" step of gen2 component migration -- **How to invoke**: Say "create accessibility analysis for [component]", "analyze accessibility for [component]", or "accessibility migration for [component]". Also invoked when you refer to the "analyze accessibility" step in the gen2 component migration workstream. -- Use when: On the analyze-accessibility step for one or more components; creating one markdown file per component at `CONTRIBUTOR-DOCS/03_project-planning/03_components/[component-name]/accessibility-migration-analysis.md` -- Applies to: `CONTRIBUTOR-DOCS/**/accessibility-migration-analysis.md` -- Provides: Required section order, ARIA recommendations structure, Shadow DOM guidance, keyboard and focus conventions, testing table format, reference examples - -#### Accessibility compliance - -- **purpose**: Implement WCAG 2.2 compliant interfaces with mobile accessibility, inclusive design patterns, and assistive technology support -- **How to invoke**: Ask for an accessibility audit, ARIA implementation, screen reader support, WCAG compliance, or inclusive UX (e.g. “make this accessible”, “add keyboard nav”). Not tied to a file type; applies to any UI or component work. -- Use when: Auditing accessibility, implementing ARIA patterns, building for screen readers, or ensuring inclusive user experiences -- Provides: WCAG checklist, ARIA patterns (e.g. button, dialog, form), contrast requirements, testing tools - -#### Ask questions (`ask-questions-if-underspecified`) - -- **purpose**: Clarify requirements before implementing when the request is underspecified or ambiguous -- **How to invoke**: Agent-triggered when it detects multiple plausible interpretations or missing key details (scope, constraints, “done”). You can also say “I’m not sure about X” or “clarify before you start” to encourage it. -- Use when: Multiple plausible interpretations exist, or key details (scope, constraints, “done”) are unclear -- Workflow: Decide if underspecified → ask must-have questions → pause until answered → confirm then proceed - -#### Contributor docs navigation - -- **purpose**: Run the CONTRIBUTOR-DOCS nav script to update breadcrumbs and TOCs, and handle link verification -- **How to invoke**: Say “update contributor docs nav”, “regenerate TOC”, “fix broken links in CONTRIBUTOR-DOCS”, or “run the nav script”. Also pointed to by the `contributor-doc-update` path-scoped rule, which fires whenever a `CONTRIBUTOR-DOCS/**` file is in context. -- Use when: Updating contributor docs structure, regenerating navigation, or fixing reported broken links -- Provides: Operator workflow (run script, verify, fix links), Maintainer workflow (when to update script). Full instructions in `.ai/skills/contributor-docs-nav/references/ai-agent-instructions.md` - -#### Jira ticket - -- **purpose**: Draft and format Jira tickets — title, labels, severity, description — following Spectrum Web Components conventions -- **How to invoke**: Ask to create, draft, or format a Jira ticket (bug, RFC, or feature/research ticket) -- Use when: Writing a Jira ticket for a bug report, RFC, or feature/research request -- Provides: Jira markup syntax rules, title format, general/bug/RFC templates (RFC generates three sequential tickets: authoring, internal shepherding, external shepherding), severity classification (SEV1–SEV5), allowed labels and issue types - -#### GitHub description - -- **purpose**: Generate GitHub PR and issue descriptions — title, labels, and body — following Spectrum Web Components conventions -- **How to invoke**: Ask to create a GitHub PR or issue description; prompts for a linked ticket if none is provided -- Use when: Drafting a PR or issue description, including the required accessibility testing checklist -- Provides: Title format, PR template (author/reviewer checklists, manual test cases, accessibility testing checklist), severity classification, allowed labels - -#### Code conformance - -- **purpose**: Review gen2 component files against project style guides, run linters, and surface guideline gaps -- **How to invoke**: Say "check code conformance", "audit this component's style", or as part of the `migration-conformance` sub-task -- Use when: Reviewing or auditing gen2 TypeScript, CSS, test, or Storybook story files for style conformance -- Provides: Per-domain review checklists (TypeScript, CSS, tests, stories) with style-guide links, lint commands to run first, guideline-gap reporting format - -#### Consistency pass - -- **purpose**: Run a consistency and validity self-audit on changed files and the migration plan -- **How to invoke**: Say "consistency pass", "check my work", or "validity pass"; also run proactively before declaring a migration phase or significant task complete -- Use when: Before declaring any migration phase (especially API, styling, testing, documentation) or significant implementation task complete -- Provides: Code-conformance check (delegates to `code-conformance`), plan-validity check (implementation vs. plan, cascading updates across plan sections), reporting format - -#### Migration phase awareness - -- **purpose**: Keep multi-phase migration obligations in context during a session and emit Migration Checkpoint blocks at phase completion -- **How to invoke**: Active whenever a `migration-*` skill is in use or migration files are being edited -- Use when: Declaring a migration phase complete, or when work drifts away from an in-progress migration -- Provides: Phase-completion checklist (skill quality gate, plan alignment, plan checklist update, consistency pass, status table), Migration Checkpoint block format, resume-prompt format for drifted work - -#### Component migration (rendering and styling) - -- **purpose**: Create rendering-and-styling migration analysis docs for the “analyze rendering and styling” step of gen2 component migration -- **How to invoke**: Say “create migration analysis for [component]”, “analyze rendering and styling for [component]”, or “rendering and styling migration for [component]”. Also invoked when you refer to the “analyze rendering and styling” step in the gen2 component migration workstream. -- Use when: On the analyze-rendering-and-styling step for one or more components; creating one markdown file per component at `CONTRIBUTOR-DOCS/03_project-planning/03_components/[component-name]/rendering-and-styling-migration-analysis.md` -- Provides: Workflow summary (specs from CSS + SWC, three-way DOM comparison, CSS⇒SWC mapping table, summary). Full instructions in `CONTRIBUTOR-DOCS/03_project-planning/02_workstreams/02_gen2-component-migration/02_step-by-step/01_analyze-rendering-and-styling/cursor_prompt.md` - -#### Consumer migration guide - -- **purpose**: Create per-component migration guides for application developers upgrading from 1st-gen Spectrum Web Components to gen2 components -- **How to invoke**: Say “create a consumer migration guide for [component]”, “write an upgrade guide for [component]”, or “document how consumers migrate [component] from 1st-gen to gen2”. -- Use when: Writing one Storybook-renderable MDX file per component at `gen2/packages/swc/components/[component-name]/migration-guide.mdx` with code updates, styling guidance, accessibility notes, and rollout advice -- Provides: Workflow summary (verified source inputs, required section order, before/after examples, migration checklist, rollout guidance). Full instructions in `.ai/skills/consumer-migration-guide/references/consumer-migration-guide-prompt.md` - -#### Washing machine migration workflow - -#### Migration — phase 1: prep (`migration-prep`) - -- **purpose**: Understand the component, critically assess the current API and behavior, plan breaking changes, and define migration scope before any refactoring begins -- **How to invoke**: Say "start migration prep for [component]", "plan the migration for [component]", "create a migration plan for [component]", "draft the Phase 1 plan for [component]", or "phase 1 migration for [component]" -- Use when: Beginning a 1st-gen → gen2 component migration; before any files are created or code is moved -- Provides: Template-backed migration plan workflow, research checklist (1st-gen API, usage, tests, analyses, React/Figma references), breaking-change analysis, source-confidence and contradiction checks, path/link verification, and staff-level API/naming review with explicit escalation for inconsistencies - -#### Migration — phase 2: setup (`migration-setup`) - -- **purpose**: Create the gen2 file and folder structure, wire up exports, and confirm the build passes before implementation begins -- **How to invoke**: Say "set up gen2 structure for [component]", "create the file structure for [component]", or "phase 2 migration for [component]" -- Use when: After prep is complete and the approved `migration-plan.md` is available; creating the scaffolding a component needs before any logic is ported -- Provides: File/folder creation checklist, export wiring steps, build-passes verification, and plan-aligned naming/structure setup - -#### Migration — phase 3: API (`migration-api`) - -- **purpose**: Move properties, methods, and types from 1st-gen to gen2 while maintaining a clear public API -- **How to invoke**: Say "migrate the API for [component]", "port properties and methods for [component]", or "phase 3 migration for [component]" -- Use when: Scaffolding is in place and the approved `migration-plan.md` defines the intended public contract for gen2 -- Provides: Property/method porting workflow, type definition guidance, API contract review, and drift detection against the approved migration plan - -#### Migration — phase 4: accessibility (`migration-a11y`) - -- **purpose**: Implement WCAG-aligned semantics, ARIA, keyboard support, and focus management, and document accessibility behavior -- **How to invoke**: Say "migrate accessibility for [component]", "implement a11y for [component]", or "phase 4 migration for [component]" -- Use when: API is in place and the approved `migration-plan.md` plus accessibility analysis define the must-ship semantics and behavior -- Provides: WCAG checklist, ARIA pattern guidance, keyboard/focus requirements, a11y documentation template, and checks against approved accessibility changes in the migration plan - -#### Migration — phase 5: styling (`migration-styling`) - -- **purpose**: Migrate CSS to the gen2 structure, apply Spectrum 2 tokens, and ensure stylelint passes -- **How to invoke**: Say "migrate styling for [component]", "port CSS for [component]", or "phase 5 migration for [component]" -- Use when: Accessibility is complete and the approved `migration-plan.md` defines the intended visual scope; translating 1st-gen CSS to gen2 with Spectrum 2 design tokens -- Provides: CSS migration checklist, token mapping guidance, stylelint validation steps, and checks against approved visual scope and custom-property decisions - -#### Migration — phase 6: testing (`migration-testing`) - -- **purpose**: Write unit tests, accessibility tests, and Storybook play functions for a migrated component -- **How to invoke**: Say "write tests for [component] migration", "add migration tests for [component]", or "phase 6 migration for [component]" -- Use when: Implementation is feature-complete and the approved `migration-plan.md` can be used to derive the must-ship test matrix before review -- Provides: Test coverage checklist, unit/a11y/play-function patterns, test-running verification, and plan-driven coverage checks for breaking changes and regressions - -#### VRT authoring (`vrt-authoring`) - -- **purpose**: Author dedicated Storybook visual regression stories for gen2 components -- **How to invoke**: Say "add VRT for [component]", "write visual regression stories", or mention `.vrt.ts`, Chromatic, forced-colors VRT, global styles VRT, or custom-property VRT -- Use when: Adding or reviewing `test/vrt/*.vrt.ts` files during migration or test cleanup -- Provides: Dedicated VRT file shape, shared helper usage, pseudo-state/forced-colors patterns, and custom-property coverage checks against the generated API metadata - -#### Migration — conformance sub-task (`migration-conformance`) - -- **purpose**: Verify all migrated files conform to project style guides, run all linters, and surface any guideline gaps as PR comment notes -- **How to invoke**: Say "check code conformance for [component]", "run conformance checks for [component]", "style guide review for [component]", or "conformance for [component] migration" -- Use when: Phase 6 (migration-testing) is complete and all tests pass; reviewing TypeScript, CSS, test files, and Storybook stories against their respective style guides before documentation -- Provides: Four-domain review workflow (TypeScript, CSS, tests, stories), linter run commands, per-file-type style guide references, and a guideline-gap documentation pattern for surfacing improvements in the PR - -#### Migration — phase 7: documentation (`migration-documentation`) - -- **purpose**: Author the per-component MDX docs page and finalize Storybook stories + public-API JSDoc so the component is usable and understandable by others -- **How to invoke**: Say "write docs for [component] migration", "document [component] for gen2", or "phase 7 migration for [component]" -- Use when: Tests pass and the approved `migration-plan.md` can be used as the source of truth for migration notes and rationale -- Provides: per-component MDX authoring (`.mdx`), public-API JSDoc guidelines on `Component.ts`, stories file finalization (drop `'autodocs'` from Playground, complete Accessibility story), documentation checklist, and plan-aligned migration-note guidance - -#### Stories format (`stories-format`) - -- **purpose**: Full reference for structuring gen2 Storybook stories files -- **How to invoke**: Say "write stories for [component]", "review the stories file", or "migrate the stories for [component]". The `stories-format` rule loads automatically for files under `stories/` and points here. -- Use when: Writing, migrating, or reviewing a gen2 `.stories.ts` file -- Provides: File structure and section separators, meta configuration, layout and decorators, story naming and ordering, tags, story types, JSDoc, accessibility requirements, and image assets - -#### Stories documentation (`stories-documentation`) - -- **purpose**: Full authoring procedure for the per-unit MDX docs page of gen2 components, internal components, patterns, and controllers -- **How to invoke**: Say "write the docs page for [component]" or "review [component].mdx". The `stories-documentation` rule loads automatically for per-unit MDX files and points here. -- Use when: Writing, migrating, or reviewing a gen2 `.mdx` docs page -- Provides: Documentation structure, the Helpers section, section patterns, 1st-gen to gen2 comparison, verification against source, and general writing guidelines - -#### Migration — phase 8: review (`migration-review`) - -- **purpose**: Run final checks, verify lint/tests/build/Storybook, update the workstream status table, and open a PR -- **How to invoke**: Say "review [component] migration", "final checks for [component]", or "phase 8 migration for [component]" -- Use when: Documentation is complete and the approved `migration-plan.md` can be used as the review baseline; preparing the migration for code review and merge -- Provides: Pre-PR checklist (lint, tests, build, Storybook), workstream status update steps, PR description guidance, and verification that code/docs/tests still match the approved migration plan - -#### Deep understanding (`deep-understanding`) - -- **purpose**: Require a thorough deep-read of the relevant codebase before planning or implementing; write findings to a persistent markdown file (e.g. `research.md`) so the user can review and correct before any work proceeds -- **How to invoke**: Applied intelligently by the agent for non-trivial work (multiple files, new area, complex behavior) before planning or writing code — no rule enforces this automatically. Say “read this folder in depth and write research.md” or “study [system] in great detail” to invoke or reinforce it explicitly. -- Use when: The task is non-trivial and would benefit from a written understanding pass first; skip it for simple, self-contained requests (a one-line fix, a single known file, a quick question) -- Provides: Workflow (scope → deep read → write report → pause for review → proceed only after validation). Written artifact is the review surface - -#### Conventional commits - -- **purpose**: Create conventional commit messages following the conventional commits specification -- **How to invoke**: Ask for a commit message when committing (e.g. “write a commit message for these changes”, “commit this”, “suggest a commit message”). Not tied to a file type; applies when you’re about to run `git commit`. -- Use when: Committing code changes, writing commit messages, or formatting git history -- Provides: Format (type(scope): subject, body, footer), type list (feat, fix, docs, etc.), examples including breaking changes - -#### Documentation (`documentation-standards`) - -- **purpose**: Follow Adobe content writing standards when writing documentation -- **How to invoke**: Use when writing or editing docs — e.g. per-unit MDX docs pages (`.mdx`), public-API JSDoc in `Component.ts`, the meta-level JSDoc in `.stories.ts`, README/changeset/Jira/PR (`.md`, `.mdx`), or when you say “write the PR description”, “draft the Jira ticket”, “write the docs for this component”. -- Use when: Authoring gen2 docs pages, writing 1st-gen docs, changesets, Jira tickets, or PR descriptions -- Provides: Voice and tone, grammar and mechanics, markdown/JSDoc reference, links to Spectrum design system content guidelines - -#### Explain code - -- **purpose**: Explain code with visual diagrams and analogies -- **How to invoke**: Ask “how does this work?”, “explain this code”, “walk me through this”, or “what does this do?”. Not tied to a file type; use on any code or file you want explained. -- Use when: Explaining how code works, teaching about the codebase, or when the user asks “how does this work?” -- Approach: Analogy → diagram → step-by-step walkthrough → highlight gotchas - -#### Session retrospective - -- **purpose**: Document lessons learned after completing work, especially when the user corrected planning documents or implementation; maintains persistent lesson files in `.ai/memory/` that future agents read at session start -- **How to invoke**: Say "document what you learned", "add to lessons", "remember this", or "run a retrospective". Also triggered when the user corrects your work or you encounter a surprising constraint. -- Use when: User corrects your work, you hit a non-obvious tool limitation, or at session end after substantial work -- Provides: Workflow for capturing lessons, format guidelines, naming convention (`-lessons.md` in `.ai/memory/`) - -#### Session handoff - -- **purpose**: Create handoff documents so another agent (or a later session) can continue work with full context -- **How to invoke**: Say “create handoff”, “save state”, “I need to pause”, “context is getting full”, or “load handoff” / “resume from” / “continue where we left off”. The agent may also suggest a handoff after substantial work (e.g. many file edits, complex debugging). -- Use when: User requests handoff/save state, context is getting full, major milestone reached, or resuming with “load handoff” / “continue where we left off” -- Provides: CREATE and RESUME workflows, scripts (create, list, validate, check staleness), handoff chaining - -#### Test-driven development - -- **purpose**: Write a failing test first, then minimal code to pass, then refactor (red–green–refactor) -- **How to invoke**: Ask to implement a feature or fix a bug (e.g. “add feature X”, “fix this bug”); the agent may use TDD by default. To invoke explicitly, say “use TDD”, “write tests first”, or “red-green-refactor”. -- Use when: Implementing any feature or bugfix, before writing implementation code -- Provides: TDD cycle, verification checklist, good/bad test examples, anti-patterns to avoid - -Trigger phrases for each migration phase ("Phase 4 migration for [component]") and for lesson capture ("remember this", "log this lesson") are in each skill's `description`, so agents match them without a separate prompt list. - -## Using rules and skills across tools and IDEs - -Canonical content lives in **`.ai/`** (this directory). Tool-specific directories (`.cursor/`, `.claude/`) are thin adapters that point back here via symlinks — edit files in `.ai/`, never in the adapter directories. - -### Current adapter structure +| Concept | Edit here | Tools read it from | +| ---------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| Always-on instructions | `AGENTS.md` files (root and nested) | The same files | +| Path-scoped rules | `.ai/rules/.md` | `.github/instructions/.instructions.md` and `.cursor/rules/.mdc` (generated); `.claude/rules` (symlink) | +| Lessons | `.ai/memory/-lessons.md` | Generated as `memory--lessons` instructions | +| Skills | `.ai/skills//SKILL.md` | `.claude/skills` and `.cursor/skills` (symlinks); Copilot discovers skills through `.claude/skills` | ```text -.ai/rules/ -└── *.md ← canonical, tool-agnostic source of truth (edit these) - -.ai/skills/ -└── /SKILL.md ← canonical, tool-agnostic source of truth (edit these) - -.github/instructions/ -└── *.instructions.md GENERATED by `yarn ai:sync` (Copilot reads `applyTo`) - -.cursor/rules/ -└── *.mdc GENERATED by `yarn ai:sync` (Cursor reads `globs`) -.cursor/skills/ → ../.ai/skills/ (directory symlink) - -.claude/rules/ → ../.ai/rules/ (directory symlink; Claude Code reads `paths`) -.claude/skills/ → ../.ai/skills/ (directory symlink; also how Copilot discovers skills) +.ai/ +├── rules/*.md ← edit: path-scoped instructions +├── memory/*.md ← edit: lessons, same schema as rules +├── skills//SKILL.md ← edit: on-demand skills, one level deep +└── scripts/ ← yarn ai:sync and yarn lint:ai + +.github/instructions/*.instructions.md GENERATED by `yarn ai:sync` (Copilot reads `applyTo`) +.cursor/rules/*.mdc GENERATED by `yarn ai:sync` (Cursor reads `globs`) +.claude/rules → ../.ai/rules symlink (Claude Code reads `paths`) +.claude/skills → ../.ai/skills symlink (Copilot and Claude Code) +.cursor/skills → ../.ai/skills symlink (Cursor) ``` -Edit only `.ai/`. Generated files start with a `GENERATED by .ai/scripts/sync.js` comment; `yarn lint:ai` fails if one is edited by hand or falls out of date. +Edit only `.ai/` sources and `AGENTS.md` files. Generated files start with a `GENERATED by .ai/scripts/sync.js` comment, and `yarn lint:ai` fails if one is edited by hand or falls out of date. The pre-commit hook runs `yarn ai:sync` whenever `.ai/` or an `AGENTS.md` file is staged. It skips the sync with a warning if `.ai/rules/`, `.ai/memory/`, `.ai/skills/`, or this README has unstaged changes, and the pre-push hook regenerates any out-of-date files and blocks the push until you commit them. -Removing a symlink with `rm` removes only the link, not its target, so it's safe for cleaning up an adapter. +Glob semantics follow GitHub's `applyTo`: `*` stays within one directory and `**` crosses directories. `*.css` matches only root-level files, while `**/*.css` matches CSS anywhere. -### Adding a new rule +## Rules or skills: how to choose -> Before adding a rule, decide whether the guidance has a natural file-path scope. If it does, give it a `paths:` list so it loads deterministically in every tool (see [Rules vs. skills: how to choose](#rules-vs-skills-how-to-choose)). If it doesn't — the guidance is about a task or intent, not a file path — write it as a skill instead. Guidance that must always apply goes in [`AGENTS.md`](../AGENTS.md). +- **Path-scoped rule:** the guidance is tied to a set of file paths, such as "when editing a `.stories.ts` file." A `paths:` list makes it load deterministically whenever a matching file is in context, so it never goes unread because a task's intent wasn't explicit. Keep rules short (under 12 KB); they load in full for every matching file. +- **Skill:** the guidance is tied to a task or intent, such as "draft a Jira ticket." The agent loads it when the task matches the skill's `description`, or when you name the skill. +- **`AGENTS.md`:** the guidance applies to everything. Copilot combines instruction files without any precedence, so always-on guidance lives in one place and must never contradict a rule. -1. Create `rule-name.md` in `.ai/rules/` with YAML frontmatter: - - `description:` — what the rule covers - - `paths:` — a YAML list of globs, one item per glob. **Quote each item** — an unquoted value starting with `*` (e.g. `**/*.mdx`) parses as an invalid YAML alias, not a literal string. Use `**/` to match in any directory; `*` alone stays in one directory - - `excludeAgent:` — optional; `code-review` or `cloud-agent` when that GitHub agent shouldn't use the rule -2. Run `yarn ai:sync` to generate `.github/instructions/rule-name.instructions.md` and `.cursor/rules/rule-name.mdc`, and commit them with the source. The pre-commit hook does this for you unless the rule has unstaged changes. -3. Run `yarn lint:ai`. It fails if a glob matches no tracked file. -4. Register it in the tables in this README (rules catalog). +When a rule mixes a short constraint with a long procedure, keep the constraint in the rule and move the procedure into a skill of the same name. The rule points to the skill; `stories-documentation` and `stories-format` work this way. -### Adding a new skill +## Authoring -1. Create `.ai/skills//SKILL.md` with `name` and `description` frontmatter. Skills are for task/intent-scoped guidance with no natural file-path trigger — if the guidance belongs to a specific set of files, write a path-scoped rule instead (see above) so it loads deterministically rather than depending on the agent matching intent. -2. Register it in the skills catalog below and in [`AGENTS.md`](../AGENTS.md). -3. Both `.cursor/skills/` and `.claude/skills/` pick it up automatically via directory symlinks. +### Add a path-scoped rule -### Symlink setup +1. Create `.ai/rules/.md`: -The three directory symlinks (`.claude/rules`, `.claude/skills`, and `.cursor/skills`) are committed to the repo, so **no setup is required after cloning**. Cursor rules are generated files, not symlinks. + ```yaml + --- + description: What the rule covers. + paths: + - '**/*.css' # quote every glob; use `**/` to match in any directory + excludeAgent: code-review # optional: code-review or cloud-agent + --- + ``` -#### Recreating broken symlinks +2. Run `yarn ai:sync` to generate the Copilot and Cursor copies, and commit them with the source. The pre-commit hook does this for you unless a source has unstaged changes. +3. Run `yarn lint:ai`. It fails if a glob matches no tracked file. -If a symlink is accidentally deleted or broken, recreate it from the repository root: +### Add a lesson + +Use the `session-retrospective` skill, or add a bullet to the matching `.ai/memory/-lessons.md` file. Lessons files use the same frontmatter as rules, so they load for the files they describe. + +### Add a skill + +1. Create `.ai/skills//SKILL.md`. The directory name must equal `name`: lowercase letters, digits, and single hyphens, 64 characters or fewer. + + ```yaml + --- + name: my-skill + description: What the skill does. Use when … (1,024 characters or fewer) + --- + ``` + +2. Put supporting files in the skill's own `references/`, `assets/`, or `scripts/` folder and link them from `SKILL.md` with relative paths. VS Code only loads files that `SKILL.md` links to. +3. Never pre-approve `shell`, `bash`, or `*` in `allowed-tools`. +4. Run `yarn ai:sync` to refresh the catalog below, then `yarn lint:ai`. + +### Validate + +- `yarn lint:ai` runs `.ai/scripts/validate.js`: story tags, links, commit and branch conventions (against commitlint), instruction and skill frontmatter, symlinks, generated files, and per-unit MDX docs pages. The header of `validate.js` describes each check. The `.ai/` checks read only files git tracks, including staged new files, so untracked or ignored local files such as tool caches and `.ai/handoffs/` notes never fail it. It then runs `.ai/scripts/sync.test.js`, which confirms that damaged catalog markers fail the check instead of skipping it. +- `yarn ai:sync` reads tracked and untracked sources but skips gitignored ones, so a local ignored file never reaches generated output. It fails if the catalog markers in this README are missing, duplicated, or out of order. +- `yarn lint:docs-pages` runs the per-unit MDX docs-page check on its own. +- To confirm what Copilot loads, run `copilot instruction list` and `copilot skill list --json` from the repository root. In VS Code, run **Chat: Open Customizations** with the Copilot harness selected. + +## Catalog + + + +_Generated by `yarn ai:sync` from frontmatter. Do not edit this block by hand._ + +### Instructions + +- **[`rules/accessibility-migration-analysis.md`](./rules/accessibility-migration-analysis.md)** (`CONTRIBUTOR-DOCS/**/accessibility-migration-analysis.md`; not used by code-review): Points to the accessibility-migration-analysis skill when an accessibility migration analysis document is being written or edited. +- **[`rules/component-readme.md`](./rules/component-readme.md)** (`1st-gen/packages/*/README.md`): Guidelines for component README documentation structure and accessibility compliance +- **[`rules/consumer-migration-guide.md`](./rules/consumer-migration-guide.md)** (`gen2/packages/swc/components/*/migration-guide.mdx`; not used by code-review): Points to the consumer-migration-guide skill when a gen2 component consumer migration guide is being written or edited. +- **[`rules/contributor-doc-update.md`](./rules/contributor-doc-update.md)** (`CONTRIBUTOR-DOCS/**`; not used by code-review): Useful for updating auto-generated navigation and validating links in the contributor docs +- **[`rules/stories-documentation.md`](./rules/stories-documentation.md)** (`gen2/packages/swc/components/*/*.mdx`, `gen2/packages/swc/patterns/*/*/*.mdx`, `gen2/packages/core/controllers/*/*.mdx`): Authoring guide for the per-unit MDX docs page for gen2 components, internal components, patterns, and controllers. Covers section content, accessible examples, and 1st-gen comparison notes. Story prose lives in MDX, not in JSDoc above story exports. +- **[`rules/stories-format.md`](./rules/stories-format.md)** (`gen2/packages/swc/components/*/stories/**`, `gen2/packages/swc/patterns/*/*/stories/**`, `gen2/packages/core/controllers/*/stories/**`): Enforces consistent file structure, section separators, meta configuration, story tags, and layout parameters for gen2 Storybook stories files. Story prose lives in per-unit MDX; the stories file is definitions-only. +- **[`rules/styles.md`](./rules/styles.md)** (`**/*.css`): Rules for consistent styling in component CSS +- **[`rules/text-formatting.md`](./rules/text-formatting.md)** (`**/*.md`, `**/*.txt`, `**/*.mdx`): Text formatting and capitalization rules for documentation and tickets +- **[`memory/agnostic-lessons.md`](./memory/agnostic-lessons.md)** (`**`; not used by code-review): Accumulated lessons for agents working in this repository, covering path depths, module format, repository layout, Storybook tag conventions, and documentation style. +- **[`memory/css-styling-lessons.md`](./memory/css-styling-lessons.md)** (`**/*.css`): Accumulated CSS lessons for component styling in this repository, covering selector syntax, custom property consumption, shorthands and fallback chains, variant states, and linter rewrites. + +### Skills + +- **[`accessibility-compliance`](./skills/accessibility-compliance/SKILL.md)**: Implement WCAG 2.2 compliant interfaces with mobile accessibility, inclusive design patterns, and assistive technology support. Use when auditing accessibility, implementing ARIA patterns, building for screen readers, or ensuring inclusive user experiences. +- **[`accessibility-migration-analysis`](./skills/accessibility-migration-analysis/SKILL.md)**: Create accessibility migration analysis docs for gen2 component migration. Use when on the "analyze accessibility" step for one or more components. +- **[`ask-questions-if-underspecified`](./skills/ask-questions-if-underspecified/SKILL.md)**: Clarify requirements before implementing. Use when serious doubts arise. +- **[`branch-naming`](./skills/branch-naming/SKILL.md)**: Suggests the preferred branch name format for Spectrum Web Components contributions (lowercase, dash-separated, a conventional commit type, and an optional issue number). Use when creating, renaming, or checking a git branch name. +- **[`clone-gen2-migration-epic`](./skills/clone-gen2-migration-epic/SKILL.md)**: Clones the SWC-1727 component migration epic template and all 7 child stories into new Jira issues, replacing all [COMPONENT] placeholders with the target component name. TRIGGER whenever a user asks to create a new component migration epic, clone the migration template, scaffold a new component migration in Jira, or says anything like "set up migration tickets for [component]", "create the migration epic for [component]", or "clone 1727 for [component]". Requires the component name as input — always ask for it if not provided. +- **[`clone-new-component-epic`](./skills/clone-new-component-epic/SKILL.md)**: Clones the SWC-2164 component migration epic template and all 7 child stories into new Jira issues, replacing all [COMPONENT] placeholders with the target component name. TRIGGER whenever a user asks to create a new component epic, clone the new component template, scaffold a new component epic in Jira, or says anything like "set up new component tickets for [component]", "create the new component epic for [component]", or "clone 2164 for [component]". Requires the component name as input — always ask for it if not provided. +- **[`code-conformance`](./skills/code-conformance/SKILL.md)**: Review gen2 component files against project style guides, run linters, and surface guideline gaps. Apply whenever reviewing or auditing gen2 component code for style conformance. +- **[`component-migration-analysis`](./skills/component-migration-analysis/SKILL.md)**: Create rendering-and-styling migration analysis docs for gen2 component migration. Use when on the "analyze rendering and styling" step for one or more components. +- **[`consistency-pass`](./skills/consistency-pass/SKILL.md)**: Defines when and how to run a consistency and validity self-audit on changed files and the migration plan. Apply before declaring any migration phase or significant implementation task complete. +- **[`consumer-migration-guide`](./skills/consumer-migration-guide/SKILL.md)**: Use when creating a per-component migration guide for application developers upgrading from Spectrum 1 Web Components to Spectrum 2 components. +- **[`contributor-docs-nav`](./skills/contributor-docs-nav/SKILL.md)**: Run the CONTRIBUTOR-DOCS nav script to update breadcrumbs and TOCs, and handle link verification. Use when updating contributor docs structure, regenerating navigation, or fixing broken links. +- **[`conventional-commits`](./skills/conventional-commits/SKILL.md)**: Create conventional commit messages following best conventions. Use when committing code changes, writing commit messages, or formatting git history. Follows conventional commits specification. +- **[`deep-understanding`](./skills/deep-understanding/SKILL.md)**: Require a thorough deep-read of the relevant codebase before planning or implementing; write findings to a persistent markdown file (e.g. research.md) so the user can review and correct before any work proceeds. +- **[`documentation-standards`](./skills/documentation-standards/SKILL.md)**: When writing documentation in a variety of scenarios, follow the Adobe content writing standards. +- **[`explain-code`](./skills/explain-code/SKILL.md)**: Explains code with visual diagrams and analogies. Use when explaining how code works, teaching about a codebase, or when the user asks "how does this work?" +- **[`github-description`](./skills/github-description/SKILL.md)**: Generates GitHub pull request and issue descriptions — title, labels, and body — following Spectrum Web Components conventions and the repository's pull request template. Prompts for a linked ticket if none is provided. Use when asked to write or draft a PR description, PR body, or GitHub issue. +- **[`jira-ticket`](./skills/jira-ticket/SKILL.md)**: Drafts and formats Spectrum Web Components Jira tickets (title, labels, severity, issue type, and required sections, using Jira markup) for bugs, features, research, and RFCs. Use when asked to create, draft, format, or review a Jira ticket. +- **[`migration-a11y`](./skills/migration-a11y/SKILL.md)**: Phase 4 of 1st-gen to gen2 component migration. Use to implement WCAG-aligned semantics, ARIA, keyboard support, and focus management, and document accessibility behavior. +- **[`migration-api`](./skills/migration-api/SKILL.md)**: Phase 3 of 1st-gen to gen2 component migration. Use to move properties, methods, and types from 1st-gen to gen2 while maintaining a clear public API. +- **[`migration-conformance`](./skills/migration-conformance/SKILL.md)**: Sub-task after Phase 6 of 1st-gen to gen2 component migration. Use to verify all migrated files conform to the project style guides, run linters, and surface any guideline gaps as PR comment notes. +- **[`migration-documentation`](./skills/migration-documentation/SKILL.md)**: Phase 7 of 1st-gen to gen2 component migration. Use to author the per-component MDX docs page and finalize Storybook stories so the component is usable and understandable by others. +- **[`migration-phase-awareness`](./skills/migration-phase-awareness/SKILL.md)**: Keeps multi-phase migration obligations in context during a session. Emits Migration Checkpoint blocks at phase completion for cross-session continuity. Apply whenever any migration-\* skill is active or migration files are being edited. +- **[`migration-prep`](./skills/migration-prep/SKILL.md)**: Phase 1 of 1st-gen to gen2 component migration. Use to understand the component, plan breaking changes, and define scope before any refactoring begins. +- **[`migration-review`](./skills/migration-review/SKILL.md)**: Phase 8 of 1st-gen to gen2 component migration. Use to run final checks, verify lint/tests/build/Storybook, update the workstream status table, and open a PR. +- **[`migration-setup`](./skills/migration-setup/SKILL.md)**: Phase 2 of 1st-gen to gen2 component migration. Use to create the gen2 file and folder structure, wire up exports, and confirm the build passes before implementation begins. +- **[`migration-styling`](./skills/migration-styling/SKILL.md)**: Phase 5 of 1st-gen to gen2 component migration. Use to migrate CSS to the gen2 structure, apply Spectrum 2 tokens, and ensure stylelint passes. +- **[`migration-testing`](./skills/migration-testing/SKILL.md)**: Phase 6 of 1st-gen to gen2 component migration. Use to write unit tests, accessibility tests, and Storybook play functions for a migrated component. +- **[`session-handoff`](./skills/session-handoff/SKILL.md)**: Creates comprehensive handoff documents for seamless AI agent session transfers. Triggered when: (1) user requests handoff/memory/context save, (2) context window approaches capacity, (3) major task milestone completed, (4) work session ending, (5) user says 'save state', 'create handoff', 'I need to pause', 'context is getting full', (6) resuming work with 'load handoff', 'resume from', 'continue where we left off'. Proactively suggests handoffs after substantial work (multiple file edits, complex debugging, architecture decisions). Solves long-running agent context exhaustion by enabling fresh agents to continue with zero ambiguity. +- **[`session-retrospective`](./skills/session-retrospective/SKILL.md)**: Document lessons learned after completing work, especially when the user corrected planning documents or implementation. Creates and maintains topic lessons files in .ai/memory/ that load as path-scoped instructions. Use when the user says "remember this", "log this lesson", "save this for next time", "add to memory", or "review the session and update memory", when the user corrects your work, or after substantial work. +- **[`stories-documentation`](./skills/stories-documentation/SKILL.md)**: Full authoring procedure for the per-unit MDX docs page of gen2 components, internal components, patterns, and controllers: documentation structure, the Helpers section, section patterns (overview, anatomy, options, states, behaviors, accessibility), 1st-gen to gen2 comparison, verification against source so docs don't claim attributes, slots, or ARIA that don't exist, and general writing guidelines. Use when writing, migrating, or reviewing a gen2 `.mdx` docs page. +- **[`stories-format`](./skills/stories-format/SKILL.md)**: Full reference for structuring gen2 Storybook stories files: file structure and section separators, meta configuration, layout and decorators, story naming and ordering, tags, story types (Playground, Overview, Anatomy, Options, States, Behaviors, Accessibility), JSDoc, accessibility requirements, and image assets. Use when writing, migrating, or reviewing a gen2 `.stories.ts` file. +- **[`storybook-mdx-conversion`](./skills/storybook-mdx-conversion/SKILL.md)**: Converts a standalone Markdown document to Storybook MDX by adding the imports and Meta tag and converting HTML comments to JSX comments, without altering any other content. Use when asked to convert a .md file to .mdx by hand, outside the automated contributor-docs pipeline. +- **[`test-driven-development`](./skills/test-driven-development/SKILL.md)**: Use when implementing any feature or bugfix, before writing implementation code +- **[`token-update`](./skills/token-update/SKILL.md)**: Dispatches to the correct design token update workflow (custom tokens only vs @adobe/spectrum-tokens version bump). Use whenever asked to update, bump, or upgrade design tokens, or when told to run a token update. +- **[`vrt-authoring`](./skills/vrt-authoring/SKILL.md)**: Author dedicated Storybook visual regression stories for gen2 components. Use when adding or reviewing `.vrt.ts` files, Chromatic VRT coverage, forced-colors coverage, pseudo-state snapshots, global stylesheet coverage, or custom-property VRT coverage. + + + +## Tool setup + +Nothing needs to be installed or configured after cloning. + +- **GitHub Copilot:** the CLI, the GitHub Copilot app, VS Code, the cloud agent, and code review all read `AGENTS.md` and `.github/instructions/`. The CLI, app, VS Code, and cloud agent also read skills from `.claude/skills`. `.vscode/settings.json` stops VS Code's local agent from loading `.claude/rules` on top of `.github/instructions`, and turns on nested `AGENTS.md` files. +- **Claude Code:** reads `.ai/rules` and `.ai/skills` through the `.claude/` symlinks. +- **Cursor:** reads the generated `.cursor/rules/*.mdc` files and `.ai/skills` through the `.cursor/skills` symlink. If Cursor doesn't pick up a change, reload the window. +- **Windows without symlink support:** enable Developer Mode and `git config core.symlinks true`, then re-clone. As a fallback for the Copilot CLI, run `copilot skill add /.ai/skills` once. +- **Other tools:** start from `AGENTS.md`, and reference `.ai/rules/` and `.ai/skills//SKILL.md` directly when prompting. + +### Recreating broken symlinks + +From the repository root: ```sh mkdir -p .claude .cursor @@ -426,21 +161,11 @@ ln -s ../.ai/skills .claude/skills ln -s ../.ai/skills .cursor/skills ``` -Verify with `ls -la .claude/ .cursor/`, then run `yarn lint:ai`. If Cursor rules are missing or stale, run `yarn ai:sync`. If Cursor doesn't pick up changes, reload the window: `Cmd+Shift+P` → "Developer: Reload Window". - -### Using rules and skills in other environments - -If you use a tool that does not read `.cursor/` or `.claude/`, point it at `.ai/` directly: - -- **Start from [`AGENTS.md`](../AGENTS.md)** at the repository root. -- **Reference files when prompting** — for example: “Follow the rules in `.ai/rules/` and load `.ai/skills/deep-understanding/SKILL.md` for this task.” -- **Copy or adapt** the markdown and JSON content into your tool’s own config format as needed. +Then run `yarn lint:ai`. Removing a symlink with `rm` removes only the link, not its target. -## MCPs +## Optional contributor setup -When developing for the SWC project, there may be instances where your coding agent needs context from external sources. Contributors and maintainers can configure [MCP (Model Context Protocol) servers](https://modelcontextprotocol.io/docs/getting-started/intro) via [Easy MCP](https://wiki.corp.adobe.com/display/assetscollab/Cursor+integration+with+Easy+MCP). Some recommended MCP servers might include: +These are personal choices. The repository commits no MCP servers, hooks, or tool permissions. -- Figma -- Corp Jira -- Adobe Wiki Confluence -- React Spectrum 2 +- **MCP servers:** add them to your own configuration, for example with `copilot mcp add`, which writes `~/.copilot/mcp-config.json`. Adobe contributors can configure MCP servers through [Easy MCP](https://wiki.corp.adobe.com/display/assetscollab/Cursor+integration+with+Easy+MCP); useful ones include Figma, Corp Jira, Adobe Wiki Confluence, and React Spectrum 2. Adobe policy decides which servers are approved. +- **Narrow tool approvals:** instead of allowing everything, approve only what you need for a session, for example `copilot --allow-tool='shell(yarn lint:*)' --allow-tool='shell(git status)'`. diff --git a/.ai/scripts/ai-files.js b/.ai/scripts/ai-files.js index 82c4fd8e08f..24c1cec57e6 100644 --- a/.ai/scripts/ai-files.js +++ b/.ai/scripts/ai-files.js @@ -161,8 +161,12 @@ export function listSkills() { } let trackedCache = null; +let trackedSetCache = null; -/** Every file tracked by git, as repository-relative POSIX paths. */ +/** + * Every file tracked by git, as repository-relative POSIX paths. This reads the index, so + * staged new files count as tracked and untracked or ignored local files don't. + */ export function trackedFiles() { if (!trackedCache) { trackedCache = execFileSync('git', ['ls-files', '-z'], { @@ -176,6 +180,41 @@ export function trackedFiles() { return trackedCache; } +/** True when the absolute path `file` is tracked by git. */ +export function isTracked(file) { + trackedSetCache ??= new Set(trackedFiles()); + return trackedSetCache.has(rel(file)); +} + +let visibleSetCache = null; + +/** + * True when the absolute path `file` is tracked or untracked but not ignored by git. These + * are the files the git hooks see, so gitignored local files never reach generated output. + */ +export function isVisible(file) { + visibleSetCache ??= new Set( + execFileSync( + 'git', + ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], + { cwd: ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 } + ) + .split('\0') + .filter(Boolean) + ); + return visibleSetCache.has(rel(file)); +} + +/** + * Absolute paths of tracked files that pass `filter` (given the repository-relative path) + * and still exist on disk, so a deletion that isn't staged yet is skipped. + */ +export const trackedFilesOnDisk = (filter) => + trackedFiles() + .filter(filter) + .map((f) => path.join(ROOT, f)) + .filter((f) => existsSync(f)); + if (typeof path.matchesGlob !== 'function') { console.error( `yarn lint:ai and yarn ai:sync need Node 22.5 or later (found ${process.version}). Run \`nvm use\` to switch to the version in .nvmrc.` diff --git a/.ai/scripts/sync.js b/.ai/scripts/sync.js index a7c78bda540..b077be0159b 100644 --- a/.ai/scripts/sync.js +++ b/.ai/scripts/sync.js @@ -19,13 +19,19 @@ * .ai/rules/.md → .github/instructions/.instructions.md (Copilot) * → .cursor/rules/.mdc (Cursor) * .ai/memory/.md → the same targets, named memory- (only with frontmatter) + * skill and rule frontmatter → the catalog block in .ai/README.md, between + * and * * Claude Code reads `.ai/rules` directly through the `.claude/rules` symlink, so it needs * no generated copy. * * Usage: * node .ai/scripts/sync.js Write generated files (`yarn ai:sync`) - * node .ai/scripts/sync.js --check Report drift without writing; exits 1 on drift + * node .ai/scripts/sync.js --check Report drift without writing; exits 1 on drift. + * Checks only git-tracked sources and generated files. + * + * Both modes fail when the catalog markers in .ai/README.md are missing, duplicated, or out + * of order, so a damaged marker can't silently turn off the catalog check. */ import { @@ -44,12 +50,20 @@ import { fileURLToPath } from 'url'; import { stringify as stringifyYaml } from 'yaml'; import { + AI_DIR, GENERATED_MARKER, + isTracked, + isVisible, listInstructionSources, + listSkills, rel, ROOT, } from './ai-files.js'; +const README = path.join(AI_DIR, 'README.md'); +const CATALOG_START = ''; +const CATALOG_END = ''; + const header = (source) => `${GENERATED_MARKER} from ${source.rel}. Do not edit. Edit the source and run \`yarn ai:sync\`. -->`; @@ -108,15 +122,78 @@ function isOwned(file) { return readFileSync(file, 'utf8').includes(GENERATED_MARKER); } +const escapeCell = (value) => + String(value ?? '') + .replace(/\s+/g, ' ') + .replace(/\|/g, '\\|') + .trim(); + +function renderCatalog(sources, skills) { + const lines = [ + CATALOG_START, + '', + '_Generated by `yarn ai:sync` from frontmatter. Do not edit this block by hand._', + '', + '### Instructions', + '', + ...sources.map((s) => { + const file = s.rel.replace(/^\.ai\//, ''); + const scope = s.data.paths.map((p) => `\`${p}\``).join(', '); + const excluded = s.data.excludeAgent + ? `; not used by ${s.data.excludeAgent}` + : ''; + return `- **[\`${file}\`](./${file})** (${scope}${excluded}): ${escapeCell(s.data.description)}`; + }), + '', + '### Skills', + '', + ...skills.map( + (s) => + `- **[\`${s.data?.name ?? s.dir}\`](./skills/${s.dir}/SKILL.md)**: ${escapeCell(s.data?.description)}` + ), + '', + CATALOG_END, + ]; + return lines.join('\n'); +} + +const count = (text, needle) => text.split(needle).length - 1; + +/** + * Locate the generated catalog block in README text. Returns `{ start, end }`, where `end` + * is the index just past the end marker, or `{ error }` unless each marker appears exactly + * once with the start marker first. + */ +export function findCatalogBlock(text) { + const starts = count(text, CATALOG_START); + const ends = count(text, CATALOG_END); + if (starts !== 1 || ends !== 1) { + return { + error: `expected one ${CATALOG_START} and one ${CATALOG_END}, found ${starts} and ${ends}`, + }; + } + const start = text.indexOf(CATALOG_START); + const end = text.indexOf(CATALOG_END); + if (end < start) { + return { error: `${CATALOG_END} comes before ${CATALOG_START}` }; + } + return { start, end: end + CATALOG_END.length }; +} + /** * Compute every generated file and compare it with disk. * With `write: true`, update disk to match. Returns `{ errors, changes, fileCount }`. + * `readme` overrides the catalog file, for tests. */ -export async function syncAi({ write = false } = {}) { +export async function syncAi({ write = false, readme = README } = {}) { const errors = []; const changes = []; + // `--check` sees only tracked files, as CI does. Writing also covers untracked sources so + // a new rule generates before it's staged, but never gitignored ones, which the git hooks + // can't see. + const inScope = write ? isVisible : isTracked; const sources = listInstructionSources().filter( - (s) => s.data && !s.error && Array.isArray(s.data.paths) + (s) => inScope(s.file) && s.data && !s.error && Array.isArray(s.data.paths) ); const expected = new Map(); @@ -127,6 +204,27 @@ export async function syncAi({ write = false } = {}) { } } + if (!existsSync(readme)) { + errors.push(`${rel(readme)}: missing, so the catalog can't be checked`); + } else { + const current = readFileSync(readme, 'utf8'); + const block = findCatalogBlock(current); + if (block.error) { + errors.push( + `${rel(readme)}: ${block.error}. Restore the catalog markers, then run \`yarn ai:sync\`` + ); + } else { + const skills = listSkills().filter( + (s) => inScope(s.file) && s.data && !s.error + ); + const replaced = + current.slice(0, block.start) + + renderCatalog(sources, skills) + + current.slice(block.end); + expected.set(readme, await formatMarkdown(replaced, readme)); + } + } + for (const [filepath, content] of expected) { const exists = existsSync(filepath) || isDanglingLink(filepath); const isLink = exists && lstatSync(filepath).isSymbolicLink(); @@ -156,7 +254,7 @@ export async function syncAi({ write = false } = {}) { } for (const name of readdirSync(dir)) { const filepath = path.join(dir, name); - if (expected.has(filepath) || !isOwned(filepath)) { + if (expected.has(filepath) || !inScope(filepath) || !isOwned(filepath)) { continue; } changes.push(`${rel(filepath)}: orphaned (source removed)`); @@ -197,6 +295,10 @@ if (isMain) { console.warn(`✔ ${fileCount} generated file(s) up to date`); } else { changes.forEach((c) => console.warn(`updated ${c}`)); + if (errors.length) { + errors.forEach((error) => console.error(`✖ ${error}`)); + process.exit(1); + } console.warn( `✔ ${fileCount} generated file(s) in sync (${changes.length} change(s))` ); diff --git a/.ai/scripts/sync.test.js b/.ai/scripts/sync.test.js new file mode 100644 index 00000000000..ff7bdbaae04 --- /dev/null +++ b/.ai/scripts/sync.test.js @@ -0,0 +1,77 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/** + * Tests for the README catalog markers in sync.js. Runs as part of `yarn lint:ai`. + * + * Usage: + * node --test .ai/scripts/sync.test.js + */ + +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { after, describe, test } from 'node:test'; + +import { findCatalogBlock, syncAi } from './sync.js'; + +const START = ''; +const END = ''; + +const broken = { + 'start marker is missing': `# Title\n\n${END}\n`, + 'end marker is missing': `# Title\n\n${START}\n`, + 'both markers are missing': '# Title\n', + 'start marker is duplicated': `${START}\n${START}\n${END}\n`, + 'end marker is duplicated': `${START}\n${END}\n${END}\n`, + 'markers are out of order': `${END}\n${START}\n`, +}; + +describe('findCatalogBlock', () => { + test('finds one well-formed block', () => { + const text = `before\n${START}\nold\n${END}\nafter\n`; + const block = findCatalogBlock(text); + assert.equal(block.error, undefined); + assert.equal(text.slice(block.start, block.end), `${START}\nold\n${END}`); + }); + + for (const [name, text] of Object.entries(broken)) { + test(`reports an error when the ${name}`, () => { + assert.match(findCatalogBlock(text).error ?? '', /ai:catalog/); + }); + } +}); + +describe('syncAi catalog check', () => { + const dir = mkdtempSync(path.join(tmpdir(), 'ai-sync-test-')); + after(() => rmSync(dir, { recursive: true, force: true })); + + const catalogErrors = async (readme) => { + const { errors } = await syncAi({ write: false, readme }); + return errors.filter((error) => error.includes(path.basename(readme))); + }; + + for (const [name, text] of Object.entries(broken)) { + test(`fails instead of skipping the catalog when the ${name}`, async () => { + const readme = path.join(dir, `${name.replaceAll(' ', '-')}.md`); + writeFileSync(readme, text); + assert.equal((await catalogErrors(readme)).length, 1); + }); + } + + test('fails when the README is missing', async () => { + const errors = await catalogErrors(path.join(dir, 'missing.md')); + assert.equal(errors.length, 1); + assert.match(errors[0], /missing/); + }); +}); diff --git a/.ai/scripts/validate-frontmatter.js b/.ai/scripts/validate-frontmatter.js index cd53cedf8fb..dc2c4b0f9f2 100644 --- a/.ai/scripts/validate-frontmatter.js +++ b/.ai/scripts/validate-frontmatter.js @@ -29,10 +29,13 @@ * Also: unique names, no hand-authored files in generated folders, and a warning for * tool-specific wording (Claude, Cursor, .mdc) in tool-agnostic sources. * + * Only git-tracked files are checked (staged new files count), so untracked or ignored + * local files, such as tool caches and handoff notes, never fail the run. + * * Returns { errors, warnings, fileCount } for integration with validate.js. */ -import { existsSync, readdirSync, readFileSync } from 'fs'; +import { readFileSync } from 'fs'; import path from 'path'; import { parse as parseYaml } from 'yaml'; @@ -43,15 +46,17 @@ import { EXCLUDE_AGENT_VALUES, GENERATED_MARKER, INSTRUCTION_KEYS, + isTracked, listInstructionSources, listSkills, matchesGlob, readMarkdown, REJECTED_INSTRUCTION_KEYS, REJECTED_SKILL_KEYS, - ROOT, + rel, SKILL_KEYS, trackedFiles, + trackedFilesOnDisk, } from './ai-files.js'; /** @@ -264,14 +269,11 @@ function validateUniqueness(sources, skills, errors) { } } - // Generated folders hold only generated files. + // Generated folders hold only generated files. Untracked local files, such as tool + // caches, are skipped because they never reach the repository. for (const dir of ['.github/instructions', '.cursor/rules']) { - const full = path.join(ROOT, dir); - if (!existsSync(full)) { - continue; - } - for (const name of readdirSync(full)) { - const file = path.join(full, name); + const inDir = (f) => path.posix.dirname(f) === dir; + for (const file of trackedFilesOnDisk(inDir)) { let text = ''; try { text = readFileSync(file, 'utf8'); @@ -280,7 +282,7 @@ function validateUniqueness(sources, skills, errors) { } if (!text.includes(GENERATED_MARKER)) { errors.push( - `${dir}/${name}: hand-authored file in a generated folder; add it to .ai/ instead` + `${rel(file)}: hand-authored file in a generated folder; add it to .ai/ instead` ); } } @@ -315,8 +317,8 @@ export function validateFrontmatter() { const errors = [...assertGlobSemantics()]; const warnings = []; - const sources = listInstructionSources(); - const skills = listSkills(); + const sources = listInstructionSources().filter((s) => isTracked(s.file)); + const skills = listSkills().filter((s) => isTracked(s.file)); sources.forEach((s) => validateInstruction(s, errors, warnings)); skills.forEach((s) => validateSkill(s, errors, warnings)); @@ -326,46 +328,24 @@ export function validateFrontmatter() { errors ); - const agents = []; - const walkAgents = (dir) => { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - if ( - ['node_modules', '.git', 'dist', 'storybook-static'].includes( - entry.name - ) - ) { - continue; - } - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - walkAgents(full); - } else if (entry.name === 'AGENTS.md') { - agents.push(readMarkdown(full)); - } - } - }; - walkAgents(ROOT); + const agents = trackedFilesOnDisk( + (f) => path.posix.basename(f) === 'AGENTS.md' + ).map(readMarkdown); + + const skillDirs = skills.map((skill) => `${rel(path.dirname(skill.file))}/`); + const skillSupport = trackedFilesOnDisk( + (f) => + f.endsWith('.md') && + path.posix.basename(f) !== 'SKILL.md' && + skillDirs.some((dir) => f.startsWith(dir)) + ).map(readMarkdown); - const skillSupport = skills.flatMap((skill) => { - const dir = path.dirname(skill.file); - return readdirSync(dir, { recursive: true }) - .filter( - (f) => - String(f).endsWith('.md') && path.basename(String(f)) !== 'SKILL.md' - ) - .map((f) => readMarkdown(path.join(dir, String(f)))); - }); + const aiDocs = trackedFilesOnDisk( + (f) => path.posix.dirname(f) === rel(AI_DIR) && f.endsWith('.md') + ).map(readMarkdown); validateToolWording( - [ - ...sources, - ...skills, - ...skillSupport, - ...agents, - ...readdirSync(AI_DIR) - .filter((f) => f.endsWith('.md')) - .map((f) => readMarkdown(path.join(AI_DIR, f))), - ], + [...sources, ...skills, ...skillSupport, ...agents, ...aiDocs], warnings ); diff --git a/.ai/scripts/validate-links.js b/.ai/scripts/validate-links.js index 7083787b404..9c8dce29272 100644 --- a/.ai/scripts/validate-links.js +++ b/.ai/scripts/validate-links.js @@ -24,27 +24,17 @@ * code, which hold examples for other documents (error) * - Every backticked `.ai/...` path in `.ai/**` Markdown resolves (warning) * + * Only git-tracked files are scanned (staged new files count), so ignored local notes such + * as `.ai/handoffs/` never produce errors or warnings. + * * Usage: * node .ai/scripts/validate-links.js */ import fs from 'fs'; import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const repoRoot = path.resolve(__dirname, '../..'); -const aiDir = path.join(repoRoot, '.ai'); - -// Directories to skip when searching for AGENTS.md files -const SKIP_DIRS = new Set([ - 'node_modules', - '.git', - 'dist', - '.wireit', - 'storybook-static', - 'coverage', -]); + +import { ROOT as repoRoot, trackedFilesOnDisk } from './ai-files.js'; // Runtime folders that agents create on demand and git ignores. const RUNTIME_PATHS = ['.ai/handoffs']; @@ -55,50 +45,23 @@ const SAMPLE_LINKS = new Set([ ]); /** - * Recursively find all AGENTS.md files under a directory. + * Every tracked AGENTS.md file. */ -function findAgentsFiles(dir) { - const results = []; - - let entries; - try { - entries = fs.readdirSync(dir, { withFileTypes: true }); - } catch { - return results; - } - - for (const entry of entries) { - if (entry.isDirectory()) { - if (!SKIP_DIRS.has(entry.name)) { - results.push(...findAgentsFiles(path.join(dir, entry.name))); - } - } else if (entry.isFile() && entry.name === 'AGENTS.md') { - results.push(path.join(dir, entry.name)); - } - } - - return results; +function findAgentsFiles() { + return trackedFilesOnDisk((f) => path.posix.basename(f) === 'AGENTS.md'); } /** - * Recursively find all Markdown files under `.ai/`. + * Every tracked Markdown file under `.ai/`. */ -function findAiMarkdown(dir) { - const results = []; - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...findAiMarkdown(full)); - } else if ( - entry.isFile() && - entry.name.endsWith('.md') && +function findAiMarkdown() { + return trackedFilesOnDisk( + (f) => + f.startsWith('.ai/') && + f.endsWith('.md') && // Skill templates hold links that resolve from wherever the template is copied. - !full.split(path.sep).includes('assets') - ) { - results.push(full); - } - } - return results; + !f.split('/').includes('assets') + ); } /** @@ -218,8 +181,8 @@ function validateAiFile(filePath) { * Returns { errors, warnings, fileCount }. */ export function validateLinks() { - const agentsFiles = findAgentsFiles(repoRoot); - const aiFiles = findAiMarkdown(aiDir); + const agentsFiles = findAgentsFiles(); + const aiFiles = findAiMarkdown(); const errors = agentsFiles.flatMap(validateFile); const warnings = []; for (const file of aiFiles) { diff --git a/.ai/scripts/validate-story-tags.js b/.ai/scripts/validate-story-tags.js index 5683ea88b4e..ef4355de9b8 100644 --- a/.ai/scripts/validate-story-tags.js +++ b/.ai/scripts/validate-story-tags.js @@ -19,16 +19,16 @@ * - Every tag value is in the known allowed set * - Every stories file has at least one `tags` declaration containing 'migrated' * + * Only git-tracked stories files are checked (staged new files count). + * * Usage: * node .ai/scripts/validate-story-tags.js */ import fs from 'fs'; import path from 'path'; -import { fileURLToPath } from 'url'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const repoRoot = path.resolve(__dirname, '../..'); +import { ROOT as repoRoot, trackedFilesOnDisk } from './ai-files.js'; // Tags defined in .ai/skills/stories-format/SKILL.md (Tags section) const ALLOWED_TAGS = new Set([ @@ -49,19 +49,12 @@ const ALLOWED_TAGS = new Set([ ]); /** - * Recursively find all *.stories.ts files under a directory. + * Every tracked *.stories.ts file under a repository-relative directory. */ function findStoriesFiles(dir) { - const results = []; - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...findStoriesFiles(full)); - } else if (entry.isFile() && entry.name.endsWith('.stories.ts')) { - results.push(full); - } - } - return results; + return trackedFilesOnDisk( + (f) => f.startsWith(`${dir}/`) && f.endsWith('.stories.ts') + ); } /** @@ -139,13 +132,7 @@ function validateFile(filePath) { * Run validation across all stories files. Returns { errors, fileCount }. */ export function validateStoryTags() { - const storiesRoot = path.join(repoRoot, 'gen2/packages/swc/components'); - - if (!fs.existsSync(storiesRoot)) { - return { errors: [], fileCount: 0 }; - } - - const files = findStoriesFiles(storiesRoot); + const files = findStoriesFiles('gen2/packages/swc/components'); const errors = files.flatMap(validateFile); return { errors, fileCount: files.length }; diff --git a/.ai/scripts/validate-symlinks.js b/.ai/scripts/validate-symlinks.js index 171f2bf267d..9cbb8d0dc1d 100644 --- a/.ai/scripts/validate-symlinks.js +++ b/.ai/scripts/validate-symlinks.js @@ -17,13 +17,16 @@ * Cursor skills: directory symlink (.cursor/skills → ../.ai/skills) * * Cursor rules (.cursor/rules/*.mdc) are generated files, not symlinks: `sync.js` writes - * them and `sync.js --check` verifies them. A leftover per-file symlink is an error. + * them and `sync.js --check` verifies them. A leftover per-file symlink tracked by git is + * an error. * * Returns { errors, fileCount } for integration with validate.js. */ -import { existsSync, lstatSync, readdirSync, readlinkSync } from 'fs'; -import { join } from 'path'; +import { existsSync, lstatSync, readlinkSync } from 'fs'; +import { join, posix } from 'path'; + +import { trackedFiles } from './ai-files.js'; const ROOT = new URL('../../', import.meta.url).pathname.replace(/\/$/, ''); @@ -47,15 +50,20 @@ function checkDirectorySymlink(linkPath, expectedTarget, errors) { } function checkNoCursorRuleSymlinks(errors) { - const dir = join(ROOT, '.cursor/rules'); - if (!existsSync(dir)) { - return 0; - } - const files = readdirSync(dir); + // Only tracked entries: an untracked local file never reaches the repository. + const files = trackedFiles().filter( + (f) => posix.dirname(f) === '.cursor/rules' + ); for (const file of files) { - if (lstatSync(join(dir, file)).isSymbolicLink()) { + let stat; + try { + stat = lstatSync(join(ROOT, file)); + } catch { + continue; + } + if (stat.isSymbolicLink()) { errors.push( - `.cursor/rules/${file} is a symlink; Cursor rules are generated now, so run \`yarn ai:sync\`` + `${file} is a symlink; Cursor rules are generated now, so run \`yarn ai:sync\`` ); } } diff --git a/.ai/scripts/validate.js b/.ai/scripts/validate.js index 1eb78736540..d3bc4511ca2 100644 --- a/.ai/scripts/validate.js +++ b/.ai/scripts/validate.js @@ -29,6 +29,9 @@ * components, patterns, and controllers conform to the per-unit MDX * authoring standards in `.ai/rules/stories-documentation.md` * + * Checks 1 through 6 read only git-tracked files (staged new files count), so untracked or + * ignored local files, such as tool caches and `.ai/handoffs/` notes, never fail the run. + * * Exits with code 1 if any check has errors; warnings are printed but do not fail. * * Usage: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 278cf4f8718..8cb40da1b17 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ diff --git a/.husky/pre-commit b/.husky/pre-commit index 8258a0cc683..5b1ee4945da 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,14 +3,16 @@ yarn lint-staged --allow-empty # If any .ai/ sources or AGENTS.md files are staged, regenerate the Copilot and Cursor # instruction files from them and stage the results (yarn ai:sync). if git diff --cached --name-only | grep -qE "^\.ai/|(^|/)AGENTS\.md$"; then - # sync.js reads the working tree, so unstaged or untracked rule edits would leak into + # sync.js reads the working tree, so unstaged or untracked source edits would leak into # the staged output. Skip instead; pre-push and CI (`lint:ai`) catch any drift. - if ! git diff --quiet -- .ai/rules .ai/memory || - [ -n "$(git ls-files --others --exclude-standard -- .ai/rules .ai/memory)" ]; then - echo "⚠ Skipped yarn ai:sync: .ai/rules or .ai/memory has unstaged changes. Run it before you push." >&2 + AI_SOURCES=".ai/rules .ai/memory .ai/skills .ai/README.md" + if ! git diff --quiet -- $AI_SOURCES || + [ -n "$(git ls-files --others --exclude-standard -- $AI_SOURCES)" ]; then + echo "⚠ Skipped yarn ai:sync: .ai/ sources have unstaged changes. Run it before you push." >&2 else node .ai/scripts/sync.js || exit 1 - git add -A -- .github/instructions .cursor/rules + # The README catalog block is generated too. + git add -A -- .github/instructions .cursor/rules .ai/README.md fi fi diff --git a/.husky/pre-push b/.husky/pre-push index e4978d0c95d..21690713d68 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,9 +1,10 @@ -# Block the push if the generated Copilot and Cursor instructions are out of sync with -# .ai/ sources. sync.js reads the working tree, so run it only when the sources and -# generated folders match HEAD; otherwise warn and leave it to CI (`lint:ai`). When it -# updates files, they aren't in this push, so stop and let the author commit them. -AI_PATHS=".ai/rules .ai/memory .github/instructions .cursor/rules" -GENERATED_PATHS=".github/instructions .cursor/rules" +# Block the push if the generated Copilot and Cursor instructions or the README catalog +# are out of sync with .ai/ sources. sync.js reads the working tree, so run it only when +# the sources and generated files match HEAD; otherwise warn and leave it to CI +# (`lint:ai`). When it updates files, they aren't in this push, so stop and let the +# author commit them. +AI_PATHS=".ai/rules .ai/memory .ai/skills .ai/README.md .github/instructions .cursor/rules" +GENERATED_PATHS=".github/instructions .cursor/rules .ai/README.md" if git diff HEAD --quiet -- $AI_PATHS && [ -z "$(git ls-files --others --exclude-standard -- $AI_PATHS)" ]; then sync_output=$(node .ai/scripts/sync.js 2>&1) || { diff --git a/CONTRIBUTOR-DOCS/01_contributor-guides/04_making-a-pull-request.md b/CONTRIBUTOR-DOCS/01_contributor-guides/04_making-a-pull-request.md index be67fdc1d5f..1aae2cdcf01 100644 --- a/CONTRIBUTOR-DOCS/01_contributor-guides/04_making-a-pull-request.md +++ b/CONTRIBUTOR-DOCS/01_contributor-guides/04_making-a-pull-request.md @@ -79,10 +79,12 @@ If you're unsure about an accessibility detail, the [Web Accessibility Initiativ ### Branch naming -We use a straightforward branch naming convention: +Name branches `/-[-swc-]`, where `` is a conventional commit type: -- `[username]/[short-description]` (e.g., `alex/fix-dropdown-bug`) -- If referencing a known issue, incorporate the issue number (e.g., `alex/123-fix-dropdown-bug`) +- `alex/fix-dropdown-bug` +- `alex/fix-dropdown-bug-swc-123` when the work references a Jira issue + +Use lowercase letters, numbers, and dashes. This is a recommendation, not an enforced rule. Coding agents follow the same guidance from the [`branch-naming` skill](https://github.com/adobe/spectrum-web-components/blob/main/.ai/skills/branch-naming/SKILL.md). Branches that the GitHub Copilot app creates for its own sessions are exempt. ### Changeset requirements @@ -130,15 +132,18 @@ Format: `type(component?): subject` The component is optional but should reference the package you are updating. -Types include: +The allowed types are the ones [`@commitlint/config-conventional`](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) defines, and the `commit-msg` hook rejects anything else. The subject must start with a lowercase letter. The most common types are: - `feat`: New features or enhancements - `fix`: Bug fixes - `docs`: Documentation changes - `style`: Formatting, linting (not CSS changes) -- `chore`: Build tooling, repo management, dependency updates +- `refactor`: Code changes that neither fix a bug nor add a feature - `perf`: Performance improvements - `test`: Adding or updating tests +- `chore`: Build tooling, repo management, dependency updates + +`build`, `ci`, and `revert` are also allowed. The [`conventional-commits` skill](https://github.com/adobe/spectrum-web-components/blob/main/.ai/skills/conventional-commits/SKILL.md) has the full list, which `yarn lint:ai` keeps in sync with commitlint. Examples: diff --git a/package.json b/package.json index c2dbdafa894..ca85de94825 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "generate:workflow-icons": "yarn workspace @adobe/spectrum-wc-icons generate:workflow-icons", "lint": "run-s --continue-on-error lint:eslint lint:styles lint:prettier lint:ai", "lint:1st-gen": "LINT_PATH=1st-gen yarn lint", - "lint:ai": "node .ai/scripts/validate.js", + "lint:ai": "node .ai/scripts/validate.js && node --test .ai/scripts/sync.test.js", "lint:docs-pages": "node scripts/validate-docs-pages.js", "lint:eslint": "echo 'Linting with ESLint...' && eslint ${LINT_PATH:-.} --cache", "lint:gen2": "LINT_PATH=gen2 yarn lint",