Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`.
2 changes: 1 addition & 1 deletion .claude/SKILL_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 3 additions & 3 deletions .claude/commands/github-review-comments.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---

Expand Down Expand Up @@ -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

Expand All @@ -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.
4 changes: 2 additions & 2 deletions .claude/commands/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand Down
214 changes: 162 additions & 52 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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 <n> --attach './after.png#Sidebar collapsed on mobile' --body 'Before/after for the error state.'
gh pr comment <n> --attach ./before.png --attach ./after.png # repeat the flag, up to 50 files
gh issue comment <n> --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. `<file>#<alt text>`
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 <file>` 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)
Loading
Loading