AGENTS.md is the canonical agent file; CLAUDE.md points at it
Paul's rule, project-wide: the agent-instructions file is AGENTS.md, with a short CLAUDE.md left behind so Claude Code still finds it. amadan, paulca.com, seapointish and woodstar already work this way; this brings the platform repo into line, using amadan's pointer text verbatim. The in-repo references move with it: eleven Go files name the rules file in comments, and those now say AGENTS.md. Plans and specs under docs/superpowers/ are left alone — they record what was true when they were written.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
17 files changed,
+214
−203
AGENTS.md+192 −0CLAUDE.md+4 −185domains_cli_test.go+2 −2fleets_cli_test.go+1 −1internal/appconfig/syncexec.go+2 −2internal/appconfig/syncexec_test.go+1 −1internal/applog/record.go+1 −1internal/applog/record_test.go+1 −1internal/console/apiservices.go+1 −1internal/console/apiservices_test.go+1 −1internal/console/apistore.go+1 −1internal/console/economicspage_test.go+1 −1internal/console/ladder_test.go+1 −1internal/status/status_test.go+1 −1services_cli_test.go+2 −2store.go+1 −1store_test.go+1 −1
diff --git a/AGENTS.md b/AGENTS.md| new file mode 100644 |
| index 000000000..ee741134a |
| --- /dev/null |
| +++ b/AGENTS.md |
| @@ -0,0 +1,192 @@ |
| +# AGENTS.md |
| + |
| +This file provides guidance to AI coding agents working with code in |
| +this repository. It is the canonical agent-instructions file — keep it |
| +current here, not in a tool-specific file. (Claude Code reads it via the |
| +`CLAUDE.md` pointer.) |
| + |
| +CARLOS platform — bucket-as-control-plane deployment platform. Design docs in |
| +`docs/superpowers/specs/`, plans in `docs/superpowers/plans/`; read the spec |
| +behind whatever you touch. |
| + |
| +## Delegation policy (Paul's rule, 2026-08-02) |
| + |
| +Delegate mechanical work to subagents on the cheapest model that fits, and |
| +have Opus review anything that grants authority or touches production paths: |
| + |
| +- **Haiku** — transcription-grade work: the plan contains the exact code, |
| + single-file mechanical fixes, doc edits. |
| +- **Sonnet** — implementation from a precise brief: 1-3 files, clear |
| + interfaces, established conventions to follow. |
| +- **Opus** — design judgment, multi-file integration, anything security- or |
| + production-touching; and the **reviewer** for work in those areas. |
| +- **Fable** — where the tier changes the outcome, not the speed (Paul, |
| + 2026-08-03): implementation whose correctness rests on several interacting |
| + invariants at once (converge-grade, root-side, fleet-wide blast radius), |
| + whole-branch final reviews, and deep work that wants a fresh |
| + full-capability context window. It burns usage budget fastest — never |
| + spend it where a precise Sonnet brief or an Opus review already fits. |
| + |
| +Always name the model explicitly when dispatching; an omitted model silently |
| +inherits the session's (usually the most expensive). A precise brief beats a |
| +capable model — exact values, file paths and interfaces go in the brief. |
| + |
| +## Operate via API and CLI (Paul's rule, 2026-08-06) |
| + |
| +Day-to-day CARLOS administration goes through the console API and the |
| +`carlos` CLI — listing routes, inspecting apps, checking channels, all of it. |
| +Direct AWS access (SSM sessions, S3 console, aws CLI) is reserved for work on |
| +the platform itself: building CARLOS internals, provisioning infra, debugging |
| +below the API surface. |
| + |
| +If a day-to-day task has no API/CLI command, that is a product gap, not a |
| +license to drop to AWS: plan and build the command (or file the gap) instead |
| +of working around it. Box-local-only counts as a gap too. (2026-08-06: |
| +`carlos instances` lists routes but only by opening the registry DB on local |
| +disk, so an agent went to Systems Manager to run it — the missing API-backed |
| +remote form was the bug.) |
| + |
| +The design-side corollary (Paul, 2026-08-08): **every `carlos` command is |
| +designed for a user with no access to the underlying infra and no box |
| +commands.** The command alone must complete the job — no follow-up SSM |
| +session, AWS console visit, or on-box verb may be required for it to |
| +deliver or verify its result. |
| + |
| +## Every change lands through a PR (Paul's rule, 2026-08-10) |
| + |
| +**Nothing merges to `main` without a pull request.** No local merge to |
| +`main`, no direct commit to `main`, no fast-forward "it's only a docs |
| +change" — every change, however small, goes up as a branch and lands |
| +through a PR. That includes doc-only edits, one-line fixes, and revert |
| +commits. |
| + |
| +Agents: this rule is on you as much as on people. When work is complete, |
| +push the branch and open the PR — never a local merge, never a direct |
| +commit, however small the change. |
| + |
| +**Merging that PR is fine** (Paul, 2026-08-16, clarifying an earlier |
| +wording that read as a blanket ban and cost a session a round trip). The |
| +rule is about how work REACHES `main`, not about whose hand lands it: the |
| +PR has to exist, so that it can carry the context, the iterations and the |
| +several commits that a single squashed push to `main` would flatten. Once |
| +it exists and is green, `gh pr merge` is an ordinary step. |
| + |
| +Use the judgement the rest of this file asks for: a PR that is still |
| +collecting review, that is gated on something (`#62`), or whose diff you |
| +would want a second pair of eyes on, waits for a person. |
| + |
| +The reason is timing, not ceremony. `.github/workflows/ci.yml` triggers on |
| +`pull_request` *and* on `push` to `main`, so a locally merged change does |
| +still meet the gate — but only **after** it is on `main`, where a red run |
| +is an incident to clean up rather than a merge that simply doesn't happen. |
| +The PR is the one place the gate runs against the merge result while the |
| +change can still be turned away, and the one place the branch's diff is |
| +read as a single piece rather than commit by commit. |
| + |
| +## Build & test |
| + |
| +- `go build ./...`, `go vet ./...`, `go test ./... -count=1` — all clean, |
| + every package (21 as of 2026-08-08; the count grows), before any commit |
| + lands. |
| +- `gofmt -l` clean for files you touch (three pre-existing unformatted files |
| + elsewhere are left alone). |
| +- `.github/workflows/ci.yml` runs that same gate on every PR, plus a |
| + `GOOS=darwin` compile. **The gate is not Linux-only**: `internal/host` is |
| + linked into the CLI, which people run on Macs, so a Linux-only type |
| + assumption breaks `go build .` for them. Cross-compiling is enough to catch |
| + it — no Mac required. |
| +- Sandbox note: use `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod |
| + GOPROXY=off`; go commands may need the sandbox disabled ("read-only file |
| + system" from the toolchain is the tell). |
| + |
| +## Rules this repo has paid for |
| + |
| +- **When a storage shape changes, the consumer list is everything that PARSES |
| + the shape** — not everything that calls the API. (Learned three times.) |
| +- **Wire-shape changes are additive**: live pointers/manifests exist in |
| + production buckets; anything written before your change must still parse. |
| +- **Production channel names are pinned in tests** |
| + (`internal/release/laddercheck_test.go`) — the fleet runs on `canary/*` |
| + channels today; a ladder change that refuses them stops deploys. |
| +- **Adoption swaps a symlink; it does not restart processes.** Order is: |
| + promote → adopt → restart. Long-running units (console, keymail relay) need |
| + the restart; hibernating tenants pick it up on wake. |
| +- **A route's Channel and Environment belong to its instance record** once the |
| + route is `Provisioned` (main, 2026-08-10). The provisioning reconciler |
| + converges both from `apps/<sqid>/<app>/instances/<host>.json` on every pass, |
| + so a `carlos route -channel` on the box lands and is reverted within seconds |
| + of the next serial bump — `carlos route` refuses both axes on such a row and |
| + names the record. Reconcile also ADOPTS an existing route whose shape matches |
| + an appearing record, which is how the fleet gets backfilled with no box |
| + commands. Adoption stamps `Provisioned`, and `Provisioned` is the one |
| + condition that GRANTS the removal phase authority over a row (the loop also |
| + gates on region/pool scope and on the record being gone) — so adoption is an |
| + authority transfer, |
| + and the shape match is what stands between a backfill and a retired |
| + production route. It refuses any row carrying a column the record cannot |
| + express (`Backing`, `Tunnel`, `Caps`) or owned by another pass (`Platform`, |
| + `Canary`). |
| +- **A ResponseWriter wrapper that edits headers needs its own header map.** |
| + Wrappers in `internal/edge` embed the writer below, so `Header()` is ONE |
| + shared map from handler to connection — and `maybeStore` caches |
| + `rec.Header().Clone()`. The gzip wrapper (`internal/edge/compress.go`) keeps |
| + a private map and writes `Content-Encoding` only to the real writer's; on the |
| + shared map it would have stored an identity body labelled gzip and served it |
| + to every later visitor. Two corollaries it paid for: a header set AFTER |
| + `WriteHeader` is a TRAILER, so the wrapper must sync late additions down or |
| + it silently eats them; and canonical for `ETag` is `Etag`, so a raw map-key |
| + comparison spelled `"ETag"` matches nothing. Ordering is load-bearing too: |
| + recorder ABOVE the compressor (cache stores identity), usage counter BELOW it |
| + (tenants are billed for wire bytes), wrap site AFTER the upgrade branch. |
| +- **Console security gates are ordered**: session → CSRF → owner (byte-identical |
| + 404) → `requireFreshAuth` LAST. A non-member must never get a redirect that |
| + confirms an account exists. |
| +- **Verify against the thing you changed, with the binary you built.** Stale |
| + DNS answers and stale `CARLOS_BIN` binaries have both produced false |
| + "verified" reports in one day. |
| +- Icons are vendored Lucide (in-code SVG constants, never CDN); emoji is |
| + content, icons are chrome. |
| +- **A test that shells out to the carlos binary must build its env through |
| + `carlosTestEnv`** (masks `CARLOS_CREDENTIALS`, `HOME`, `CARLOS_CONSOLE`) |
| + **and set `cmd.Dir` to a scratch dir** — the binary walks cwd ancestors for |
| + `.carlos/config`. The fallback is the operator's real login and default |
| + console; a logged-in laptop running `go test ./...` minted 40+ real accounts |
| + on the production console before this was isolated. |
| + |
| +## Deploy reality (2026-08-02; convergence rewritten 2026-08-08) |
| + |
| +- Flagship (keymail AWS acct): console, kass, keymail, platform-site. Ship via |
| + `scripts/ship-app.sh` / `promote-app.sh` in the pinfra checkout — the signing |
| + key comes from SSM inside the script, never a terminal. |
| +- Tito production is self-hosted CARLOS on `carlos-tito-ie` (titogo AWS acct). |
| +- keymail.dev deploys via `carlos ship`/`promote` (account `bae`), NOT via its |
| + repo's legacy `deploy/deploy.sh` — see that repo's CLAUDE.md. |
| +- **Since main 1fba18f (PR #108, live both boxes 2026-08-08): convergence is |
| + seconds, not ticks.** Console-mediated writes bump `control/serial`; boxes |
| + poll it at 2s and run the full pass set. Direct-bucket promotes (pinfra |
| + scripts) don't bump but canary/* channels are name-scoped fast anyway. |
| + Blocked stages relay to a root pass (`carlos-stage.path` — must be armed; |
| + update-platform-binary.sh does it). `carlos deploy` is the member one-command |
| + form: ship → promote → wait until `X-Carlos-Version` on the URL reports the |
| + shipped build over 2xx. Verify deploys with that header (`curl -sI |
| + --resolve`), never a cached answer. `-I` is fine for the VERSION header, but |
| + it sends a HEAD — and HEAD is excluded from edge compression, so anything |
| + about `Content-Encoding` needs a GET (`curl -s -D - -o /tmp/b`) instead. |
| +- **Console restarts: manual until the route is unit-stamped** (#110, fixed |
| + in main 916cd6b, not yet rolled). Routes can now name the systemd unit that |
| + owns their process (`carlos route -registry /data/registry.db -host |
| + <console-host> -unit console.service`, run as carlos, not root); a stamped |
| + route is cycled by adoption itself, and a failed named-unit restart rolls |
| + the adoption back instead of stamping a lying header. Until a box runs |
| + 916cd6b+ AND its console row is stamped, the old rule stands: promote → |
| + adopt → `systemctl restart console.service`, and the header overclaims |
| + until the restart. Ordinary unit/exec tenants restart themselves either way. |
| +- **`carlos restart -app X`** (#113, merged 916cd6b, live after the same |
| + roll): member-facing process cycle with no version or config change — |
| + console door touches `apps/<acct>/<app>/restart-gen.json` + bumps the |
| + serial; boxes act on the serial move (exec children bounced in-process, |
| + unit-shaped routes via root `carlos adopt -restart-only` through |
| + `carlos-restart.path`, which update-platform-binary.sh arms since pinfra |
| + PR #10). Hibernating instances are never woken. Static routes carry |
| + `X-Carlos-Version` from the same roll (#112), so `carlos deploy -kind |
| + static -host <site>` verifies static sites too. |
diff --git a/CLAUDE.md b/CLAUDE.md| index 9fa6ab4a3..270f5c9ca 100644 |
| --- a/CLAUDE.md |
| +++ b/CLAUDE.md |
| @@ -1,187 +1,6 @@ |
| # CLAUDE.md |
| -CARLOS platform — bucket-as-control-plane deployment platform. Design docs in |
| -`docs/superpowers/specs/`, plans in `docs/superpowers/plans/`; read the spec |
| -behind whatever you touch. |
| - |
| -## Delegation policy (Paul's rule, 2026-08-02) |
| - |
| -Delegate mechanical work to subagents on the cheapest model that fits, and |
| -have Opus review anything that grants authority or touches production paths: |
| - |
| -- **Haiku** — transcription-grade work: the plan contains the exact code, |
| - single-file mechanical fixes, doc edits. |
| -- **Sonnet** — implementation from a precise brief: 1-3 files, clear |
| - interfaces, established conventions to follow. |
| -- **Opus** — design judgment, multi-file integration, anything security- or |
| - production-touching; and the **reviewer** for work in those areas. |
| -- **Fable** — where the tier changes the outcome, not the speed (Paul, |
| - 2026-08-03): implementation whose correctness rests on several interacting |
| - invariants at once (converge-grade, root-side, fleet-wide blast radius), |
| - whole-branch final reviews, and deep work that wants a fresh |
| - full-capability context window. It burns usage budget fastest — never |
| - spend it where a precise Sonnet brief or an Opus review already fits. |
| - |
| -Always name the model explicitly when dispatching; an omitted model silently |
| -inherits the session's (usually the most expensive). A precise brief beats a |
| -capable model — exact values, file paths and interfaces go in the brief. |
| - |
| -## Operate via API and CLI (Paul's rule, 2026-08-06) |
| - |
| -Day-to-day CARLOS administration goes through the console API and the |
| -`carlos` CLI — listing routes, inspecting apps, checking channels, all of it. |
| -Direct AWS access (SSM sessions, S3 console, aws CLI) is reserved for work on |
| -the platform itself: building CARLOS internals, provisioning infra, debugging |
| -below the API surface. |
| - |
| -If a day-to-day task has no API/CLI command, that is a product gap, not a |
| -license to drop to AWS: plan and build the command (or file the gap) instead |
| -of working around it. Box-local-only counts as a gap too. (2026-08-06: |
| -`carlos instances` lists routes but only by opening the registry DB on local |
| -disk, so an agent went to Systems Manager to run it — the missing API-backed |
| -remote form was the bug.) |
| - |
| -The design-side corollary (Paul, 2026-08-08): **every `carlos` command is |
| -designed for a user with no access to the underlying infra and no box |
| -commands.** The command alone must complete the job — no follow-up SSM |
| -session, AWS console visit, or on-box verb may be required for it to |
| -deliver or verify its result. |
| - |
| -## Every change lands through a PR (Paul's rule, 2026-08-10) |
| - |
| -**Nothing merges to `main` without a pull request.** No local merge to |
| -`main`, no direct commit to `main`, no fast-forward "it's only a docs |
| -change" — every change, however small, goes up as a branch and lands |
| -through a PR. That includes doc-only edits, one-line fixes, and revert |
| -commits. |
| - |
| -Agents: this rule is on you as much as on people. When work is complete, |
| -push the branch and open the PR — never a local merge, never a direct |
| -commit, however small the change. |
| - |
| -**Merging that PR is fine** (Paul, 2026-08-16, clarifying an earlier |
| -wording that read as a blanket ban and cost a session a round trip). The |
| -rule is about how work REACHES `main`, not about whose hand lands it: the |
| -PR has to exist, so that it can carry the context, the iterations and the |
| -several commits that a single squashed push to `main` would flatten. Once |
| -it exists and is green, `gh pr merge` is an ordinary step. |
| - |
| -Use the judgement the rest of this file asks for: a PR that is still |
| -collecting review, that is gated on something (`#62`), or whose diff you |
| -would want a second pair of eyes on, waits for a person. |
| - |
| -The reason is timing, not ceremony. `.github/workflows/ci.yml` triggers on |
| -`pull_request` *and* on `push` to `main`, so a locally merged change does |
| -still meet the gate — but only **after** it is on `main`, where a red run |
| -is an incident to clean up rather than a merge that simply doesn't happen. |
| -The PR is the one place the gate runs against the merge result while the |
| -change can still be turned away, and the one place the branch's diff is |
| -read as a single piece rather than commit by commit. |
| - |
| -## Build & test |
| - |
| -- `go build ./...`, `go vet ./...`, `go test ./... -count=1` — all clean, |
| - every package (21 as of 2026-08-08; the count grows), before any commit |
| - lands. |
| -- `gofmt -l` clean for files you touch (three pre-existing unformatted files |
| - elsewhere are left alone). |
| -- `.github/workflows/ci.yml` runs that same gate on every PR, plus a |
| - `GOOS=darwin` compile. **The gate is not Linux-only**: `internal/host` is |
| - linked into the CLI, which people run on Macs, so a Linux-only type |
| - assumption breaks `go build .` for them. Cross-compiling is enough to catch |
| - it — no Mac required. |
| -- Sandbox note: use `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod |
| - GOPROXY=off`; go commands may need the sandbox disabled ("read-only file |
| - system" from the toolchain is the tell). |
| - |
| -## Rules this repo has paid for |
| - |
| -- **When a storage shape changes, the consumer list is everything that PARSES |
| - the shape** — not everything that calls the API. (Learned three times.) |
| -- **Wire-shape changes are additive**: live pointers/manifests exist in |
| - production buckets; anything written before your change must still parse. |
| -- **Production channel names are pinned in tests** |
| - (`internal/release/laddercheck_test.go`) — the fleet runs on `canary/*` |
| - channels today; a ladder change that refuses them stops deploys. |
| -- **Adoption swaps a symlink; it does not restart processes.** Order is: |
| - promote → adopt → restart. Long-running units (console, keymail relay) need |
| - the restart; hibernating tenants pick it up on wake. |
| -- **A route's Channel and Environment belong to its instance record** once the |
| - route is `Provisioned` (main, 2026-08-10). The provisioning reconciler |
| - converges both from `apps/<sqid>/<app>/instances/<host>.json` on every pass, |
| - so a `carlos route -channel` on the box lands and is reverted within seconds |
| - of the next serial bump — `carlos route` refuses both axes on such a row and |
| - names the record. Reconcile also ADOPTS an existing route whose shape matches |
| - an appearing record, which is how the fleet gets backfilled with no box |
| - commands. Adoption stamps `Provisioned`, and `Provisioned` is the one |
| - condition that GRANTS the removal phase authority over a row (the loop also |
| - gates on region/pool scope and on the record being gone) — so adoption is an |
| - authority transfer, |
| - and the shape match is what stands between a backfill and a retired |
| - production route. It refuses any row carrying a column the record cannot |
| - express (`Backing`, `Tunnel`, `Caps`) or owned by another pass (`Platform`, |
| - `Canary`). |
| -- **A ResponseWriter wrapper that edits headers needs its own header map.** |
| - Wrappers in `internal/edge` embed the writer below, so `Header()` is ONE |
| - shared map from handler to connection — and `maybeStore` caches |
| - `rec.Header().Clone()`. The gzip wrapper (`internal/edge/compress.go`) keeps |
| - a private map and writes `Content-Encoding` only to the real writer's; on the |
| - shared map it would have stored an identity body labelled gzip and served it |
| - to every later visitor. Two corollaries it paid for: a header set AFTER |
| - `WriteHeader` is a TRAILER, so the wrapper must sync late additions down or |
| - it silently eats them; and canonical for `ETag` is `Etag`, so a raw map-key |
| - comparison spelled `"ETag"` matches nothing. Ordering is load-bearing too: |
| - recorder ABOVE the compressor (cache stores identity), usage counter BELOW it |
| - (tenants are billed for wire bytes), wrap site AFTER the upgrade branch. |
| -- **Console security gates are ordered**: session → CSRF → owner (byte-identical |
| - 404) → `requireFreshAuth` LAST. A non-member must never get a redirect that |
| - confirms an account exists. |
| -- **Verify against the thing you changed, with the binary you built.** Stale |
| - DNS answers and stale `CARLOS_BIN` binaries have both produced false |
| - "verified" reports in one day. |
| -- Icons are vendored Lucide (in-code SVG constants, never CDN); emoji is |
| - content, icons are chrome. |
| -- **A test that shells out to the carlos binary must build its env through |
| - `carlosTestEnv`** (masks `CARLOS_CREDENTIALS`, `HOME`, `CARLOS_CONSOLE`) |
| - **and set `cmd.Dir` to a scratch dir** — the binary walks cwd ancestors for |
| - `.carlos/config`. The fallback is the operator's real login and default |
| - console; a logged-in laptop running `go test ./...` minted 40+ real accounts |
| - on the production console before this was isolated. |
| - |
| -## Deploy reality (2026-08-02; convergence rewritten 2026-08-08) |
| - |
| -- Flagship (keymail AWS acct): console, kass, keymail, platform-site. Ship via |
| - `scripts/ship-app.sh` / `promote-app.sh` in the pinfra checkout — the signing |
| - key comes from SSM inside the script, never a terminal. |
| -- Tito production is self-hosted CARLOS on `carlos-tito-ie` (titogo AWS acct). |
| -- keymail.dev deploys via `carlos ship`/`promote` (account `bae`), NOT via its |
| - repo's legacy `deploy/deploy.sh` — see that repo's CLAUDE.md. |
| -- **Since main 1fba18f (PR #108, live both boxes 2026-08-08): convergence is |
| - seconds, not ticks.** Console-mediated writes bump `control/serial`; boxes |
| - poll it at 2s and run the full pass set. Direct-bucket promotes (pinfra |
| - scripts) don't bump but canary/* channels are name-scoped fast anyway. |
| - Blocked stages relay to a root pass (`carlos-stage.path` — must be armed; |
| - update-platform-binary.sh does it). `carlos deploy` is the member one-command |
| - form: ship → promote → wait until `X-Carlos-Version` on the URL reports the |
| - shipped build over 2xx. Verify deploys with that header (`curl -sI |
| - --resolve`), never a cached answer. `-I` is fine for the VERSION header, but |
| - it sends a HEAD — and HEAD is excluded from edge compression, so anything |
| - about `Content-Encoding` needs a GET (`curl -s -D - -o /tmp/b`) instead. |
| -- **Console restarts: manual until the route is unit-stamped** (#110, fixed |
| - in main 916cd6b, not yet rolled). Routes can now name the systemd unit that |
| - owns their process (`carlos route -registry /data/registry.db -host |
| - <console-host> -unit console.service`, run as carlos, not root); a stamped |
| - route is cycled by adoption itself, and a failed named-unit restart rolls |
| - the adoption back instead of stamping a lying header. Until a box runs |
| - 916cd6b+ AND its console row is stamped, the old rule stands: promote → |
| - adopt → `systemctl restart console.service`, and the header overclaims |
| - until the restart. Ordinary unit/exec tenants restart themselves either way. |
| -- **`carlos restart -app X`** (#113, merged 916cd6b, live after the same |
| - roll): member-facing process cycle with no version or config change — |
| - console door touches `apps/<acct>/<app>/restart-gen.json` + bumps the |
| - serial; boxes act on the serial move (exec children bounced in-process, |
| - unit-shaped routes via root `carlos adopt -restart-only` through |
| - `carlos-restart.path`, which update-platform-binary.sh arms since pinfra |
| - PR #10). Hibernating instances are never woken. Static routes carry |
| - `X-Carlos-Version` from the same roll (#112), so `carlos deploy -kind |
| - static -host <site>` verifies static sites too. |
| +Guidance for AI coding agents working in this repository lives in |
| +**AGENTS.md**. Claude Code: read AGENTS.md. Update AGENTS.md — not this |
| +file — going forward; this file is only a pointer so Claude Code finds |
| +it. |
diff --git a/domains_cli_test.go b/domains_cli_test.go| index bdf969f1f..5f8aa3fb7 100644 |
| --- a/domains_cli_test.go |
| +++ b/domains_cli_test.go |
| @@ -6,7 +6,7 @@ |
| // full verb (flag parsing, printing) against the same kind of fake console, |
| // credentials written straight to disk. Usage-error paths (log.Fatal, would |
| // kill an in-process test binary) run as a real subprocess, carlosTestEnv + |
| -// scratch cmd.Dir per CLAUDE.md's rule. |
| +// scratch cmd.Dir per AGENTS.md's rule. |
| package main |
| import ( |
| @@ -529,7 +529,7 @@ func TestRunDomainsAttachWildcardFlagSentOnlyWhenNamed(t *testing.T) { |
| // malshaped arguments as a real subprocess (buildCarlos) — the only way to |
| // observe a log.Fatal/os.Exit refusal without killing the test binary. |
| // carlosTestEnv masks CARLOS_CREDENTIALS/HOME/CARLOS_CONSOLE and cmd.Dir is |
| -// a scratch dir (CLAUDE.md's rule), so a case that DID try to dial out would |
| +// a scratch dir (AGENTS.md's rule), so a case that DID try to dial out would |
| // fail on "not logged in", not silently succeed. |
| func TestDomainsCLIUsageErrors(t *testing.T) { |
| bin := buildCarlos(t) |
diff --git a/fleets_cli_test.go b/fleets_cli_test.go| index 167ef85c2..3a0f257d9 100644 |
| --- a/fleets_cli_test.go |
| +++ b/fleets_cli_test.go |
| @@ -649,7 +649,7 @@ func TestRunAppsPlaceHitsRightPathAndClears(t *testing.T) { |
| // a log.Fatal/os.Exit refusal without killing the test binary. Every case |
| // here must exit nonzero without ever reaching the network: carlosTestEnv |
| // masks CARLOS_CREDENTIALS/HOME/CARLOS_CONSOLE and cmd.Dir is a scratch dir |
| -// (CLAUDE.md's rule — a subprocess that inherited a real login has minted |
| +// (AGENTS.md's rule — a subprocess that inherited a real login has minted |
| // real accounts on the flagship before), so a case that DID try to dial out |
| // would fail on "not logged in", not silently succeed. |
| func TestFleetsAndPlaceCLIUsageErrors(t *testing.T) { |
diff --git a/internal/appconfig/syncexec.go b/internal/appconfig/syncexec.go| index 2c7f9a381..1c897955b 100644 |
| --- a/internal/appconfig/syncexec.go |
| +++ b/internal/appconfig/syncexec.go |
| @@ -184,7 +184,7 @@ func SyncExec(ctx context.Context, reg *registry.Registry, s release.Store, priv |
| // A value THIS binary doesn't understand, in a bundle that |
| // does state one: a statement, so it decides — treated as |
| // not opted in, loudly, once per bundle per pass, never a |
| - // hard failure (additive discipline, CLAUDE.md). |
| + // hard failure (additive discipline, AGENTS.md). |
| fmt.Printf(" %s/%s: config: exec_delivery %q in environment %q is not a value this binary understands — ignoring it (not opted in); upgrade carlos if this is expected\n", acct, app, eb.m.ExecDelivery, env) |
| c, decided = optedInRes{v: false, known: true}, true |
| } |
| @@ -233,7 +233,7 @@ func SyncExec(ctx context.Context, reg *registry.Registry, s release.Store, priv |
| default: |
| // A manifest a NEWER carlos wrote with a value THIS binary |
| // doesn't understand must not brick this box's sync (additive |
| - // discipline, CLAUDE.md) — ignore it, loudly, once, and treat |
| + // discipline, AGENTS.md) — ignore it, loudly, once, and treat |
| // the app as not opted in rather than refusing the whole pass. |
| fmt.Printf(" %s/%s: config: exec_delivery %q in the default bundle is not a value this binary understands — ignoring it (not opted in); upgrade carlos if this is expected\n", acct, app, def.m.ExecDelivery) |
| c = optedInRes{v: false, known: true} |
diff --git a/internal/appconfig/syncexec_test.go b/internal/appconfig/syncexec_test.go| index 9daa680cf..aab960ce0 100644 |
| --- a/internal/appconfig/syncexec_test.go |
| +++ b/internal/appconfig/syncexec_test.go |
| @@ -706,7 +706,7 @@ func TestSyncExecUnoptedAddrRouteIsSkipped(t *testing.T) { |
| } |
| // TestSyncExecUnrecognizedExecDeliveryValueIgnoredNotFatal covers additive |
| -// discipline (CLAUDE.md): a manifest a NEWER carlos wrote, carrying an |
| +// discipline (AGENTS.md): a manifest a NEWER carlos wrote, carrying an |
| // exec_delivery value this binary doesn't understand, must not brick this |
| // box's sync — the whole pass must still succeed, the unknown value must |
| // be treated as "not opted in" (not a fatal error, not silently treated |
diff --git a/internal/applog/record.go b/internal/applog/record.go| index 8e08f14c6..92ae49d69 100644 |
| --- a/internal/applog/record.go |
| +++ b/internal/applog/record.go |
| @@ -63,7 +63,7 @@ func levelRank(level string) int { |
| func ValidLevel(s string) bool { return levelRank(s) >= 0 } |
| // Record is one log line on the wire — one JSONL line inside a chunk. |
| -// Additive shape (CLAUDE.md rule): every field but TS and Msg is omitempty, |
| +// Additive shape (AGENTS.md rule): every field but TS and Msg is omitempty, |
| // unknown keys are ignored on read, and anything written before a shape |
| // change must still parse. TS is the box clock in UTC; cross-box order is |
| // best-effort by TS with (Box, Seq) as tiebreak — per-box order is the only |
diff --git a/internal/applog/record_test.go b/internal/applog/record_test.go| index ee89c54ae..98d176924 100644 |
| --- a/internal/applog/record_test.go |
| +++ b/internal/applog/record_test.go |
| @@ -11,7 +11,7 @@ import ( |
| // and msg is omitempty, and a record written by a future revision with keys |
| // this one has never heard of still parses. Live chunks exist in production |
| // buckets from the moment this ships — anything written before a change |
| -// must still parse (CLAUDE.md wire-shape rule). |
| +// must still parse (AGENTS.md wire-shape rule). |
| func TestRecordWireShapeIsAdditive(t *testing.T) { |
| r := Record{ |
| TS: time.Date(2026, 8, 10, 14, 3, 11, 482913000, time.UTC), |
diff --git a/internal/console/apiservices.go b/internal/console/apiservices.go| index c7a6127ae..2447b6053 100644 |
| --- a/internal/console/apiservices.go |
| +++ b/internal/console/apiservices.go |
| @@ -118,7 +118,7 @@ func (s *Server) apiOwnerAccount(w http.ResponseWriter, r *http.Request) (Princi |
| // actually be admitted to. A hand-written list in a help string would be |
| // right on the day it was written and wrong on the day a door moved. |
| // |
| -// This exists because of CLAUDE.md's no-infra rule read as a rule about |
| +// This exists because of AGENTS.md's no-infra rule read as a rule about |
| // comprehension rather than only about access: an operator must be able to |
| // see what they granted without reading a spec. |
| func (s *Server) doorsFor(role svcrole.Role) []string { |
diff --git a/internal/console/apiservices_test.go b/internal/console/apiservices_test.go| index 873d31fd8..f896bb8cc 100644 |
| --- a/internal/console/apiservices_test.go |
| +++ b/internal/console/apiservices_test.go |
| @@ -263,7 +263,7 @@ func TestMintRefusesADuplicateName(t *testing.T) { |
| } |
| // TestCreateAnswersWithTheDoorsItGranted — an operator must be able to see |
| -// what they gave away without reading a spec (CLAUDE.md's no-infra rule |
| +// what they gave away without reading a spec (AGENTS.md's no-infra rule |
| // applied to comprehension, not just access). The list is derived from the |
| // same map the gate reads, so it cannot drift from what the gate allows. |
| func TestCreateAnswersWithTheDoorsItGranted(t *testing.T) { |
diff --git a/internal/console/apistore.go b/internal/console/apistore.go| index 631d7a81a..d49849e92 100644 |
| --- a/internal/console/apistore.go |
| +++ b/internal/console/apistore.go |
| @@ -105,7 +105,7 @@ func storeEffectivePrefix(rec *objstore.Record) string { |
| // it is here because without it `status` cannot answer the question a waiting |
| // member actually has: is my declaration queued behind an operator, or does |
| // this deployment have no object storage at all and never will? The |
| -// no-infra-access rule (CLAUDE.md) says the command alone must complete the |
| +// no-infra-access rule (AGENTS.md) says the command alone must complete the |
| // job, and "declared, nothing happening" with no way to tell those apart |
| // fails it. Disclosure-wise it is the same class as secrets_pubkey and the |
| // apps domain: a deployment-wide fact about the platform, carrying nothing |
diff --git a/internal/console/economicspage_test.go b/internal/console/economicspage_test.go| index a45cf2e53..f2cd2a16a 100644 |
| --- a/internal/console/economicspage_test.go |
| +++ b/internal/console/economicspage_test.go |
| @@ -683,7 +683,7 @@ func TestEconomicsPagePooledMarginIsDash(t *testing.T) { |
| // (customer hardware, zero cost to the platform — PriceAccount's cost |
| // formula has no fleet term at all), and a number that looks like a |
| // modelling error gets an explanation somewhere on the page (dashboard-truth |
| -// rule, CLAUDE.md). The title= tooltip on the Margin header is invisible on |
| +// rule, AGENTS.md). The title= tooltip on the Margin header is invisible on |
| // touch/keyboard, so both notes ALSO need a visible one-line legend under |
| // each table — this asserts the tooltip AND the legend paragraph, on both |
| // pages, not just "the text appears somewhere" (which the tooltip alone |
diff --git a/internal/console/ladder_test.go b/internal/console/ladder_test.go| index 3d4ccc712..0425f65f8 100644 |
| --- a/internal/console/ladder_test.go |
| +++ b/internal/console/ladder_test.go |
| @@ -452,7 +452,7 @@ func TestLadderEnvironmentAndProdWiredVisibleToNonOwner(t *testing.T) { |
| // A canary promoted moments ago — exactly what every real promotion through |
| // this console produces, Promote always stamping "now" — must carry no |
| -// expired badge. This is the case CLAUDE.md's own console route is in on |
| +// expired badge. This is the case AGENTS.md's own console route is in on |
| // every deploy (canary/console, re-promoted each time): a false positive |
| // here would badge the console's own production traffic as neglected. |
| func TestLadderFreshCanaryCarriesNoExpiredBadge(t *testing.T) { |
diff --git a/internal/status/status_test.go b/internal/status/status_test.go| index 85790bde9..03ae43667 100644 |
| --- a/internal/status/status_test.go |
| +++ b/internal/status/status_test.go |
| @@ -250,7 +250,7 @@ func TestCollectBoxOmitsMetricsWhenAllUnknown(t *testing.T) { |
| // TestBoxBackCompatParse: yesterday's Box shape (no metrics key) must parse |
| // today, and a FUTURE box's unknown keys (inside or alongside metrics) must |
| -// not break today's parse either — additive wire shape, per CLAUDE.md's |
| +// not break today's parse either — additive wire shape, per AGENTS.md's |
| // "consumer = everything that PARSES the shape" rule. |
| func TestBoxBackCompatParse(t *testing.T) { |
| yesterday := []byte(`{"generated_at":"2026-08-03T10:00:00Z","label":"ada-stargazing", |
diff --git a/services_cli_test.go b/services_cli_test.go| index 3a51719c4..c03d213ce 100644 |
| --- a/services_cli_test.go |
| +++ b/services_cli_test.go |
| @@ -19,7 +19,7 @@ |
| // |
| // EVERY test here that shells out to the built carlos binary builds its env |
| // through carlosTestEnv (masks CARLOS_CREDENTIALS/HOME/CARLOS_CONSOLE) and |
| -// sets cmd.Dir to its own scratch dir — CLAUDE.md's rule, restated in this |
| +// sets cmd.Dir to its own scratch dir — AGENTS.md's rule, restated in this |
| // task's brief: a subprocess that inherited a real login has minted real |
| // accounts on the flagship before. |
| package main |
| @@ -748,7 +748,7 @@ func TestRunServicesRevokeMatchedFalseDoesNotPrintSuccess(t *testing.T) { |
| // log.Fatal/os.Exit refusal without killing the test binary. Every case |
| // here must exit nonzero without ever reaching the network: |
| // carlosTestEnv masks CARLOS_CREDENTIALS/HOME/CARLOS_CONSOLE and cmd.Dir is |
| -// a scratch dir (CLAUDE.md's rule — a subprocess that inherited a real login |
| +// a scratch dir (AGENTS.md's rule — a subprocess that inherited a real login |
| // has minted real accounts on the flagship before), so a case that DID try |
| // to dial out would fail on "not logged in", not silently succeed. |
| func TestServicesCLIUsageErrors(t *testing.T) { |
diff --git a/store.go b/store.go| index bdca699e5..c1d99e10b 100644 |
| --- a/store.go |
| +++ b/store.go |
| @@ -245,7 +245,7 @@ func runStoreRotate(args []string) { |
| // that fact is added too: a member cannot see CARLOS_STORE_BACKEND for |
| // themselves, and "declared, nothing happening" with no way to tell a queued |
| // request from a deployment that will never grant one fails the no-infra- |
| -// access rule (CLAUDE.md) just as surely as a missing CLI command would. |
| +// access rule (AGENTS.md) just as surely as a missing CLI command would. |
| func printStoreCreate(w io.Writer, res apiStoreReply) { |
| c := colorFor(w) |
| prefix := "" |
diff --git a/store_test.go b/store_test.go| index cf1f6af97..9aa983c76 100644 |
| --- a/store_test.go |
| +++ b/store_test.go |
| @@ -499,7 +499,7 @@ func TestPrintStoreStatusDeclaredNotProvisioned(t *testing.T) { |
| // ---- runStore dispatch / refusals (real binary) ----------------------------- |
| // TestStoreVerbDispatchAndRefusals drives the real binary through |
| -// carlosTestEnv with a scratch cmd.Dir — the repo rule (CLAUDE.md) — so a |
| +// carlosTestEnv with a scratch cmd.Dir — the repo rule (AGENTS.md) — so a |
| // logged-in laptop's real credential and console can never be what this test |
| // exercises: unknown sub-verb, missing -app on each sub-verb, and direct |
| // bucket mode refused with store's own sentence. |