Integration of React + Webpack + Rails including server-side rendering of React, enabling a better developer experience and faster client performance.
# AGENTS.md
Instructions for AI coding agents working on the React on Rails codebase.
React on Rails is a Ruby gem + npm package that integrates React with Ruby on Rails, providing server-side rendering (SSR) via Node.js or ExecJS. This is a monorepo: the open-source gem lives at `react_on_rails/`, the npm package at `packages/react-on-rails/`, and the Pro package at `react_on_rails_pro/`.
## Reusable Workflows
- `AGENTS.md`: canonical entry point for agent instructions and workflow discovery
- Shared agent workflow skills may be installed in the user's or agent's normal
skill directory and reused across repos; they must resolve repo-specific
values through this repo's `AGENTS.md` seam. The canonical shared source is
[`shakacode/agent-workflows`](https://github.com/shakacode/agent-workflows).
Use that repo's `agent-workflows-status` and `upgrade-agent-workflows`
helpers to keep installed Codex or Claude homes current.
- When updating reusable agent workflows, skills, commands, or prompt
templates, first consider whether the change belongs in
`shakacode/agent-workflows` rather than this repo; keep local edits focused on
React on Rails-specific policy, seams, or overrides.
- `.agents/skills/`: repo-specific skills, explicit overrides, and
repo-pinned helper `bin/` copies for checkout-only agent sessions. Keep shared
workflow `SKILL.md` files installed in the user's or agent's normal skill
directory; duplicating them here creates duplicate Codex skill picker entries.
Current repo-specific skills include `$stress-test`,
`$optimize-rsc-performance`, `$react-on-rails-update-changelog`, and
`$react-on-rails-release-train-issue-evaluation`.
- `.claude/skills`: symlink to `.agents/skills` so Claude Code exposes the
repo-specific local skills kept by this checkout. Shared skills should come
from the installed shared pack, not this symlink.
- `.agents/workflows/`: repo-local workflow files for Codex, GPT, and other
non-Claude tools when this checkout needs copy/paste workflows or overrides.
- `.agents/bin/shared-skill-dir`: helper for workflow files that need a
repo-pinned helper copy or installed/shared skill directory.
- `.agents/skills/*/bin`: repo-pinned shared helper scripts for workflows that
must run inside checkout-only or directory-restricted agent sessions. These
directories intentionally do not include duplicate shared `SKILL.md` files.
- This repo deliberately does not adopt the pack's
`autonomous-merge-eligibility`, `merge-assurance`, `pr-merge-submit`, or
`autonomous-merge-closeout` helpers as repo-pinned merge or closeout gates.
When pack logic probes those absent helpers, its result is a structural
`UNKNOWN` for the unadopted helper chain, not evidence that a particular PR
is blocked. The adopted React on Rails merge seam is
`script/pr-merge-ledger <PR> --strict`, followed by the applicable phase and
merge-authority gates in this `AGENTS.md`. Shaka merge preference is `ask`;
GitHub remains authoritative for merge queue and required checks.
- `.agents/.rubocop.yml`: lint seam for repo-local agent helper scripts. Keep it
aligned with `shakacode/agent-workflows/.rubocop.yml`, with only local
toolchain compatibility adjustments such as this repo's supported Ruby target.
- `.agents/agent-workflow-drift.yml`: immutable source revision and reviewed
exact/overlay mapping for repo-pinned shared workflow files. Required CI checks
out that revision, runs the consumer-owned completeness test, then runs the
source pack's read-only drift checker. Update the manifest and hashes only as
part of a reviewed shared-workflow reconciliation.
- If a tool or skill picker only exposes installed/global skills, treat those
skills as launchers. Installed/global skills never override this repo's
`AGENTS.md`; repo-local files win only when this repo explicitly names or
keeps a local copy/override.
- Installed Shaka is the only seam validator. It validates
`.agents/agent-workflow.yml`; required CI pins the reviewed Shaka Git revision
and runs candidate-only validation. Pack management helpers such as `agent-workflows-status`,
`install-agent-workflows`, `upgrade-agent-workflows`, and `bin/validate`
remain transitional installed/shared-pack tools.
- `.agents/bin/agent-workflow-drift-manifest-test.rb`: the consumer-owned
completeness boundary for pinned shared files. It governs explicit files and
whole source-pack prefixes, including reviewed source-only exclusions, so a
new upstream helper cannot silently escape the drift manifest.
- `internal/contributor-info/agent-workflow-adoption.md`: React on Rails'
current Shaka seam boundary, transitional source-pack content, and validation
- `internal/contributor-info/portable-agent-workflows-seam-design.md`: current
Shaka seam architecture and legacy retirement status
- `internal/contributor-info/agent-pr-batch-skills.md`: contributor guide for choosing and sequencing `$plan-issue-triage`, `$plan-pr-batch`, and `$pr-batch`
- `internal/contributor-info/multi-batch-operations.md`: operator guide for running multiple batches across machines, launch surfaces, and repos
- `internal/contributor-info/issue-evaluation.md`: principles for deciding whether issues and proposed fixes are worth implementing
- When deciding whether an issue or proposed fix is worth doing, use the
installed/shared `$evaluate-issue` skill; a short invocation is
`$evaluate-issue` or "Is this issue worth fixing?"
- When deciding whether a React on Rails issue belongs on the active release
train (`release/X.Y.Z`, especially `release/17.0.0`) or should wait for
`main`, use the repo-local
`.agents/skills/react-on-rails-release-train-issue-evaluation/SKILL.md`; a
short invocation is `$react-on-rails-release-train-issue-evaluation`.
- When the user wants a ready prompt for review-only GitHub issue triage or an
all-open-issues audit, use the installed/shared `$plan-issue-triage` skill; a
short invocation is `$plan-issue-triage` or "Plan an issue triage"
- When the user wants a generated whole-surface issue/PR inventory, dependency
graph, and capacity-aware batch split, use the installed/shared `$triage`
skill; a short invocation is `$triage` or "Run triage"
- When the user wants to choose issues or PRs for a future agent/Codex/Claude
batch, use the installed/shared `$plan-pr-batch` skill to produce a ready
`$pr-batch` goal; a short invocation is `$plan-pr-batch` or "Plan a PR batch"
- When the user wants a multi-issue or multi-PR agent/Codex/Claude batch, use the
installed/shared `$pr-batch` skill; a short invocation is `$pr-batch`,
"Run an agent batch", "Run a Codex batch", or "Run a Claude batch"
- When the user wants to stop or cancel an in-flight Codex/Claude batch (for example to relaunch it with updated skills), follow the **Cancelling Or Stopping A Batch** protocol in `.agents/workflows/pr-processing.md#cancelling-or-stopping-a-batch`; there is no short skill invocation for this coordinator action
- When the user wants to audit merged batch work, missed reviews,
release-candidate risk, or possible bad merges, use the installed/shared
`$post-merge-audit` skill; reusable prompts live in
`.agents/workflows/post-merge-audit.md`
- When the user wants an adversarial PR review, red-team review, Claude/Codex
comparison review, or a stricter pre-merge gate, use the installed/shared
`$adversarial-pr-review` skill; reusable prompts live in
`.agents/workflows/adversarial-pr-review.md`
- When the user assigns an issue, PR, review-fix pass, or merge queue to an agent, follow `.agents/workflows/pr-processing.md`
- When the user asks to address PR review comments, use the installed/shared
`$address-review` skill; `.agents/workflows/address-review.md` remains a
copy/paste prompt for assistants without skill support
- When the user wants to manually verify a bug-fix PR by reproducing the failure
before the fix and confirming it is gone after (with captured evidence or
screenshots, optionally posted to the PR and issue), use the installed/shared
`$verify-pr-fix` skill; a short invocation is `$verify-pr-fix` or
"manually verify this fix"
- When the user explicitly asks for destructive React on Rails stress testing,
use the repo-local `.agents/skills/stress-test/SKILL.md`; a short invocation is
`$stress-test`
- When the user plans, implements, validates, or reviews RSC page performance
optimization in this repo, use the repo-local
`.agents/skills/optimize-rsc-performance/SKILL.md`; a short invocation is
`$optimize-rsc-performance`
- When React on Rails release-train changelog work needs `target=release` or a
PR targeting `release/X.Y.Z`, use the repo-local
`.agents/skills/react-on-rails-update-changelog/SKILL.md`; a short invocation
is `$react-on-rails-update-changelog`. For ordinary mainline changelog updates
on `main`, use the installed/shared `$update-changelog` skill.
- When a maintainer wants to run or inspect RC/beta validation across the demo
fleet, use the repo-local `.agents/skills/run-fleet-validation/SKILL.md`; a
short invocation is `$run-fleet-validation`. Its Ruby generator reads
`internal/contributor-info/demo-fleet.yml` and emits a complete lifecycle pack:
release/capability preflight, balanced subagent-driven hard-gate prompts,
report-only soft-track coverage, a durable result ledger/schema, independent
audit, authorized merge, reachability/tree-parity proof, and tracker closeout.
The same skill also generates public-only standing-health packs that verify
stable artifact currency, exact-default-head CI/smoke, review-app capability,
default-branch staleness, and the evaluated Dependabot v1 policy without
mutating demo repositories.
- Default simplify model: `claude-opus-4-8`
## External Flagship Demo Coordination
The public [`shakacode/react-on-rails-demo-flagship`](https://github.com/shakacode/react-on-rails-demo-flagship)
repo is the single clone-and-run flagship example for React on Rails Pro, React Server Components, React 19,
streaming SSR, the Node renderer, Shakapacker, and Rspack.
Update that demo repo when changes in this monorepo affect the recommended user-facing Pro/RSC path, including:
- React on Rails Pro or RSC generator output (`--pro`, `--rsc`, `react_on_rails:pro`, `react_on_rails:rsc`)
- Pro installation, licensing, or "license optional for evaluation/demo/non-production" messaging
- React, React DOM, `react-on-rails-rsc`, Shakapacker, Rspack, or Node renderer version pins/defaults
- Auto-bundling behavior for `.client.` / `.server.` files or the `'use client'` directive
- Streaming SSR/RSC helper usage, Node renderer configuration, Docker, or deployment defaults, including changes that
affect `bin/smoke` or Docker smoke-validation steps the demo repo runs during verification
Why: the flagship demo is the external proof that the Pro/RSC happy path works in a real Rails app. If this monorepo
changes the recommended path but the demo stays stale, agents and users will copy the wrong setup.
Keep one flagship demo for now. Do not create a separate OSS-only flagship unless the user explicitly asks. The demo's
README should document how to turn Pro/RSC off for comparison, but the default app should remain Pro + RSC.
Additional examples are valuable when they teach distinct repo-generation patterns, but they should not dilute or
compete with the flagship Pro/RSC path.
The machine-readable catalog of demos, tiers, and packages is `internal/contributor-info/demo-fleet.yml`.
When updating the demo, make the change in a separate checkout/branch of `react-on-rails-demo-flagship`, regenerate and
commit lockfiles when dependency changes alter them, and do not mix demo repo commits into this monorepo. Use the
JavaScript package manager declared by the demo repo (`packageManager` field or lockfile), then run focused validation
such as:
- `bundle install`
- the lockfile install command for the declared package manager (`npm ci` for the current flagship)
- `bin/shakapacker` or the equivalent asset build command documented by the demo repo
- `bin/smoke` or Docker smoke validation
## Canonical Agent Policy
`AGENTS.md` is the canonical source for repository-wide agent rules:
- Commands and test/lint workflow
- Code style and formatting expectations
- Git/PR boundaries and safety rules
- Directory and documentation boundaries
Other agent-facing docs (for example `CLAUDE.md`) should contain only tool-specific workflow notes and link back here.
If there is a conflict, `AGENTS.md` wins.
## React on Rails Pro Guardrails
React on Rails Pro includes the Ruby Pro tree and the Pro npm packages:
- `react_on_rails_pro/`
- `packages/react-on-rails-pro/`
- `packages/react-on-rails-pro-node-renderer/`
Before modifying, copying, vendoring, porting, or reimplementing Pro code from
any of those paths, read and follow the Pro-specific guardrails in
[`react_on_rails_pro/AGENTS.md`](react_on_rails_pro/AGENTS.md). Those guardrails
do not replace the Pro license or EULA; they tell agents when to stop and ask
for explicit licensing confirmation.
## Freshness And Skill Resolution
Before planning issue/PR work, creating a new branch, or creating a new
worktree, run:
```bash
git fetch --prune origin main
```
Base new issue branches and new worktrees on the freshly fetched `origin/main`
unless the user explicitly asks to reproduce an old SHA, continue an existing PR
branch, bisect, or work offline. Creating a new worktree does not fetch from
GitHub by itself.
After fetching, verify the `## Agent Workflow Configuration` seam before relying
on installed/shared skills for issue, PR, or batch work:
```bash
shaka seam check --root "$(pwd)" --ref "$(git rev-parse origin/main)"
```
This trusted-ref check establishes workflow policy. Candidate CI validation uses
the separately pinned Shaka checkout with `--local`, which grants neither policy
nor merge authority.
If a workflow explicitly needs a repo-local `.agents/skills/...` file, it should
be a repo-specific local skill such as `stress-test` or
`optimize-rsc-performance`, release-branch changelog handling such as
`react-on-rails-update-changelog`, a pinned helper `bin/` copy without
`SKILL.md`, or a deliberate override. Shared workflow skills normally resolve
from the installed/shared pack for picker-visible instructions. Helper commands
may resolve to repo-pinned `.agents/skills/<skill>/bin` copies so checkout-only
or directory-restricted agents can still run repo workflows. If a required
repo-local skill or `.agents/workflows/...` file is missing in the checkout but
present on `origin/main`, update the worktree before continuing; if it is still
missing, report the repo workflow state as `UNKNOWN`.
For user-installed shared skills, check the installed pack with:
```bash
agent-workflows-status --host codex
```
Use `--host claude` for Claude Code installs. Upgrade the transitional shared
pack, then validate this repository with trusted Shaka:
```bash
upgrade-agent-workflows --host codex
shaka seam check --root "$(pwd)" --ref "$(git rev-parse origin/main)"
```
<!-- prettier-ignore-start -->
## Agent Workflow Configuration
Resolve the trusted default branch to an immutable commit. Load and validate
`.agents/agent-workflow.yml` with the trusted installed `shaka seam check --ref REF`
command. Run the fixed executable paths reported by that command from the candidate
checkout; do not reconstruct their behavior from prose. `AGENTS.md` retains human-only boundaries.
## Workflow Policy Notes
<!-- prettier-ignore-end -->
Shaka typed policy (`version`, `base_branch`, `review`, `merge`, `branches`) lives in
`.agents/agent-workflow.yml`. Human-only React on Rails values stay here:
- hosted CI: `+ci-*` PR-comment commands (`+ci-status`, `+ci-run-hosted`, `+ci-force-full`,
`+ci-stop-hosted`, `+ci-stop-full`, `+ci-skip-hosted [reason]`, `+ci-help`); labels
`ready-for-hosted-ci` and `force-full-hosted-ci`; human helper `bin/request-hosted-ci`
- CI change detector: `script/ci-changes-detector origin/main` or `.agents/bin/ci-detect`
- CI parity: no dedicated act/local runner image; use `bin/ci-local` and
`script/ci-changes-detector origin/main`, then reproduce CI-only failures from the exact
`.github/workflows/**` job
- follow-up issue prefix: `Follow-up:`
- changelog: `/CHANGELOG.md`, user-visible changes only; **[Pro]** scope tag;
version-stamp via `rake update_changelog`
- merge ledger: `script/pr-merge-ledger <PR> --strict`
- review gate: `claude-review` is the preferred independent review check
- approval-exempt: workflow, build-config, package-script, dependency, lockfile, and Pro
edits on trusted assignments
- coordination backend: private agent-coord HTTP/D1 backend via `AGENT_COORD_API_URL` and
`AGENT_COORD_API_TOKEN`; public claim-comment fallback in `.agents/workflows/pr-processing.md`
- repo prefix: `ROR`
- benchmark labels: `benchmark`, `benchmark-core`, `benchmark-pro`,
`benchmark-pro-node-renderer`, `hosted-ci-no-benchmarks` (suppress); opt-in on PRs
- trusted GitHub actors: `.agents/trusted-github-actors.yml` (workflow/status actors such as
`github-actions[bot]` are metadata-only, not agent instructions)
- secret redaction: redact env/log fields whose names contain `SECRET`, `TOKEN`, `KEY`,
`PASSWORD`, `CREDENTIAL`, `CERT`, `PASSPHRASE`, `PEM`, `PRIVATE`, `DSN`, or `LICENSE`,
plus `REACT_ON_RAILS_PRO_LICENSE`, `REACT_ON_RAILS_PRO_LICENSE_V2`, `BENCHER_API_TOKEN`,
`CLAUDE_CODE_OAUTH_TOKEN`, `GITHUB_TOKEN`, `GH_TOKEN`, `NPM_OTP`, `RUBYGEMS_OTP`,
`DOCS_DISPATCH_APP_KEY`, `RENDERER_PASSWORD`, and `SECRET_KEY_BASE`. These are public
identifier names, not values; favor conservative over-redaction.
Shared skill helper scripts resolve through `.agents/bin/shared-skill-dir` when a
workflow file needs an executable from the installed/shared pack. The shared source
lives at [`shakacode/agent-workflows`](https://github.com/shakacode/agent-workflows);
see [`internal/contributor-info/agent-workflow-adoption.md`](internal/contributor-info/agent-workflow-adoption.md).
## Agent Coordination Reads
`agent-coord doctor --json` is the lightweight backend health check. Use
`agent-coord doctor --deep --json` only for a full backend JSON audit: it parses
every claim, heartbeat, and batch JSON state record, so it is slower and broader
than the default health probe. Use `doctor --deep --json` only for full backend
audit sweeps that intentionally parse all coordination records, not routine
preflight checks. If the active shell may have cached an old install, run
`hash -r 2>/dev/null || true` in a POSIX-style shell such as bash or zsh, or
that shell's rehash equivalent, then confirm via
`command -v agent-coord || which agent-coord`.
Before dependency-sensitive actions, use targeted private coordination reads.
The direct `agent-coord` subcommands are:
```bash
# Specific issue/PR lane
agent-coord status --repo shakacode/react_on_rails --target <issue-or-pr> --json
# Batch lane/dependency state
agent-coord status --batch-id <batch-id> --json
```
When the repo workflow calls for bounded reads, pass the same targeted status
subcommand through the installed/shared `pr-batch` helper so a slow private read
becomes explicit degraded state instead of an indefinite wait:
```bash
PR_BATCH_SKILL_DIR="${PR_BATCH_SKILL_DIR:-$(.agents/bin/shared-skill-dir pr-batch)}"
# Specific issue/PR lane
"${PR_BATCH_SKILL_DIR}/bin/agent-coord-bounded" --timeout 20 status --repo shakacode/react_on_rails --target <issue-or-pr> --json
# Batch lane/dependency state
"${PR_BATCH_SKILL_DIR}/bin/agent-coord-bounded" --timeout 20 status --batch-id <batch-id> --json
```
Do not use broad `agent-coord status` for routine lane checks. Broad private
coordination reads are audit-only; if they time out, exit 1 (unexpected error),
or exit 2, report private coordination as `UNKNOWN`/degraded and use structured
public claim comments only as advisory evidence. Any non-zero exit other than
`CLAIM_REFUSED` (exit 3) is treated as `UNKNOWN`/degraded. If targeted status
exits 0, private coordination state is authoritative. Refused claims
(`CLAIM_REFUSED` / exit 3) remain hard stops for machine agents.
## Commands
```bash
# Install dependencies
# The committed root Gemfile.lock is generated with Bundler 4.0.10; use Bundler
# 4.0.10 or newer before running root bundle commands.
bundle && (cd react_on_rails && bundle) && pnpm install
# The root Gemfile is intentionally limited to repo-wide lint, hook, release,
# and benchmark script spec tooling. After changing package Gemfiles, run bundle
# install in that package directory; after changing the root Gemfile, run bundle
# install at the repo root to sync the tooling lock.
# Build TypeScript β JavaScript
pnpm run build
# Lint (MANDATORY before every commit)
(cd react_on_rails && BUNDLE_GEMFILE=../Gemfile bundle exec rubocop) # OSS Ruby lint β CI-equivalent
# Pro Ruby lint β CI-equivalent when Pro files or RuboCop config change
(cd react_on_rails_pro && BUNDLE_GEMFILE=../Gemfile bundle exec rubocop --ignore-parent-exclusion)
pnpm run lint # JS/TS via ESLint
pnpm start format.listDifferent # Check Prettier formatting
(cd react_on_rails && bundle exec rake lint) # Package lint task (Ruby + JS + formatting)
# Optional Ruby diagnostic from the repo root (not the CI contract)
BUNDLE_GEMFILE="$(git rev-parse --show-toplevel)/Gemfile" bundle exec rubocop
# Auto-fix formatting
(cd react_on_rails && bundle exec rake autofix) # Preferred for all formatting
# Run tests
(cd react_on_rails && bundle exec rake run_rspec:gem) # Ruby unit tests (gem code)
(cd react_on_rails && bundle exec rake run_rspec:dummy) # Ruby integration tests (dummy Rails app)
pnpm run test # JavaScript/TypeScript tests
(cd react_on_rails && bundle exec rake) # Full package suite (lint + tests except examples)
# Type checking
pnpm run type-check # TypeScript
(cd react_on_rails && bundle exec rake rbs:validate) # RBS signatures
# Additional test subsets
(cd react_on_rails && bundle exec rake run_rspec) # All Ruby tests
(cd react_on_rails && bundle exec rake all_but_examples) # All tests except generated examples
(cd react_on_rails && bundle exec rake run_rspec:shakapacker_examples_basic) # Single example test
# Documentation checks
script/check-docs-sidebar # Validate docs sidebar coverage
bin/check-links # Markdown link checks (requires lychee)
# Full initial setup
bundle && (cd react_on_rails && bundle) && pnpm install && \
(cd react_on_rails && bundle exec rake shakapacker_examples:gen_all node_package && bundle exec rake)
# CI/workflow linting
actionlint # GitHub Actions lint
yamllint .github/ # YAML lint (do NOT run RuboCop on .yml files)
# Dependency version updates
rake shakapacker:update_version[9.6.1] # Update shakapacker across the monorepo
```
### Updating Shakapacker
Use `rake shakapacker:update_version[VERSION]` to update shakapacker across the entire monorepo. This single command updates all Gemfiles, package.json files, Gemfile.lock files, and pnpm-lock.yaml. Do **not** manually edit individual version references β always use the rake task to keep everything in sync.
The task handles Ruby version switching for apps that require a different Ruby version (set `RUBY_VERSION_MANAGER` to `rvm`, `rbenv`, `asdf`, or `mise` if needed; defaults to `rvm`). It continues gracefully if a single lock file update fails (e.g., due to a missing Ruby version).
After Shakapacker version or default updates, check the External Flagship Demo Coordination section to decide whether the
flagship demo needs the same change.
## Testing
- **Prefer local testing over CI iteration** β don't push "hopeful" fixes. Apply the **15-minute rule**: if 15 more minutes of local testing would catch the issue before CI does, spend the 15 minutes.
- **Never claim a test is "fixed" without running it locally first.** Use "This SHOULD fix..." or "Proposed fix (UNTESTED)" for unverified changes.
- **Automated tests passing is necessary but not sufficient.** If your changes affect how the app starts, builds, or serves, you must also verify the dev environment manually. See [Manual Dev Environment Testing](.claude/docs/manual-dev-environment-testing.md) for the full checklist.
- **Ruby**: RSpec. Unit tests in `react_on_rails/spec/react_on_rails/`, integration tests via a dummy Rails app in `react_on_rails/spec/dummy/`.
- **JavaScript/TypeScript**: Jest. Tests in `packages/react-on-rails/tests/`.
- **E2E**: Playwright. Tests in `react_on_rails/spec/dummy/e2e/playwright/e2e/`. Run with `cd react_on_rails/spec/dummy && pnpm test:e2e`.
- **The dummy app** (`react_on_rails/spec/dummy/`) is a full Rails application used for integration testing. Many tests require it.
Run specific test files:
```bash
(cd react_on_rails && bundle exec rspec spec/react_on_rails/path/to/spec.rb)
cd react_on_rails/spec/dummy && bundle exec rspec spec/path/to/spec.rb
```
## Project Structure
| Directory | Purpose |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| `react_on_rails/lib/react_on_rails/` | Ruby gem source β helpers, configuration, SSR pool, engine |
| `react_on_rails/lib/generators/` | Rails generators for `react_on_rails:install` |
| `react_on_rails/spec/` | RSpec tests (unit + integration via dummy app) |
| `react_on_rails/spec/dummy/` | Full Rails app for integration testing and E2E |
| `packages/react-on-rails/src/` | TypeScript source β client-side React integration |
| `packages/react-on-rails/tests/` | Jest tests for the npm package |
| `react_on_rails_pro/` | Pro package (separate gem + npm) |
| `rakelib/` | Rake task definitions |
| `docs/oss/` | OSS documentation β published to the [ShakaCode website](https://reactonrails.com/docs/) |
| `docs/pro/` | Pro documentation β installation, configuration, RSC, node renderer, caching |
| `internal/contributor-info/` | Internal contributor docs (not published to the website) |
| `internal/planning/` | Internal planning docs, designs, and drafts |
| `internal/react_on_rails_pro/contributors-info/` | Internal Pro contributor docs (not published to the website) |
| `internal/analysis/` | Investigation and analysis documents (kebab-case `.md` files) |
## Code Style
### Ruby (RuboCop)
Line length max 120 characters. Run `bundle exec rubocop [file]` to check.
**Line length β break long chains:**
```ruby
# Bad
content = pack_content.gsub(/import.*from.*['"];/, "").gsub(/ReactOnRails\.register.*/, "")
# Good
content = pack_content.gsub(/import.*from.*['"];/, "")
.gsub(/ReactOnRails\.register.*/, "")
```
**Named subjects in RSpec:**
```ruby
# Bad
subject { instance.method_name(arg) }
# Good
subject(:method_result) { instance.method_name(arg) }
```
**Security violations β scope disable comments tightly:**
```ruby
# rubocop:disable Security/Eval
expect { evaluate(sanitized_content) }.not_to raise_error
# rubocop:enable Security/Eval
```
### JavaScript/TypeScript
Prettier handles all formatting. Never manually format β run `rake autofix` instead.
### GitHub Actions
For GitHub Actions jobs that install Ruby gems, prefer `.github/actions/setup-bundle` over hand-written `actions/cache` plus `bundle install` steps.
The action validates a committed `Gemfile.lock`, configures the bundle path and Bundler version for later `bundle exec` steps,
restores/saves the gem cache, and supports non-frozen installs via `frozen: 'false'` for minimum-dependency jobs.
## Git Workflow
**Branch naming**: `type/descriptive-name` (e.g., `fix/ssr-hydration-mismatch`)
**Commit messages**: Explain why, not what. One logical change per commit.
**Squash merges**: When completing a GitHub squash merge, include the PR number in the squash commit title using the format `<PR title> (#<PR number>)`, for example `Docs: clarify rails new JavaScript skip flag (#3666)`. For CLI merges, pass `--subject "<PR title> (#<PR number>)"` to `gh pr merge --squash` and verify the title before confirming the merge.
**PR creation**: Use `gh pr create` with a clear title, self-contained why/rationale summary, implementation summary, and test plan. If an issue exists, link it, but do not make reviewers open the issue to understand why the PR exists; include at least a short rationale in the PR description itself.
**PR processing**: Before pushing a review-fix batch, opening a PR, marking a PR ready, requesting hosted CI, requesting force-full hosted CI, or reporting merge-readiness, run the agent PR processing flow in `.agents/workflows/pr-processing.md`: verify the work is worth doing, self-review the diff, run local validation, use the pre-push AI review and simplify gate when appropriate, batch fixes, and document exact verification evidence plus churn notes. After a PR and its reviews exist, wait for configured review agents and triage actionable review feedback before marking ready, requesting merge, or merging.
**Hosted CI usage**: Do not use hosted CI as the first real validation pass. Prefer local checks first, then optimized hosted CI when the branch is ready for remote confirmation or final validation. Use the `+ci-*` PR comment commands for an auditable decision: `+ci-status` before deciding, `+ci-run-hosted` after the final push for optimized hosted CI, `+ci-force-full` only when a maintainer intentionally wants every hosted suite to bypass optimized selection, `+ci-stop-hosted` to return an iterating PR to the required gate, `+ci-stop-full` to remove only the force-full override, `+ci-skip-hosted [reason]` only with explicit maintainer approval for a low-risk waiver, and `+ci-help` when syntax is unclear. Put one `+ci-*` command per PR comment. Human/user-token label writes through `bin/request-hosted-ci` or `gh pr edit --add-label ready-for-hosted-ci` are valid, but workflow-token label writes are not a current-head trigger; automation must dispatch workflows explicitly or use `+ci-run-hosted`.
**GitHub follow-up issues**: Follow-up issues are the exception. Prefer fixing or declining review feedback in the PR. If deferred work remains valuable, present one bundled deferred-work summary and ask whether to track it. Prefer an existing issue; otherwise create at most one bundled issue per PR unless the user explicitly approves more. New follow-up issue titles must begin with `Follow-up:`. Build multi-line issue bodies as Markdown files and pass them with `gh issue create --body-file`; do not pass escaped newline strings through `--body`.
**GitHub Actions post-merge exercise follow-ups**: Semantic changes to `.github/workflows/**` or `.github/actions/**`
are a standing exception to the default "no follow-up issue" rule. Before merge, link an existing tracking issue or
create one bundled issue titled `Follow-up: Exercise GitHub Actions changes from PR #NNNN`. The issue must name the
source PR, changed workflow/action files, exact post-merge event or secondary verification PR to exercise, expected
evidence, cleanup instructions for any verification-only PR, and owner if known. This is required for trigger,
permission, job, matrix, condition, concurrency, secret, reusable-action, command-parsing, workflow-dispatch, or
CI-routing behavior changes. It is not required for comments, docs, typo fixes, formatting-only changes, or
non-semantic actionlint cleanup when local validation evidence documents that classification.
**Process gap disposition**: When an audit, review, or batch closeout finds a recurring process miss, do not add a prose-only rule by default. The issue plan or PR evidence must choose one mechanism target: `script`, `schema`, `checklist+replay`, or `park`, and record the motivating miss, replay evidence or park reason, and non-goal. `park` means the miss is plausible but not worth mechanizing now.
### Release Version Ownership
`bundle exec rake release[...]` owns the coordinated React on Rails product-version
change. For ordinary RC and final preparation, agents prepare and stamp
`CHANGELOG.md`, but must not manually bump React on Rails' own gem/npm version
fields or create the ordinary `Bump version to ...` commit. The release task
updates the OSS and Pro gem version files, the `version` field in all five
`package.json` files, and the Ruby `Gemfile.lock` files in that generated commit.
It does not run `pnpm install` or regenerate `pnpm-lock.yaml`; workspace-protocol
dependency conversion during npm publishing is temporary and is restored afterward.
If a release-preparation or dependency-pin PR changes dependency ranges or pins,
regenerate the affected npm/pnpm lockfiles in that PR. Do not defer those lockfile
updates to the React on Rails product-version release task.
Pins for independently released dependencies are separate changes. For example,
promoting `react-on-rails-rsc` from an RC to a stable version still requires a
normal reviewed and tested PR that updates the generator pin, package metadata,
and affected lockfiles before cutting the next React on Rails RC. Do not confuse
that dependency update with manually bumping React on Rails' own version.
## Maintainer Attention Contract
Maintainer attention is for judgment, not for routine progress pings or
machine-checkable work. Agents working PRs, reviews, or batches must apply this
contract unless a maintainer explicitly narrows the run.
- **Autonomous nits**: behavior-preserving `OPTIONAL` review nits may be fixed
inline without asking when they stay inside the PR scope, are low-risk, and are
before the final-candidate debounce point: once a merge-readiness review cycle
has started, do not introduce new nit commits that would restart it.
Inside the PR scope means the file, section, or workflow copy is already part
of the PR diff or directly cited by current review feedback. Cross-copy
consistency edits are in scope only when the paired section is already in the
PR diff or directly cited by current review feedback; this excludes unrelated
cleanup, other machine lanes, reserved files, generated output not already in
scope, and separate workflow files that merely discuss the same concept.
The final-candidate debounce point begins when the agent explicitly
designates the current head as merge-ready or the final candidate in a PR
body, PR comment, or handoff, or when the agent pushes after completing the
final local validation/review gate and records that push as the candidate.
Automatically queued checks from ordinary fix-phase pushes do not count unless
that push or check set has been declared as the final readiness gate. Earlier
incremental per-file checks during the fix phase do not count.
Behavior-preserving means wording, formatting, or mechanical
whitespace/punctuation cleanup that does not alter public APIs, generated
output, runtime behavior, validation scope, or the semantic meaning of any
section that has an unresolved review thread on it. Low-risk means local and
mechanically checkable, such as a formatter-confirmed cleanup; a rename that
requires searching all callers is not low-risk. Mechanical means deterministic
and local, such as rerunning a formatter or fixing whitespace introduced by
the nit, without reasoning about runtime behavior, callers, or policy.
Qualifying examples: typo/comment punctuation, whitespace or trailing comma
cleanup, or unambiguous documentation wording.
Disqualifying examples: renaming a public method or constant, changing
generated content, altering CI or release policy, adding/removing validation,
removing an import or `require` whose module side effects are not proven by a
dedicated tool or code inspection, or touching another lane's files. If the nit
is not worth fixing, record it as deferred or declined with rationale instead
of asking "OK to fix this nit?".
Autonomous deferred/declined nit replies must include `[auto-deferred]` on its
own line plus a one-line rationale, for example:
```text
[auto-deferred]
Whitespace cleanup deferred to avoid restarting the final-candidate gate.
```
Post the tag and rationale before resolving the review thread; do not resolve
an auto-deferred thread without that reply.
If an autonomous nit fix fails local validation or self-review, repair it in
the same batch only when the repair is still mechanical and in scope;
otherwise drop or revert that nit, record the failed validation and rationale,
and promote the underlying concern to `DISCUSS` only when it is a correctness
issue, regression risk, or explicit reviewer request.
Never push a failing autonomous nit or ask the maintainer to debug it.
Escalate only when the item changes behavior, expands scope, conflicts with
policy, or has unclear risk.
- **CI-wait protocol**: while checks or review bots are running, do bounded
useful work such as self-review, local-only cleanup notes, documentation sync
that does not require pushing the active PR head, or another independent lane.
Do not introduce optional cleanup commits that restart current-head gates after
the final-candidate debounce point. Do not interrupt the maintainer for routine
"CI is still running", "CI is green", or "review arrived" updates. CI failures
and new `MUST-FIX`-tier review findings are not routine; surface them
immediately.
- **One decision point per lane**: batch genuine judgment calls into one decision
block at lane completion or hard block. The block must include the question,
options, recommendation, evidence links or command output, and the next action
after an answer. Avoid "see above" decisions that require the maintainer to
reconstruct context.
- **Self-verification before escalation**: anything provable by tests, lint,
screenshots, repro scripts, `gh` state, or code inspection must arrive with
that evidence attached. Use `UNKNOWN` for facts that could not be verified.
- **Attention metric**: batch closeouts count human decision points per PR, with
a target of at most one for low-risk lanes: lanes with no `MUST-FIX` items,
no blocking questions, and only documentation, process, or mechanical changes.
Higher counts are reported as FYI process churn, not hidden in narrative
handoffs. Counts above target invite a later check on whether smaller lanes,
sharper scope, or better batching would reduce future churn; they are not a
hard failure by themselves. A human decision point is any question, option
selection, or confirmation directed at a maintainer that required direct input,
excluding git confirmations that safety rules or explicit local-only /
inspect-before-push instructions require after the maintainer already selected
the action, such as a required confirmation before a destructive force-push. A
standalone "should I push this ordinary PR-iteration fix?" question counts.
Report it as `Decision points: N` in the FYI section of the batch handoff.
- **Confidence notes**: `merge_authority` has three states:
`auto_merge_when_gates_pass` is the only autonomous merge grant when the
current user or batch goal grants it and the release-mode rules permit it;
`ask` requires one confirmation before merging; and `none` grants no merge
authority. When `auto_merge_when_gates_pass` applies and the gate is met,
exercising it is the expected close-out β an authorized, gate-satisfied,
confident merge that is downgraded to a "ready to merge" recommendation is an
unfinished task, not a safe default. Before exercising merge authority,
complete the confidence note: validations and evidence are recorded, no
unresolved MUST-FIX threads remain, and any remaining `UNKNOWN` facts or
residual risk do not affect merge safety. Before a merge under
`auto_merge_when_gates_pass` or after an `ask` confirmation, the worker or
coordinator documents the merge qualifications in the PR description:
- which release-mode gate applied and that it was satisfied
- the confidence note: validated commands, evidence links, remaining
`UNKNOWN` facts, and residual risk
- the finalizer, when accelerated-RC requires one
This intentionally narrows merge-authority evidence to the PR description so
the merge decision is auditable from a single location. Use the issue or batch
handoff only for no-merge readiness evidence.
When merge authority is not granted, use the same confidence-note format for
merge-readiness evidence without merging:
```text
Confidence note:
- Validated: <commands or checks run and outcomes>
- Evidence: <links to CI, screenshots, logs, or inline output>
- UNKNOWN: <facts that could not be verified, or "none">
- Residual risk: <one-line risk summary, or "none">
```
## Tracking Issues And Handoffs
Keep the issue tracker for durable work β product features, real bugs, release
gates β not for transient agent-process state. Process state accretes into
clutter because "open a tracker" has no matching "close it" step.
- **Do not open a new issue for a session handoff or a point-in-time audit.** A
handoff is transient coordination and an audit is a snapshot; neither is
durable backlog. Record a handoff as a comment on the relevant parent tracking
issue (for example the roadmap umbrella), or β if a dedicated agent-coordination
repo is in use β there. If the work has no parent umbrella (a standalone PR or a
one-off batch), put the handoff in the PR's final comment or description rather
than creating an issue to hold it. Append a point-in-time audit to the standing
release audit ledger in place. Never spawn a standalone `Handoff: ...` or
`Post-rc.N audit` issue.
- **One durable ledger per recurring concern, updated in place.** Release audits
append to the standing release audit ledger; cross-agent coordination state β
the heartbeats and leases that signal which agent is live on which lane β lives
in the coordination-layer tracker. Do not create a sibling issue each cycle.
(At time of writing these are #4010 and #3974, but treat any such number as a
movable pointer: confirm it is still the live ledger before relying on it, and
update the pointer if it has been superseded β the same staleness this policy
guards against applies to the ledgers themselves.)
- **Closure follows the work, not the opener.** A tracking issue closes when its
underlying PR/work lands, done by whoever finishes the work β not by whoever
opened the tracker. "I opened it" does not mean "I must close it": WIP can
outlive a session (lost chat, unanswered question, disconnect). The heartbeat β
the coordination layer's liveness signal that flags when no agent is active on a
lane β detects abandonment, and an unfinished PR is the real signal of remaining
work; act on the PR, not on a stale tracker.
- **The 30-day test.** Before opening any tracking or meta issue, ask whether it
will still matter in 30 days. If not, it is a comment or a ledger entry, not an
issue.
- **Sweep on sight.** When a handoff/audit/process-snapshot issue's underlying
work has landed or its snapshot is obsolete, close it β first consolidating any
still-live finding into the durable ledger or a real backlog issue. Verify it is
actually resolved or superseded before closing; never close a tracker that still
fronts unfinished work.
## Release Mode And Auto-Merge Coordination
Use the current release tracker to decide whether PRs are in normal development, accelerated RC, strict RC, or final-release mode. The tracker is the live source of truth for the mode; committed docs define how to interpret it.
The repo ships releases with a **release train**: `main` never freezes and keeps absorbing batch work, RCs are stabilized on an ephemeral `release/X.Y.Z` branch, and the final is the **last good RC promoted by dropping `-rc`** β not a re-cut from `main`. The merge gate an agent must apply is a function of the **target branch's release phase** (`beta` / `rc` / `final`); the phase composes with the mode below. See **[Release-Train Branching And Phase Gating](#release-train-branching-and-phase-gating)** for the phaseβgate table and [`internal/contributor-info/release-train-runbook.md`](internal/contributor-info/release-train-runbook.md) for the full branching runbook.
- An active tracker is an open release gate issue, usually found by the existing `release` and `TRACKING` labels or the `Release gate:` title. Also search closed release gate issues updated within the last 7 days before defaulting to `development`, so agents can detect stale trackers. The mode must be recorded in the issue body, not encoded by adding more labels.
- Valid tracker modes are `development`, `accelerated-rc`, `strict-rc`, and `final-release`.
- If no active tracker exists, assume `development` mode. This is not a blocker; it means the repo is moving toward the next beta/RC/final. If a release tracker was closed within the last 7 days and lacks a closing label/comment containing `Released` or `Superseded`, report `release-mode-stale-tracker` and do not auto-merge until a maintainer confirms the mode. A maintainer can resolve the stale signal with a PR or tracker comment such as `No active release, proceed`; verify the comment author has `write`, `maintain`, or `admin` permission before treating it as maintainer confirmation. Inspect tracker labels and comments with `gh issue view <tracker> --comments --json labels,comments` before deciding that the closing signal is absent.
- If exactly one active tracker exists, read its `Agent Release Mode` block from the issue body. If the block is absent, use `strict-rc` and report the missing block.
- If multiple active trackers have different final release targets, select the tracker matching the PR's target only when the target is unambiguous from the PR body, linked issue, branch, or release/changelog text. If the PR target is unclear, or if trackers for the selected target disagree about mode or canonical status, report `release-mode-conflict` and do not auto-merge until resolved. Do not let unrelated final-release targets block each other when the PR target is clear.
- For duplicate trackers with the same final release target (the eventual semver without prerelease suffix, for example `v1.2.0.rc.1` and `v1.2.0.rc.2` share the `v1.2.0` target) and no conflicting mode, the oldest open tracker is canonical unless it explicitly says it is superseded by another tracker. If same-target trackers disagree about mode or canonical status, report `release-mode-conflict` and do not auto-merge until resolved. Agents may close clean duplicates only after preserving non-conflicting useful information in the canonical tracker and posting a closing comment that links to the canonical issue.
- Agents do not auto-create release trackers. A maintainer creates one when entering accelerated RC, strict RC, or final-release coordination.
- To avoid concurrent issue-body overwrites, re-read the tracker immediately before editing it. Prefer append-only comments for per-PR/batch status from concurrent agents, and only edit the tracker body when preserving the latest body content. If the latest tracker body changed in a way the agent cannot safely merge, post a comment with a `Tracker Update:` header containing the intended update and report the conflict; later agents must fetch tracker comments and consider both the latest body and latest unresolved `Tracker Update:` conflict comment before acting.
Reporting `release-mode-stale-tracker`, `release-mode-conflict`, or a missing
release-mode block means posting a PR comment with a `Release Mode Block:`
header, the signal name, relevant tracker URLs, and the current decision.
In `development` and `strict-rc` modes, apply the standard merge qualification in the Review Workflow section; the accelerated-RC confidence block and auto-merge threshold do not apply. In `final-release` mode, do not auto-merge; apply standard merge qualification plus the final-release audit and explicit maintainer release decision below.
During `accelerated-rc`, affected areas such as SSR, RSC, hydration, package release, generators, CI, benchmarks, and Pro/core boundaries do not cap confidence by themselves. They choose the validation checklist. Actual uncertainty, missing proof, failed checks, or unresolved findings lower confidence.
Auto-merge during accelerated RC requires a finalized PR-body confidence block. The authoring agent may draft it, but a separate coordinator, finalizer, or review agent must finalize it. The finalizer must be a different GitHub account or named GitHub check/app identity than the PR authoring agent, verifiable from the git log or GitHub review/check record. Two sessions running under the same GitHub acco