SKILL.md
Plutonium — Router & Bootstrapper
Entry point for all Plutonium work. Does three things:
- Surfaces the most expensive mistakes up front (🚨 below).
- Tells you which skills to load for greenfield work.
- Maps specific "about to…" actions to the right targeted skill (router table).
🚨 Critical (read first)
- Plutonium is generator-driven. Almost every file you'd hand-write has a
pu:*generator. Hand-written files drift from conventions and break future generator runs. - For greenfield (new app, substantial new feature, first resource in a new domain) — load the bootstrap bundle below before writing code.
- For targeted edits — use the router table to jump to the right skill.
- For anything touching tenant scoping — load
plutonium-tenancy. Don't reach forwhere(organization: ...)in a policy; fix the model instead. - Unattended execution: always pass
--dest=,--force(when re-running meta-generators),--auth=,--skip-bundle,--quietso generators don't block on prompts. See [Unattended execution](#unattended-execution). - Inspect before you act. Every targeted skill now opens with a CHECK gate — read the relevant files yourself before scaffolding or editing. Don't ask the user to describe their app when you can read it.
The mental model (read once — it decides what you should write)
Plutonium applies Rails' bargain — follow the convention and the framework carries you; reach for an escape hatch when you need one — to the layer above CRUD: auth, authorization, multi-tenancy, admin UI, business operations. Four consequences change what you should actually type.
1. Everything is derived from something you already declared
Not "defaults someone picked for you" — computed from existing declarations:
| Derived | From |
|---|---|
| Field types, required markers, select choices | model columns, associations, attachments, enums, and validations (presence: true → required; inclusion: → select choices) |
| A collection's preloads (index, kanban, export) | the policy's permitted field set — there is no includes list to write or maintain |
| Tenant scope | your associations — direct belongsto, then hasone/hasone :through, then reverse hasmany |
| Action type (record / bulk / resource) | whether the interaction declares :resource, :resources, or neither |
| An association input's typeahead | the target resource's own search block |
| CRUD, nested and action routes | one register_resource line |
⇒ Declare only what differs. A field :title matching the detected type is dead code — and one more line to fall out of step when the column changes. This is the single most common way generated-looking code goes wrong.
2. Definition and policy answer different questions
- Definition = how a field renders.
- Policy = whether it appears at all.
"Only admins see this field" is permittedattributesfor_*. Never a definition declaration, and never a condition: (that only hides UI — the route stays live).
3. Overrides are plain Ruby inheritance
AdminPortal::PostDefinition < ::PostDefinition, and the same for policies and controllers. App-level default, portal-level subclass. No registry of overrides, no precedence DSL, no merge semantics — so "why does this field show here but not there" is always readable as a class hierarchy.
4. Climb the escape-hatch ladder only as far as the problem requires
- Change an option —
input :content, as: :markdown - Render inline —
display :priority, as: :phlexi_render, with: ->(value, attrs) do … end - Write a component — a field component (subclasses the Phlexi base) plugs into
as:; anything with its own constructor goes through a block (display :card do |field| … end) - Implement a hook — controller hooks instead of reopening
create/update; pagerenderbefore/renderafterinstead ofview_template - Replace the page —
view_templateon the nested class, or an ERB view at the controller path (ERB wins when both exist)
Reaching for rung 5 on a rung-1 problem is how you end up owning breadcrumbs, the header and turbo frame wiring you never meant to touch.
Underneath all of it, it stays Rails. Models are plain ActiveRecord, controllers inherit from Rails controllers, views resolve through Rails view paths. A Plutonium resource and a hand-written controller coexist in one app.
✅ Orient before you route (CHECK — read the app, don't assume)
A one-line request rarely says whether this is a new app, a half-built one, or a multi-tenant one — and those change which path you take. Spend 30 seconds reading the app before loading a bundle or running anything:
| Read | Tells you | |
|---|---|---|
| `git log --oneline \ | head; is there a populated Gemfile + app/`? |
Greenfield vs existing → install path (plutonium-app: base.rb, never plutonium.rb on an existing app) |
grep Gemfile for plutonium; ls config/packages.rb |
Already installed? → skip install | |
ls packages/ |
What portals / feature packages already exist | |
Does any model belongs_to an org/team/tenant? |
Multi-tenant → load plutonium-tenancy before declaring scoping |
This is the global "look before you leap"; each targeted skill carries its own ASK/CHECK gate for the specifics. Never run an installer or scaffold from a one-line request without first reading what's already there.
The skills
| Skill | Covers |
|---|---|
| [[plutonium-app]] | Installation, packages (feature + portal), portal engines, mounting, register_resource (including singular and custom routes), pu:res:conn |
| [[plutonium-resource]] | The resource itself — pu:res:scaffold, field types, model layer (Plutonium::Resource::Record, has_cents, SGID, routing), definition layer (fields/inputs/displays/columns, search/filters/scopes/sorting, custom actions, bulk actions, index views, page customization) |
| [[plutonium-behavior]] | Controllers (hooks, key methods, presentation), policies (action methods, permittedattributesfor*, permittedassociations), interactions (structure, outcomes, chaining, URL generation) |
| [[plutonium-async-interactions]] | Async interactions — async, the Run STI model, failure policies (halt/continue/transactional), authorization re-derivation at perform time, registering AsyncRun as a resource (progress page + running banner), scheduling ReapJob |
| [[plutonium-ui]] | Page classes, forms, displays, tables, custom Phlex components, layouts, modals & tabs, Tailwind config, Stimulus, design tokens, .pu-* classes, Phlexi themes |
| [[plutonium-kanban]] | kanban do…end DSL in a Definition — columns, cardfields, positionon, realtime, column actions, kanban_move? policy, quick-add, static vs dynamic boards |
| [[plutonium-auth]] | Rodauth install, account types (basic / admin / SaaS), profile resource, security section |
| [[plutonium-tenancy]] | Entity scoping (associatedwith, defaultrelation_scope, three model shapes), nested resources, invites |
| [[plutonium-testing]] | pu:test:install, pu:test:scaffold, ResourceCrud/ResourcePolicy/ResourceDefinition/ResourceModel/NestedResource/PortalAccess/ResourceInteraction, AuthHelpers |
| [[plutonium-wizard]] | Multi-step flows — the wizard DSL (step/review/using:/condition:, per-step onsubmit/persist/onrollback, execute), anchoring & resume, one-time wizards + gate, registration (wizard macro + register_wizard), storage/config + SweepJob |
Greenfield bootstrap bundle
Triggers: installing Plutonium, building a new app, adding the first resource in a new domain, setting up a new portal or package, "build me a Y app", "set up X from scratch".
Load these before writing code:
plutonium-app— install, portals, packages, routes.plutonium-resource— scaffold, model, definition (the bulk of the work).plutonium-behavior— controllers, policies, interactions.plutonium-tenancy— only if multi-tenant; load before declaring entity scoping.
Add when relevant:
plutonium-authfor login / accounts / profile.plutonium-uifor custom pages, forms, components, or theming.plutonium-testingwhen scaffolding tests.
Router table
| About to… | Load |
|---|---|
| Install Plutonium, create a portal or package, mount engines, register routes (incl. singular / custom routes) | [[plutonium-app]] |
Run pu:res:scaffold, pick field types, set scaffold options |
[[plutonium-resource]] |
Edit a model, add associations, use hascents, override toparam / to_label |
[[plutonium-resource]] |
| Edit a definition — fields, inputs, displays, columns, search, filters, scopes, custom actions, bulk actions, index views, modal/slideover, page titles | [[plutonium-resource]] |
Override a controller action, hook, redirect, or resource_params |
[[plutonium-behavior]] |
Write relationscope, permittedattributesfor*, permitted_associations, action methods, or any policy override |
[[plutonium-behavior]] (+ [[plutonium-tenancy]] if scoping) |
| Write an interaction class for business logic | [[plutonium-behavior]] |
Make a bulk/long-running interaction async (async), or schedule the stalled-run reaper |
[[plutonium-async-interactions]] |
Scope a model to a tenant, write associated_with, set portal entity strategy |
[[plutonium-tenancy]] |
| Configure parent/child nested routes, custom parent resolution | [[plutonium-tenancy]] |
| Set up user invitations or entity membership | [[plutonium-tenancy]] |
Build or customize a kanban board view — kanban do…end, columns, cardfields, positionon, realtime, column actions, kanban_move? policy |
[[plutonium-kanban]] |
Build a custom page (override ShowPage/IndexPage/NewPage/EditPage), custom form, custom display, custom table, custom Phlex component |
[[plutonium-ui]] |
| Configure Tailwind, register Stimulus controllers, edit design tokens, theme forms/displays/tables, write a custom layout | [[plutonium-ui]] |
| Install Rodauth, set up accounts, configure login flow, add the profile resource | [[plutonium-auth]] |
Write tests for a resource, run pu:test:scaffold, include Plutonium::Testing::* concerns |
[[plutonium-testing]] |
Build a multi-step flow — onboarding, checkout, branching create — register a wizard / register_wizard, gate a one-time wizard |
[[plutonium-wizard]] |
Resource architecture at a glance
A resource is four cooperating layers — Plutonium auto-fills defaults from the model, so you only declare overrides:
| Layer | File | Purpose |
|---|---|---|
| Model | app/models/post.rb |
Data, validations, associations |
| Definition | app/definitions/post_definition.rb |
UI — fields, filters, actions |
| Policy | app/policies/post_policy.rb |
Authorization — who, what |
| Controller | app/controllers/posts_controller.rb |
Request handling (rarely edited — use hooks) |
Plus one optional fifth layer:
| Layer | File | Purpose |
|---|---|---|
| Interaction | app/interactions/publishpostinteraction.rb |
Business logic for custom actions |
Generator catalog
Every Plutonium generator is discoverable via rails g pu:<tab>. Always pass --dest= to skip prompts.
| Generator | Purpose | Skill |
|---|---|---|
pu:core:install |
Initial Plutonium setup | plutonium-app |
pu:core:assets |
Custom Tailwind + Stimulus toolchain | plutonium-ui |
pu:res:scaffold NAME field:type ... |
New resource (model, migration, controller, policy, definition) | plutonium-resource |
pu:res:conn RESOURCE --dest=PORTAL |
Connect resource to a portal | plutonium-app |
pu:pkg:package NAME |
Feature package | plutonium-app |
pu:pkg:portal NAME --auth=... --scope=... |
Portal package | plutonium-app |
pu:rodauth:install |
Install Rodauth base | plutonium-auth |
pu:rodauth:account NAME |
Basic Rodauth account | plutonium-auth |
pu:rodauth:admin NAME |
Hardened admin account (2FA, lockout, audit) | plutonium-auth |
pu:saas:setup --user ... --entity ... |
Meta: user + entity + membership + portal + profile + welcome + invites | plutonium-auth + plutonium-tenancy |
pu:saas:user / :entity / :membership / :portal / :welcome |
Individual SaaS pieces | plutonium-auth + plutonium-app |
pu:profile:install / :setup / :conn |
Profile resource + security section | plutonium-auth |
pu:invites:install |
User invitations package | plutonium-tenancy |
pu:invites:invitable NAME |
Mark a model as invitable | plutonium-tenancy |
pu:eject:layout |
Eject base layout for customization | plutonium-ui |
pu:eject:shell |
Eject topbar/sidebar partials | plutonium-ui |
pu:test:install |
Install Plutonium::Testing scaffolding |
plutonium-testing |
pu:test:scaffold NAME --portals=... |
Scaffold integration tests | plutonium-testing |
pu:skills:sync |
Sync Plutonium Claude skills into the project | (this skill) |
Unattended execution
Plutonium generators are interactive by default. For scripts, agents, or CI:
| Flag | Generators | Purpose |
|---|---|---|
--dest=main_app / --dest=<package> |
pu:res:scaffold, pu:res:conn, package-targeted generators |
Skip "select destination" prompt |
--force |
any | Overwrite conflicting files (required when re-running pu:saas:setup or meta-generators) |
--auth=<account> / --public / --byo |
pu:pkg:portal |
Skip auth-type prompt |
--skip-bundle |
gem-installing generators | Avoid mid-run bundle install |
--quiet |
most | Reduce output noise |
Meta-generators (pu:saas:setup) propagate flags to the generators they chain. Always pass --force when re-running a meta-generator on an app that already has some of its outputs.
Workflow summary
- Load the bootstrap bundle (or the targeted skill from the router table).
- Generate —
rails g pu:res:scaffold Model field:type ... --dest=main_app. - Migrate —
rails db:prepare. - Connect —
rails g pu:res:conn Model --dest=portal_name. - Customize — edit definition / policy as needed.
- Verify — hit the route in the browser.