From 0cc6890ff1a121ac625b344d5c96c932d5eecb1d Mon Sep 17 00:00:00 2001 From: mhenrixon Date: Sat, 19 Sep 2026 18:08:32 +0200 Subject: [PATCH] chore(rtk): AGENTS.md as the shared instruction file --- .claude/README.md | 4 +- .claude/SKILL_TEMPLATE.md | 2 +- .claude/commands/github-review-comments.md | 6 +- .claude/commands/plan.md | 4 +- AGENTS.md | 214 ++++++++++++++++----- CLAUDE.md | 126 +----------- 6 files changed, 174 insertions(+), 182 deletions(-) diff --git a/.claude/README.md b/.claude/README.md index d0fd781..0cd7757 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -63,7 +63,7 @@ Rules of thumb: scans — pass a cheaper `model:` explicitly. Left unset, a subagent inherits the session model, so the most mechanical work runs at the highest price. -The convention is also recorded in the repo `CLAUDE.md` ("Slash Commands") so it +The convention is also recorded in the repo `AGENTS.md` ("Slash Commands") so it survives across sessions. ## Authoring a new command @@ -73,4 +73,4 @@ survives across sessions. 3. Write a `description` that leads with what it does and when to use it. 4. Scope `allowed-tools` if the command should be constrained (review/CI commands usually are; open implementation commands usually are not). -5. Add a row to the "Slash Commands" table in `CLAUDE.md`. +5. Add a row to the "Slash Commands" table in `AGENTS.md`. diff --git a/.claude/SKILL_TEMPLATE.md b/.claude/SKILL_TEMPLATE.md index a5ae1c3..1223094 100644 --- a/.claude/SKILL_TEMPLATE.md +++ b/.claude/SKILL_TEMPLATE.md @@ -56,4 +56,4 @@ bundle exec rubocop lib spec ``` After creating the file, add a row to the "Slash Commands" table in the repo -`CLAUDE.md` so the command is discoverable and its tier is recorded. +`AGENTS.md` so the command is discoverable and its tier is recorded. diff --git a/.claude/commands/github-review-comments.md b/.claude/commands/github-review-comments.md index 090b81f..9c94e1c 100644 --- a/.claude/commands/github-review-comments.md +++ b/.claude/commands/github-review-comments.md @@ -101,7 +101,7 @@ For each unresolved comment, read the full body and categorise it: 2. Check if the suggestion is technically correct for THIS codebase 3. Check if it would break existing functionality (a consuming docs site) 4. Check if existing patterns/conventions contradict the suggestion -5. Check CLAUDE.md rules -- project conventions override reviewer preferences +5. Check AGENTS.md rules -- project conventions override reviewer preferences --- @@ -220,7 +220,7 @@ When pushing back: - Use technical reasoning grounded in the actual codebase - Reference existing patterns if the suggestion contradicts them -- Reference CLAUDE.md rules when applicable +- Reference AGENTS.md rules when applicable - Explain what would break or what edge case the reviewer missed - If the suggestion is valid in principle but wrong for this context, say so @@ -231,7 +231,7 @@ When pushing back: - Always read the actual code before evaluating a comment -- reviewers sometimes misread diffs - If a comment reveals a genuine bug you missed, fix it without defensiveness - If multiple comments suggest the same change, implement it once and reference the fix in all replies -- Bot reviewers (CodeRabbit, etc.) sometimes suggest changes that conflict with project conventions -- verify against CLAUDE.md +- Bot reviewers (CodeRabbit, etc.) sometimes suggest changes that conflict with project conventions -- verify against AGENTS.md - If a new round of review comments appears after your push (from re-review), report that to the user rather than entering an infinite loop Now begin by determining the PR number from `$ARGUMENTS` or the current branch. diff --git a/.claude/commands/plan.md b/.claude/commands/plan.md index 62fd9f3..eed2c3a 100644 --- a/.claude/commands/plan.md +++ b/.claude/commands/plan.md @@ -29,7 +29,7 @@ Protect this session's context: delegate mechanical exploration to cheaper subag 1. Fan out Explore agents (`model: haiku`) for file discovery and naming-convention sweeps; use `model: sonnet` agents when a subsystem needs to be read and summarized. Launch independent explorations in parallel. 2. Read the load-bearing files yourself — the ones the design decision actually hinges on. Don't design from subagent summaries alone. -3. Check the architecture layers in `CLAUDE.md` and read the matching source files — past decisions and gotchas live there. +3. Check the architecture layers in `AGENTS.md` and read the matching source files — past decisions and gotchas live there. 4. Check `git log` for recent related work; the design should extend it, not fight it. ## Phase 2 — Surface the unknowns (blindspot pass + interview) @@ -41,7 +41,7 @@ Investigation tells you what the codebase says; this phase finds what the REQUES - edge cases the codebase makes possible that the request never mentions - anything with no precedent in this repo — flag it explicitly as unknown-unknown territory 2. **Interview the user** with AskUserQuestion, one question at a time, prioritized by blast radius: architecture-changing answers first, then public API / config surface, then UX. Rules: - - Skip anything the codebase, CLAUDE.md, or an existing issue already answers. + - Skip anything the codebase, AGENTS.md, or an existing issue already answers. - 2–5 questions is the sweet spot; zero is fine when the request is genuinely unambiguous — say so rather than inventing questions. - Every question offers concrete options with a recommended default, never an open-ended essay prompt. 3. **Record the answers** in the plan's Decision section as `Settled in interview:` bullets — constraints the executor must not re-litigate. diff --git a/AGENTS.md b/AGENTS.md index 8ea8159..ede0431 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,48 +1,128 @@ -# AGENTS.md - -Guidance for AI coding agents working in the **phlex-forms** repository. This is -the cross-tool convention file (Claude Code, Cursor, Copilot, Aider, …). Claude -Code users also have `.claude/commands/` and `.claude/rules/`; see `CLAUDE.md` -for the full project brief. This file is the fast orientation. - -phlex-forms is a model-bound form builder for [Phlex](https://www.phlex.fun): -`field :email` renders label + input + error/hint in one call, with the input -type and `required` flag inferred from the model. It's DaisyUI-styled by default -with a Plain (unstyled) theme fallback, and offers optional server-truth live -validation over phlex-reactive. The gem also **dogfoods a docs site** under -`docs/` (built on docs-kit). - -## The two things you'll be asked to do - -### A. Change the gem (a component, inference rule, theme, config, the live layer) - -Read `CLAUDE.md` first — it has the layer map and the critical rules. The -non-negotiables: - -- **Resolve components through the theme** — `PhlexForms::Theme` maps a role - (`:input`, `:select`, `:control`, …) to a component class, so the same form - renders daisy or Plain. A new leaf needs BOTH a daisy component and a - `Forms::Plain::*` one (accepts-and-ignores variants, `aria-invalid` / - `data-field-error` hooks, zero styling classes). -- **Guard every model touch** — inference (`PhlexForms::Inference`) and field - metadata (`Forms::Field`) sit behind `respond_to?` + `rescue StandardError`, - so POROs / Structs / untyped ActiveModel degrade to the attribute-name map. - Never require ActiveRecord. -- **daisyui and phlex-reactive are SOFT dependencies** — `require`-rescue- - `LoadError` + Zeitwerk `ignore`. The gem must boot and render (Plain theme) - without either; the `live` macro raises a clear `FeatureUnavailable` when - phlex-reactive is absent. -- **Literal class strings only** — `"input input-primary"`, never - `"input-#{x}"`; interpolated names get tree-shaken by the host's Tailwind scan. -- **Never `raw`/`html_safe` user or model data** — field names, choices, and - values are user-influenced; let Phlex escape. -- **TDD**: write the failing spec first — `spec/phlex_forms/` for pure logic - (inference precedence, config, theme), `spec/forms/` for rendered output via - the `render_form` kit helper. Assert on semantics (`name=`, selected option, - error text), not HTML snapshots. Model doubles come from `build_model` - (ActiveModel, `spec/support/model_helpers.rb`) — no database. - -### B. Write a docs page for phlex-forms' own docs site (under `docs/`) +# phlex-forms + +Project instructions for every agent: Claude Code (`CLAUDE.md` imports this file), +Cursor, Copilot, Codex, and any other AI tool working in this repo. Claude Code +users also have `.claude/commands/` and `.claude/rules/` for slash commands and +auto-loaded rules — this file is still the full project brief. + +A model-bound form builder for [Phlex](https://www.phlex.fun): `field :email` +renders a label, an input, and an error/hint in one call, inferring the input +type and the `required` flag from the model. DaisyUI-styled by default (via the +[`daisyui`](https://github.com/zoolutions/daisyui) gem) with a Plain (unstyled) +theme fallback, plus optional server-truth live validation over +[phlex-reactive](https://github.com/zoolutions/phlex-reactive). The gem also +**dogfoods a docs site** under `docs/` (built on docs-kit). + +## Tech Stack + +- **Ruby**: >= 3.4 (aligns with the optional phlex-reactive live integration) +- **Rendering**: Phlex 2 — components live under `Forms::`, internals under `PhlexForms::` +- **Styling**: daisyUI (soft dependency) — leaf components delegate markup + variants to the `daisyui` gem +- **Type inference**: `PhlexForms::Inference` reads columns / enums / associations / validators +- **Live validation**: `Forms::Live` over phlex-reactive (soft dependency) +- **Autoloading**: zeitwerk (two roots: `lib/forms` → `Forms::`, `lib/phlex_forms` → `PhlexForms::`) +- **Testing**: RSpec (unit + integration render specs) +- **Linting**: RuboCop +- **Docs site**: a nested docs-kit app under `docs/` (its own Ruby 4.0.5, its own bundle) + +## Critical Rules + +### Never Do +1. **NO hardcoded component classes** — leaf components resolve through `PhlexForms::Theme` (role → class), so the same form renders daisy or Plain. Don't reference `Forms::Input` directly where a theme role belongs. +2. **NO interpolated Tailwind/daisy class strings** — write literal class strings (`"input input-primary"`, never `"input-#{x}"`); the host's Tailwind scanner can't see a built name, so the style never ships. +3. **NO unguarded model introspection** — every model touch sits behind `respond_to?` guards + a `StandardError` rescue (see `Forms::Field#required?`, `PhlexForms::Inference`), so plain objects / Structs / untyped ActiveModel degrade to the name map. Inference must never require ActiveRecord. +4. **NO hard dependency on daisyui or phlex-reactive** — both are soft: `require`-rescue-`LoadError` + Zeitwerk `ignore` of the files that reference them. The gem must boot and render (Plain theme) without either. +5. **NO `raw`/`html_safe` on user/model data** — let Phlex escape; only gem-authored trusted markup may bypass it. Field names, choices, values are user-influenced. +6. **NO caller options silently lost** — in `field`, explicit `as:`/`choices:`/caller kwargs always win over inferred attributes. +7. **NO manual `gem push`** — release via `bin/release` (patch/minor/major/explicit; wraps `rake release[X.Y.Z]`, which stages ONLY the version file + the docs lockfile pin; the gem root `Gemfile.lock` is gitignored, correct for a library gem). + +### Always Do +1. **TDD**: write tests BEFORE implementation (RED → GREEN → REFACTOR). +2. **Preserve graceful degradation** — a change to inference/theming must keep POROs and non-daisy hosts working; the existing specs are the regression suite. +3. **Honor both themes** — a new leaf component needs a daisy form AND a `Forms::Plain::*` form that accepts-and-ignores variants, wires `aria-invalid`/`data-field-error`, and ships zero styling classes. +4. **Wire inference, don't special-case** — a new type mapping goes into `PhlexForms::Inference`'s precedence chain, behind guards, with a unit spec in the precedence table. +5. **Config gets a default** — a new `PhlexForms::Configuration` knob has a sensible default so existing apps keep working. +6. **Assert on semantics** — a spec checks `name="user[email]"`, the selected option, the error message — not a brittle full-HTML snapshot. + +## Commands + +```bash +bundle exec rspec # Full suite (unit + integration render specs) +bundle exec rubocop lib spec # Lint (rubocop -A lib spec to autocorrect) +bundle exec rake # spec + rubocop (the default task) +``` + +The docs site under `docs/` has its own bundle (Ruby 4.0.5): `cd docs && bin/dev`. +Ruby floor is **3.4** (the CI matrix is 3.4 + 4.0). + +Command output is condensed by rtk (PreToolUse hook). Write commands in +hook-rewritable shapes: no `for`/subshell wrappers, no `| head` on rtk-handled +commands, `bundle exec rubocop` not `bin/rubocop`. + +## Slash Commands + +| Command | Purpose | +|---------|---------| +| `/plan` | Fable-powered planning → GitHub issue or `docs/plans/` markdown (read-only; execute with `/lfg`) | +| `/lfg` | Full autonomous workflow: branch → understand → explore → plan → TDD → verify → PR | +| `/tdd` | Enforce RED → GREEN → REFACTOR | +| `/architect` | Coordinate a change across the builder → components → inference → theme → live layers | +| `/security` | Security audit (HTML escaping, model-bound params, the live action whitelist, CSRF) | +| `/review-pr` | Review a PR for pattern compliance | +| `/github-review-pr` | Full PR pass: fix CI failures, then resolve review comments (in that order) | +| `/github-review-failures` | Fix failing CI checks until green | +| `/github-review-comments` | Process unresolved PR review comments | +| `/finish-prs` | Drive a stack of open PRs to merge-ready one at a time | + +## Architecture + +``` +Layer 5: Live validation lib/forms/live.rb, lib/forms/live/field.rb (phlex-reactive; the :validate action, signed identity, touched tracking) — SOFT dep +Layer 4: Theming lib/phlex_forms/theme.rb (role → component map), lib/forms/plain/*.rb (bare semantic HTML), daisy leaves are the default +Layer 3: Type inference lib/phlex_forms/inference.rb (columns/enums/associations/validators → control + attrs, all behind respond_to? guards) +Layer 2: Field components lib/forms/*.rb (Input, Select, Textarea, Checkbox, Toggle, Radio, FileInput, ...) delegating markup to daisyui via lib/phlex_forms/delegated_field.rb +Layer 1: The builder API lib/phlex_forms/builder.rb (the `field` verb + PascalCase escape hatches, row/group), lib/forms/form.rb (inline), lib/forms/base.rb (declarative classes), lib/forms/field.rb (per-field context) +Layer 0: Config + engine lib/phlex_forms/configuration.rb (theme/infer_from_model/field_variants/icon_renderer), lib/phlex_forms.rb (soft-require wiring), lib/phlex_forms/engine.rb (Rails: Stimulus controllers, locales, the live param type) + Docs site docs/ (a docs-kit Rails app), deployed via .github/workflows/deploy-docs.yml → docs-kit's reusable workflow +``` + +## The mental model + +> The model already knows. `field :notify` renders a toggle for a boolean +> column, an enum becomes a humanized select, a `belongs_to` a collection +> select — `as:`/`choices:` are overrides, not requirements. The same form +> class renders daisy or Plain by swapping a theme; live validation runs the +> real ActiveModel validators server-side. + +Everything is additive and degrades: no ActiveRecord? name-map inference. No +daisyui? Plain theme. No phlex-reactive? the `live` macro raises a clear +`FeatureUnavailable` and the Stimulus `validate: true` fallback still works. + +## Model tiers (for Claude Code commands & agents) + +Commands and agents pin a model **tier** via frontmatter aliases, not a full +model ID — aliases track the latest model in each tier, so pins never go stale: + +- `haiku` — mechanical/config work, diff pattern-scans +- `sonnet` — layer specialists / pattern-following implementation (the default for `/tdd`, the review-comment/failure runbooks) +- `opus` — orchestration, security, production/PR review (`/lfg`, `/architect`, `/security`, `/review-pr`, `/github-review-pr`) +- `fable` — pinned only on `/plan` (read-only planning that hands execution to cheaper models); otherwise choose it per-session with `/model` for architecture and the hardest debugging + +When spawning subagents for mechanical work (file finding, pattern scans), pass a +cheaper model explicitly (`model: haiku`) rather than letting them inherit the +session model. See `.claude/rules/agents.md`. + +## Testing + +- Unit specs (`spec/phlex_forms/`) cover pure logic with no rendering — `PhlexForms::Inference` (the full precedence table), `PhlexForms::Configuration`, `PhlexForms::Theme`. +- Integration specs (`spec/forms/`) render a real form through a kit-context helper (`render_form(model) { |f| ... }`, see `spec/support/phlex_helpers.rb`) and assert on the produced markup's semantics (`name=`, selected option, error message, the error variant class). +- Model doubles use `build_model` (an anonymous ActiveModel class, `spec/support/model_helpers.rb`) — no database. +- `Forms::Live` specs are guarded by `if defined?(Phlex::Reactive)` and stub the reply (the endpoint isn't booted); a class-level assertion (`skip_verify_authorized?(:validate)`) guards the one behavior specs can't drive. +- Aspire to 100% for `PhlexForms::Inference` / `Configuration` / `Theme` — the public API sites depend on. +- CI: `.github/workflows/main.yml` runs `bundle exec rspec` on Ruby 3.4 + 4.0 for every push to `main` and every PR; lint on 4.0. +- See `.claude/rules/testing.md`. + +## Writing a docs page for phlex-forms' own docs site (under `docs/`) The docs site is a docs-kit site. Its registry is `docs/app/models/doc.rb`; its pages are `docs/app/views/docs/pages/`. To document a gem feature: @@ -66,14 +146,44 @@ example of the whole contract is any existing page under `docs/app/views/docs/pages/` (e.g. `field_api.rb`, `inference.rb`). There is a `write-docs-page` skill under `docs/.claude/skills/` for this exact task. -## Verify before you finish (every change) +**3. Verify**: `cd docs && bundle exec rspec && bundle exec rubocop` (`bun run +build:css` if you added classes the CSS scans). + +## Screenshots on PRs and issues (always) + +A rendered form or docs page change ships with before/after pictures **on the +PR**, attached from the terminal. Never a local path, a base64 blob, or +"screenshot available on request". Applies to: a `Forms::`/`PhlexForms::` +component's rendered markup (daisy AND Plain theme when both changed), and any +`docs/` page. + +`gh` ≥ 2.99 uploads images and videos itself: ```bash -bundle exec rspec # the suite (unit + integration render specs) -bundle exec rubocop lib spec # lint — no offenses (rubocop -A lib spec to fix) -bundle exec rake # both, together +gh pr create --attach './after.png#Sidebar collapsed on mobile' --title … --body … # picture in hand already +gh pr comment --attach './after.png#Sidebar collapsed on mobile' --body 'Before/after for the error state.' +gh pr comment --attach ./before.png --attach ./after.png # repeat the flag, up to 50 files +gh issue comment --attach ./repro.mp4 # video renders as a player ``` -Ruby floor is **3.4** (the CI matrix is 3.4 + 4.0). The `docs/` app has its own -bundle (Ruby 4.0.5). Never `gem push` by hand — release via `rake release[X.Y.Z]` -(it stages only the version file; `Gemfile.lock` is gitignored, correct for a gem). +- Quote the whole argument: the alt text has spaces and bare `<`/`>` would redirect. `#` + sets the alt text; without it the filename is used. A body that already + references the file (`![alt](./after.png)`) gets that reference rewritten to the uploaded + asset, so images can sit inline; unreferenced attachments are appended at the end. +- `create`, `edit` and `comment` all take `--attach` (all three landed in gh 2.99). Attach at create time when + the picture already exists; comment when it comes later, as it does after a verification run. +- Capture with `agent-browser screenshot ` against `docs/` (`cd docs && bin/dev`) or a spec-rendered + page. Save under the scratchpad, never in the repo. +- No `--attach` flag means an old `gh`: `brew upgrade gh`. + +## Release & docs deploy + +- `bin/release [patch|minor|major|X.Y.Z]` (`list` / `--dry-run` / `--force`) computes the next version, shows the commits since the last tag, confirms, then runs `rake release[X.Y.Z]`, which bumps the version, verifies `gem build --strict`, pushes, and creates the GitHub release; CI (`release.yml`) tests, builds, signs (Sigstore), and publishes to RubyGems via trusted publishing. +- The docs site deploys on release via `.github/workflows/deploy-docs.yml`, which calls docs-kit's reusable dash + GHCR workflow. `image`/`service` are `zoolutions/phlex-forms`. + +## More Documentation + +- `.claude/commands/` — slash command definitions +- `.claude/rules/` — coding style, git workflow, testing, agents +- `README.md` — the full field API / inference / theming / live-validation guide +- `docs/` — the published documentation site (docs-kit) diff --git a/CLAUDE.md b/CLAUDE.md index d3824e9..7813b76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,124 +1,6 @@ -# phlex-forms +# phlex-forms — Claude Code entry point -A model-bound form builder for [Phlex](https://www.phlex.fun): `field :email` -renders a label, an input, and an error/hint in one call, inferring the input -type and the `required` flag from the model. DaisyUI-styled by default (via the -[`daisyui`](https://github.com/zoolutions/daisyui) gem) with a Plain (unstyled) -theme fallback, plus optional server-truth live validation over -[phlex-reactive](https://github.com/zoolutions/phlex-reactive). +@AGENTS.md -## Tech Stack - -- **Ruby**: >= 3.4 (aligns with the optional phlex-reactive live integration) -- **Rendering**: Phlex 2 — components live under `Forms::`, internals under `PhlexForms::` -- **Styling**: daisyUI (soft dependency) — leaf components delegate markup + variants to the `daisyui` gem -- **Type inference**: `PhlexForms::Inference` reads columns / enums / associations / validators -- **Live validation**: `Forms::Live` over phlex-reactive (soft dependency) -- **Autoloading**: zeitwerk (two roots: `lib/forms` → `Forms::`, `lib/phlex_forms` → `PhlexForms::`) -- **Testing**: RSpec (unit + integration render specs) -- **Linting**: RuboCop -- **Docs site**: a nested docs-kit app under `docs/` (its own Ruby 4.0.5, its own bundle) - -## Critical Rules - -### Never Do -1. **NO hardcoded component classes** — leaf components resolve through `PhlexForms::Theme` (role → class), so the same form renders daisy or Plain. Don't reference `Forms::Input` directly where a theme role belongs. -2. **NO interpolated Tailwind/daisy class strings** — write literal class strings (`"input input-primary"`, never `"input-#{x}"`); the host's Tailwind scanner can't see a built name, so the style never ships. -3. **NO unguarded model introspection** — every model touch sits behind `respond_to?` guards + a `StandardError` rescue (see `Forms::Field#required?`, `PhlexForms::Inference`), so plain objects / Structs / untyped ActiveModel degrade to the name map. Inference must never require ActiveRecord. -4. **NO hard dependency on daisyui or phlex-reactive** — both are soft: `require`-rescue-`LoadError` + Zeitwerk `ignore` of the files that reference them. The gem must boot and render (Plain theme) without either. -5. **NO `raw`/`html_safe` on user/model data** — let Phlex escape; only gem-authored trusted markup may bypass it. Field names, choices, values are user-influenced. -6. **NO caller options silently lost** — in `field`, explicit `as:`/`choices:`/caller kwargs always win over inferred attributes. -7. **NO manual `gem push`** — release via `bin/release` (patch/minor/major/explicit; wraps `rake release[X.Y.Z]`, which stages ONLY the version file + the docs lockfile pin; the gem root `Gemfile.lock` is gitignored, correct for a library gem). - -### Always Do -1. **TDD**: write tests BEFORE implementation (RED → GREEN → REFACTOR). -2. **Preserve graceful degradation** — a change to inference/theming must keep POROs and non-daisy hosts working; the existing specs are the regression suite. -3. **Honor both themes** — a new leaf component needs a daisy form AND a `Forms::Plain::*` form that accepts-and-ignores variants, wires `aria-invalid`/`data-field-error`, and ships zero styling classes. -4. **Wire inference, don't special-case** — a new type mapping goes into `PhlexForms::Inference`'s precedence chain, behind guards, with a unit spec in the precedence table. -5. **Config gets a default** — a new `PhlexForms::Configuration` knob has a sensible default so existing apps keep working. -6. **Assert on semantics** — a spec checks `name="user[email]"`, the selected option, the error message — not a brittle full-HTML snapshot. - -## Commands - -```bash -bundle exec rspec # Full suite (unit + integration render specs) -bundle exec rubocop lib spec # Lint (rubocop -A lib spec to autocorrect) -bundle exec rake # spec + rubocop (the default task) -``` - -The docs site under `docs/` has its own bundle (Ruby 4.0.5): `cd docs && bin/dev`. - -## Slash Commands - -| Command | Purpose | -|---------|---------| -| `/plan` | Fable-powered planning → GitHub issue or `docs/plans/` markdown (read-only; execute with `/lfg`) | -| `/lfg` | Full autonomous workflow: branch → understand → explore → plan → TDD → verify → PR | -| `/tdd` | Enforce RED → GREEN → REFACTOR | -| `/architect` | Coordinate a change across the builder → components → inference → theme → live layers | -| `/security` | Security audit (HTML escaping, model-bound params, the live action whitelist, CSRF) | -| `/review-pr` | Review a PR for pattern compliance | -| `/github-review-pr` | Full PR pass: fix CI failures, then resolve review comments (in that order) | -| `/github-review-failures` | Fix failing CI checks until green | -| `/github-review-comments` | Process unresolved PR review comments | -| `/finish-prs` | Drive a stack of open PRs to merge-ready one at a time | - -## Architecture - -``` -Layer 5: Live validation lib/forms/live.rb, lib/forms/live/field.rb (phlex-reactive; the :validate action, signed identity, touched tracking) — SOFT dep -Layer 4: Theming lib/phlex_forms/theme.rb (role → component map), lib/forms/plain/*.rb (bare semantic HTML), daisy leaves are the default -Layer 3: Type inference lib/phlex_forms/inference.rb (columns/enums/associations/validators → control + attrs, all behind respond_to? guards) -Layer 2: Field components lib/forms/*.rb (Input, Select, Textarea, Checkbox, Toggle, Radio, FileInput, ...) delegating markup to daisyui via lib/phlex_forms/delegated_field.rb -Layer 1: The builder API lib/phlex_forms/builder.rb (the `field` verb + PascalCase escape hatches, row/group), lib/forms/form.rb (inline), lib/forms/base.rb (declarative classes), lib/forms/field.rb (per-field context) -Layer 0: Config + engine lib/phlex_forms/configuration.rb (theme/infer_from_model/field_variants/icon_renderer), lib/phlex_forms.rb (soft-require wiring), lib/phlex_forms/engine.rb (Rails: Stimulus controllers, locales, the live param type) - Docs site docs/ (a docs-kit Rails app), deployed via .github/workflows/deploy-docs.yml → docs-kit's reusable workflow -``` - -## The mental model - -> The model already knows. `field :notify` renders a toggle for a boolean -> column, an enum becomes a humanized select, a `belongs_to` a collection -> select — `as:`/`choices:` are overrides, not requirements. The same form -> class renders daisy or Plain by swapping a theme; live validation runs the -> real ActiveModel validators server-side. - -Everything is additive and degrades: no ActiveRecord? name-map inference. No -daisyui? Plain theme. No phlex-reactive? the `live` macro raises a clear -`FeatureUnavailable` and the Stimulus `validate: true` fallback still works. - -## Model tiers (for Claude Code commands & agents) - -Commands and agents pin a model **tier** via frontmatter aliases, not a full -model ID — aliases track the latest model in each tier, so pins never go stale: - -- `haiku` — mechanical/config work, diff pattern-scans -- `sonnet` — layer specialists / pattern-following implementation (the default for `/tdd`, the review-comment/failure runbooks) -- `opus` — orchestration, security, production/PR review (`/lfg`, `/architect`, `/security`, `/review-pr`, `/github-review-pr`) -- `fable` — pinned only on `/plan` (read-only planning that hands execution to cheaper models); otherwise choose it per-session with `/model` for architecture and the hardest debugging - -When spawning subagents for mechanical work (file finding, pattern scans), pass a -cheaper model explicitly (`model: haiku`) rather than letting them inherit the -session model. See `.claude/rules/agents.md`. - -## Testing - -- Unit specs (`spec/phlex_forms/`) cover pure logic with no rendering — `PhlexForms::Inference` (the full precedence table), `PhlexForms::Configuration`, `PhlexForms::Theme`. -- Integration specs (`spec/forms/`) render a real form through a kit-context helper (`render_form(model) { |f| ... }`, see `spec/support/phlex_helpers.rb`) and assert on the produced markup's semantics (`name=`, selected option, error message, the error variant class). -- Model doubles use `build_model` (an anonymous ActiveModel class, `spec/support/model_helpers.rb`) — no database. -- `Forms::Live` specs are guarded by `if defined?(Phlex::Reactive)` and stub the reply (the endpoint isn't booted); a class-level assertion (`skip_verify_authorized?(:validate)`) guards the one behavior specs can't drive. -- Aspire to 100% for `PhlexForms::Inference` / `Configuration` / `Theme` — the public API sites depend on. -- CI: `.github/workflows/main.yml` runs `bundle exec rspec` on Ruby 3.4 + 4.0 for every push to `main` and every PR; lint on 4.0. -- See `.claude/rules/testing.md`. - -## Release & docs deploy - -- `bin/release [patch|minor|major|X.Y.Z]` (`list` / `--dry-run` / `--force`) computes the next version, shows the commits since the last tag, confirms, then runs `rake release[X.Y.Z]`, which bumps the version, verifies `gem build --strict`, pushes, and creates the GitHub release; CI (`release.yml`) tests, builds, signs (Sigstore), and publishes to RubyGems via trusted publishing. -- The docs site deploys on release via `.github/workflows/deploy-docs.yml`, which calls docs-kit's reusable dash + GHCR workflow. `image`/`service` are `zoolutions/phlex-forms`. - -## More Documentation - -- `.claude/commands/` — slash command definitions -- `.claude/rules/` — coding style, git workflow, testing, agents -- `README.md` — the full field API / inference / theming / live-validation guide -- `docs/` — the published documentation site (docs-kit) +`AGENTS.md` is the single project-instruction file, shared with Cursor, Copilot +and Codex. Claude-only material lives in `.claude/`.