CLAUDE.md
Rama main · réplica de lectura del repositorio original.
# Working agreements
Binding. Nothing is here that a session can derive by reading the code.
**Almost nothing here is mechanically enforced.** The shell guards that used to
run as hooks were removed on 2026-08-14; they tried to decide what a command
would write by parsing its text, which is undecidable, and across a full day
they caught none of the real defects while the review process caught all of
them. What survives is `permissions.deny` in `.claude/settings.json`, the
`.githooks/pre-push` lock, and `ci.yml`. Everything else below is a standing
instruction to you, and it holds because you follow it.
Detail lives in `.claude/rules/*.md`, which load when you touch the files they
cover. `docs/harness.md` says what still enforces what.
## The map
Live: `apps/api` (Go control plane), `apps/web` (React dashboard),
`apps/agent` (the WordPress plugin, published on wordpress.org as
`fleet-agent-site-manager`), `apps/marketing` (the public wpmgr.app Next.js
site), `apps/tracker` (the RUM collector; its build output ships inside the
agent zip as `assets/wpmgr-rum.js`).
Dead, do not edit, do not deploy: `apps/landing` (last commit 2026-06-20,
superseded by `apps/marketing`, absent from `pnpm-workspace.yaml`), `apps/cli`
(untouched since the 2026-06-01 squash).
Generated, never hand-edit: `apps/api/internal/api/gen/**`,
`apps/api/internal/db/sqlc/**`, `apps/web/src/routeTree.gen.ts`,
`packages/openapi-client/src/generated/**`. A hand-sync of the sqlc tree caused a
production 500. Regenerate. A `deny` rule refuses `Edit`, `Write` and
`NotebookEdit` on all four; it does not see a shell write, so `sed -i`, `tee`, a
heredoc or `git apply` reaches them unchallenged.
## Routing: who writes the code
The session you are in is a **router**. Reading, searching, planning, deciding,
replying: yours. Writing code: a builder's, in its own worktree. The builder is
Codex working from a job brief (see **Codex builds**, below), or a specialist
agent from the table. If you are that builder, read **Building from a job
brief** first.
| Path | Agent |
|---|---|
| `apps/api/migrations/**`, `apps/api/db/schema.sql`, `apps/api/db/query/**` | `database-engineer` |
| `apps/api/**/*.go` | `backend-architect` |
| `apps/web/**`, `apps/marketing/**`, `apps/tracker/**`, `packages/**` | `frontend-architect` |
| `apps/agent/**` | `wp-agent-engineer` |
| `.github/workflows/**`, `Makefile`, `scripts/**`, `infra/**` | `devops-engineer` |
| `docs/**`, `README.md`, `CHANGELOG.md` | `docs-writer` |
| auth, RLS, tenancy, crypto, the agent protocol, deletes, GC, reclamation, locks | `security-reviewer` reviews it, always, and never writes the fix |
**"It is only a small edit" is exactly when this gets skipped.** Size is not the
test. The path is the test. The one exception: a change you can describe in one
sentence, in one file, on no path in that table.
**Nothing asks you before a main-thread write to a routed path.** A hook used to,
and it is gone. The table above is a standing instruction, and the moment it
matters is the moment nothing interrupts you: you notice the path, or the change
ships unrouted and unreviewed. Two of those paths were previously refused
outright rather than queried, because neither is a judgement call, and both are
now on you: editing a migration that already exists in `HEAD` (see
`.claude/rules/db-migrations.md`), and hand-editing a generated tree by shell.
**A change that spans a migration and Go code is two agents in sequence,
`database-engineer` first**, never one agent doing both.
**This rule outranks a narrower instruction inside one session.** A session
prompt that says "do not call agents unless asked" does not repeal it. Surface
the conflict out loud and get a ruling; never resolve it silently. Taking such
an instruction literally is how a whole OAuth sign-in feature got hand-built
inline and unreviewed here.
**Pass `model: "opus"` when the change is irreversible.** Frontmatter beats
inheriting from this loop, so a dispatch drops to whatever the agent pins even
when you are on Opus. `database-engineer` and `security-reviewer` already pin
`opus`; every other builder pins `sonnet`, so the override is what raises them.
Check the frontmatter rather than trusting this sentence. Override for:
deletes, GC, retention sweeps and object-storage reclamation; locks, leader
election, job claiming and anything racing a live process; auth, crypto, key
storage, RLS and tenancy; destructive migrations. Leave UI, copy, docs, additive
endpoints, tests and config on the default. The cheap tier wrote competent code
that would have deleted a running backup's working directory; the review caught
it. Never drop the review either way: builder self-anchoring is structural and
independent of model tier.
**Every reported bug goes through a workflow, not an ad-hoc fix.** Parallel
investigation over each half of the system, then one reconciling design pass
that states the root cause with `file:line` and a failing-to-passing regression
test, then the specialist build, then an adversarial review sized to the risk,
then ship and verify. Right-size the fan-out to severity. Do not trust the
reporter's root cause, or your own first one: on the restore data-loss bug both
the reporter and one investigator had it wrong, and an ad-hoc fix would have
fixed the wrong thing.
**For a latency or performance bug, the first change is measurement.** Add
phase timing, deploy, capture one real slow request, and let the numbers name
the culprit. Reasoning from symptoms shipped four wrong fixes to one endpoint
across three releases; instrumenting found it in one.
## Briefing an agent
The measured failure here is not that long agents die. It is that a blocked
agent gets killed, and a killed agent that has not committed loses everything.
Every brief states, or it is not ready to send:
1. **One job.** A commit range, a file list, named run ids. Never a category
("check CI", "audit the tests"). Size it to finish in about ten minutes.
Splitting a long job into short ones is how work survives; running two agents
on one job at once is how work gets lost.
2. **Key files, pre-resolved**, one line of purpose each. The agent should spend
its budget on judgement, not on search.
3. **What counts as done, and what would count as a finding.**
4. **The budget, and what to do at it**: report what you have and stop.
5. **This clause, verbatim:** *Commit at the first increment that compiles,
before any checks, and push. Open the draft PR on that first commit. Do not
hold the commit for checks, fast or slow: commit first, then check, then
report. If you are interrupted after the commit, the work survives.*
A clause that waits for checks leaves the whole build window unprotected: on
2026-09-01 an agent ran twenty-one minutes uncommitted and lost every line
when interrupted, no branch, no remote ref, no PR.
A dead or interrupted agent is **resumed**, not re-run from zero. Its transcript
and its worktree both survive. Re-brief only when resuming is impossible, and
then narrower, carrying the previous attempt's partial findings.
**Never wait on a process with an unbounded loop.** `until … sleep` / `while
true` against a build, a CI run or a log file is the single shape that produced
most of this project's killed agents. Nothing denies that shape any more, so
write the bounded form yourself: a fixed number of polls, or a command run under
an explicit timeout, and report where it had got to when the count runs out.
## Codex builds
Owner ruling, 2026-10-12: builder jobs go to Codex CLI, so Claude's budget is
spent on planning and review. The router writes the brief, starts the job,
reviews the result and pushes it. Review is unchanged: `security-reviewer`
reviews every change on a path in the routing table's last row, whoever wrote
it.
- **One job per run**, briefed by the five rules above, except rule 5: Codex
cannot commit, push or open a pull request, because its sandbox keeps `.git`
read-only even in its own clone. Its brief replaces rule 5 with: *Leave your
changes uncommitted in this clone and end your report with the commit message
to use; the router reviews, commits, pushes and opens any PR.* The brief also
names the failing test to start from, the exact commands, and the report to
return.
- **Its own clone, never this repository's `.git`.** Make it with `git clone
--shared`, so objects are read through this repository and never written to
it. Install dependencies there, then run `codex exec -C <clone> -s
workspace-write -o <file> - < <brief>` in the background. Never pass
`--add-dir` for this repository's `.git` or for a shared build cache: a
builder that can write either can plant a hook, a config line or a cached
build output that later runs outside the sandbox. A Go job keeps `GOCACHE`
inside its clone. The sandbox has no network, so installs, fetches and pushes
happen outside it.
- **Read the diff before running anything from it outside the sandbox.** Refuse
a diff that touches files the brief did not name, `.githooks/`, CI, or a
generated tree or migration the brief did not ask for. Then re-run the touched
packages' full tests outside the sandbox, which blocks some of them (an
`httptest` listener on `[::1]`, anything needing Docker). Codex reporting
green is not the check.
- **Which model and effort Codex runs at lives in `~/.codex/config.toml`**, not
in this file. Raise the effort for a job in one of the irreversible categories
the routing section lists.
## Building from a job brief
If you are Codex, or a specialist agent, working from a brief: you are the
builder, not the router. The routing table and the two sections above say how
your brief was made. Do the one job it names, in the worktree you were started
in.
- **The map, Isolation, Claims, What shipped code may say, Guards and tests,
Commit messages and Long sessions bind you** exactly as they bind the router.
- **Failing test first, then commit early.** Show the failing test, the one
check allowed before the change. Make the change, commit at the first
increment that compiles, then run the other checks the brief names and commit
their fixes. Codex skips every commit step and leaves the work uncommitted.
Never a whole-repository run.
- **Commit** on the branch you were started on, with a message that starts with
the job id. Codex cannot commit inside its sandbox: it leaves the changes in
the working tree and gives the message in its report, and the router commits.
Codex never pushes, opens a pull request or creates a branch.
- **Change only what the brief names.** Never delete or move anything outside
your worktree, and never open a folder outside this repository unless the
brief names it.
- **Report what the brief asks for:** the commit, the files, the failing and
passing test lines, and anything you were unsure about.
## Isolation
Every writing agent has `isolation: worktree`, and `worktree.baseRef` is `head`
so it branches from your work rather than from `main`. Two agents in one
checkout means one commits the other's uncommitted edits; that has happened
here.
- **Stage by name.** Never `git add -A`, never `git add .`, never `commit -a`.
- Inside a worktree, plain separate commands only: heredocs with unquoted
delimiters and brace expansion are refused and cannot be re-enabled.
- `.env` is gitignored; `.worktreeinclude` carries it into worktrees.
Missing-config errors inside a worktree start there.
- Finish by handing the worktree back. **Nothing reaps worktrees now** — the
target that did was removed with the guards. Clear merged ones and orphaned
`worktree-*` branches by hand (`git worktree list`, `git worktree remove`,
`git branch -d`), and watch the disk: exhaustion has killed a build here
mid-link.
## Claims
**Every number is the output of a command you ran in this turn, and the command
goes next to the number.** Every behavioural claim is something you executed.
Recount at the moment of writing, not from memory, not from the PR body, not
from earlier in the session, because this session compacts.
**Never hard-code a count** in a comment, a workflow, a CHANGELOG entry or a
commit message. Make the script print it. `.github/workflows/api-integration.yml`
and `ci.yml` still say "344 tests" in four places; run
`grep -rhoE '^func Test[A-Za-z0-9_]+' apps/api/tests/*.go | wc -l` and compare.
This paragraph carried its own worked example until 2026-08-14, quoting what
that command returned on the day it was written. The number had drifted by six
before anyone re-ran it, inside the rule against hard-coding counts. The command
stays; the answer does not.
A claim in prose is part of the deliverable. **Wrong prose is a defect.**
## What shipped code may say
**Never name a competitor plugin in code, a code comment, a commit message, or
a committed doc.** That is the clean-room boundary, and `ci.yml` enforces it
over `docs/**.md` and root markdown. Describe the technique neutrally: "standard
minify-and-rewrite technique", never "as used by X".
**Never write a defensive disclaimer.** No "not copied from", no "original
implementation". They imply the question arose, which reads worse than saying
nothing.
**A comment that justifies a safety label describes the shipped contract, never
a live defect's mechanism.** If the honest reason for a classification is a bug
that is still unpatched, write the label and what it means for a caller, and
stop. The file, the line and the trigger sequence go to `~/.wpmgr/worklog/`, and
the mechanism is cited only once the fix is merged **and** released to the
fleet.
This bites hardest on label and classification work, where naming the mechanism
feels like rigour. It has happened here: a docblock was written to justify a
label by describing how the unsafe thing worked, so the next reader could check
the claim without re-deriving it, and it merged while the fix did not yet exist.
About an hour later the same session correctly refused to open a *public issue*
on that defect, for the reason that already applied to the commit it had just
merged. The worklog rule below covers worklogs, issues and PR bodies; nobody had
made a code comment into a disclosure before, and the reason applies
identically. A commit cannot be recalled, and rewriting history to remove one
orphans every release tag.
Note what this paragraph does not say. Naming the method, the file or the lines
would point a reader at the mechanism, which is the thing the rule exists to
keep private — an example is subject to its own rule, and the first draft of
this one was not.
The publishable form states the consequence and no more: "Unsafe on retry: a
retry can overwrite a file that now occupies an original path." True, useful to
a caller, not a recipe.
**Never name a local reference directory in a tracked ignore file**, not
`.gitignore`, not `.gcloudignore`, not `.dockerignore`. Those files are
committed, so the line publishes the reference. References are used and then
deleted, never ignored.
Two carve-outs, both deliberate. `apps/marketing/**` **may** name competitors:
comparison pages are wanted there, and only the dash rule applies. Integration
targets, the functional conflict-detection map and wordpress.org directory slugs
in `plugin_signatures` are facts about interop, not provenance, and stay.
`apps/agent/readme.txt` is the wordpress.org listing page and stays under the
strict rule.
## Standing permissions
**Agent releases publish without asking.** When a feature needs a new agent
version, ship it as part of shipping and surface the version and channel in the
reply. This covers the agent specifically; every other outward-facing action
still gets a decision.
## Guards and tests
**Prove it fires, then prove it does not over-fire.** Plant the real failure,
watch it go red, restore, watch it go green, paste both outputs with their
commands. A test nobody has seen fail is not known to test anything. Then
construct the honest cases it must not block: a guard that reddens correct work
gets switched off, and then it guards nothing.
**A guard that finds nothing must go red, not green.** This project's signature
defect is announcing success over its own errors. Ask of every check: what does
it print, and what does it exit, when its input is missing, its command fails,
or its pattern matches nothing?
**Reach the thing under test through the same code path production uses**, and
as the same database role. Proofs that opened their own connections left the RLS
policies inert while every test passed; a documented recovery statement worked
as superuser and failed as `wpmgr_app`, which is the role every install runs as.
**Build-gating logic goes in a repo script with a committed test suite**, never
in a YAML block scalar. `scripts/check-version-surfaces.sh` plus
`scripts/check-version-surfaces_test.sh` is the pattern. `ci.yml` runs the two
scripts directly, the self-test first, so a broken guard cannot pass by failing
open; `make check-versions-test` and `make check-versions` are the local names
for the same two.
## Delivery
**The only route to `main` is a pull request.** Branch, push the branch, open the
PR, let `ci.yml` and review run, merge. Never commit on `main` in the main
checkout and never `git push` while `HEAD` is `main` — not for a one-line fix,
not for a typo, not because CI will pass anyway.
**There is no server-side enforcement.** Branch protection on `main` carries
required contexts, but `enforce_admins` is deliberately `false` so an
owner-token push is accepted and no required check ever runs against it. That is
a standing decision, kept so an incident hotfix stays possible. Everything below
is therefore client-side, and a determined push always gets through:
`git push --no-verify`, `git send-pack`, and `git -c core.hooksPath=…` each skip
it. These layers stop an accident, which is what actually happened.
- `.githooks/pre-push` is the lock, and now the only one. Git hands it the
already-resolved refs, so `eval`, a `cd`, quoting, `@`, and exotic refspecs
cannot disguise a push from it. **It is repo-local config and config is never
committed, so every clone and every fresh worktree starts unprotected.**
`make hooks` installs it; `make hooks-status` says whether it is live and
fails if the installed copy has drifted from the tracked one. **Run
`make hooks-status` yourself at the start of a session that will push** —
nothing reports its absence for you any more, and a guard that is silently
absent is worse than none, because you would trust it.
**Approval has to precede the irreversible half.** On 2026-08-12 the #406 fix was
committed to `main`, pushed, and *then* followed by "want me to open a PR for
it?". The code was on `origin/main` before the question was asked, which makes it
an announcement wearing a question mark. Ask, then push.
`ci.yml` is the gate. **It does not run the integration package**, and
`.claude/rules/ci-and-build-logic.md` names that package and says why. That is
where the tenancy and RLS proofs live, so run `make test-integration` locally
before merging anything touching RLS, tenant scoping, the email domain, or
object-storage reclamation. A regression there merges green.
A multi-layer change deploys every layer: api, web, agent, marketing. Agent
before control plane when the agent fix stands alone: the fleet is only
repaired when the plugin updates. `make check-versions` before any release; the
marketing hero badge names the **agent** version, so it must not move on a
control-plane-only release.
A DoD step that cannot find its binary must fail loudly, never be skipped.
Where a toolchain binary lives is a property of this machine and not of this
file, and nothing prints it for you at session start any more: resolve it with
`command -v` in the turn you need it, and if that comes back empty, say so and
stop rather than skipping the step.
## Reporting outside this repository
**Never say "fixed" until it is merged AND deployed AND verified against the
running system.** Order: merge, release, deploy, verify, reply.
**Verify the thing, never the pipeline's report of it.** A green deploy job says
a container started. Query the deployed revision; download the published zip and
grep it; list the objects. State what you verified and how, and say plainly what
is **not** fixed, with the exact remedy, in the same message.
## Commit messages
**No assistant trailers, ever, and no session URL.** No `Co-Authored-By:
Claude`, no `Claude-Session:`, no `claude.ai/code/session` link. This repository
is public and a commit message cannot be recalled. `ci.yml`'s
`commit-hygiene` job fails the PR on either.
Commits from before this rule carry them. They stay: rewriting them changes
every commit id since June 2026 and orphans the release tags.
## Long sessions
Decisions, measured numbers and owner rulings go to `~/.wpmgr/worklog/<issue>.md`
**as they are made**, each with the command that produced it. Compaction
summarises the conversation away, and the next session re-derives it wrong.
**A worklog is private and never enters this repository.** Not in `docs/`, not
under any other path, not committed, not pushed, not attached to an issue or a
PR. This repository is public, and a worklog is the one artefact that routinely
holds what must not be: an unshipped finding, a defect's `file:line` before the
fix exists, the mechanism of a live vulnerability. It is a working note for the
next session, not a deliverable, and it has no audience outside this machine.
Write it to `~/.wpmgr/worklog/`, which is outside every checkout and every
worktree. Never create `docs/worklog/`, and never add a worklog path to
`.gitignore` — an ignore rule is itself committed, so it publishes the thing it
is hiding. The correct location leaves nothing to ignore.
On 2026-08-12 a worklog for GH #406 was written to `docs/worklog/406.md` while
the privilege escalation it described was live, unpatched and shipped, and while
the owner's standing ruling was to disclose nothing until the fix was deployed.
It was caught before any commit. The rule above exists because the instruction to
keep a worklog and the instruction to disclose nothing collided, and the session
followed the first without noticing the second.
This file is re-injected from disk after compaction. Path-scoped
`.claude/rules/` are not, until a matching file is read again. If a rule must
hold for a whole session, it belongs here.
**When a session compacts, write durable state to a file before the turn
ends.** The destination is not a free choice: this file takes guidance,
rules, pointers, working agreements, whatever is true forever and fit to be
public. `~/.wpmgr/worklog/` takes everything else that must survive, under
the rule above. If what you are about to write would not be fine to see
public forever, it belongs in the worklog, never here.
Reviews: `review.md`, read in full before reviewing anything.
## AI connection wizard
The plan for the AI connection wizard, its approved steps, the rulings behind
them, a per-step gap analysis and a build order, lives in
`~/.wpmgr/worklog/WIZARD-SPEC.md`. That path is outside this repository and
stays there, same as every other worklog (see Long sessions, above); this file
only names it, never quotes it. **Read the spec before any wizard work. Never
reconstruct the plan by reading the code.**
Current status belongs on the GitHub issues tracking each step, not here: a
status list here is exactly the kind of fact this file already bans
hard-coding, because it goes stale within days. On 2026-09-01 the session
rebuilt the wizard roadmap from the code after every compaction instead,
got it wrong each time, and the owner had to correct it repeatedly.
## The write surface
The plan for giving the AI the ability to change a site — the two independent
tracks, what is done, what is decided and must not be relitigated, and the
build order — lives in `~/.wpmgr/worklog/WRITE-SURFACE-TRACKS.md`. That path is
outside this repository and stays there, same as every other worklog; this file
only names it. **Read it before deciding what to work on next, and never
reconstruct the tracks by reading the code.**
It exists because the work is easy to lose across a compaction and was, on
2026-09-11, easy to lose behind defects found while doing something else. The
distinction that file turns on is worth repeating here: a **blocker** stops the
feature and is worked first; an **incidental defect found along the way** does
not, and runs in parallel or waits. Incidental is the load-bearing word — a
blocker can perfectly well be found while doing something else, and "I found it
along the way" is not a reason to defer something that leaves the feature
inoperable. Before sequencing anything ahead of a track in that file,
ask whether the feature literally does not work until the new thing lands. If it
does work, it was not a blocker, and putting it first is a choice being made on
the owner's behalf.