🤖 Deployed. Canary: https://contra.bdj.oncarlos.com
Branch web/bootstrap at 361cff8, pushed. make ci — vet, gofmt, go test ./..., rastrillo migration check — green.
Verified the way the item asks
Not a 200. The edge stamps X-Carlos-Version from the route row, so it can report a build the process has not actually restarted onto; the process's own answer is GET /api/version. Both say 361cff8, which is the commit the binary was stamped from:
x-carlos-version: 361cff8 GET /api/version -> 361cff8
Instances were not enabled for contra, so that came first, then the instance on contra.bdj.oncarlos.com (channel edge — the app's only channel; a release ladder is a decision worth making awake, not overnight), then CONTRA_ORIGIN set to the canonical origin so the CSRF check has a real yardstick rather than the localhost default.
⚠️ One half of "done when" I cannot reach
amadan ci status says "No CI status recorded for this commit." The repo requires nothing of its runners, but no runner has claimed a job, and arranging one is refused to me: amadan ci offer list and amadan ci requests both answer "an agent cannot manage agents."
So .amadan/ci exists and passes — but a human has to attach a runner before amadan ci status can be green on anything. Worth doing before the next merge, because AGENTS.md's bootstrap exception is spent the moment .amadan/ci exists: from this commit the three-condition rule binds, and right now the third condition is unreachable for a reason that is not the code's fault. Flagging rather than guessing.
Scaffolded, not hand-rolled
rastrillo new --shell=console. rastrillo/db already does what item 3 asks — WAL, busy_timeout before journal_mode, foreign keys, a single-connection writer pool — so none of it is hand-rolled. But "the framework does it" is a claim, and TestDatabasePragmas turns it into a fact; TestWriterPoolIsSingleConnection and TestMigrationsApplyOnce do the same for the rest.
Each .amadan/ci.d step execs a make target rather than carrying its own commands, following amadan's own rule: make ci locally is byte-for-byte what a runner will do.
Two departures from the scaffold, both on purpose
The example notes(title, body) model is deleted, not renamed. In a host-blind password manager a plaintext table is not a starting point to edit — the whole SQLite file replicates to the bucket, so it is precisely the shape the one rule forbids. The app therefore owns no tables at all yet. TestAppOwnsNoTablesYet fails the moment one appears and says why: it is a placeholder holding the door until web/2's merciless greps exist, to be replaced by them rather than deleted.
sessions.Schema is in BootSchema from migration 0001, though sign-in is web/3's. Migrations are additive-only and the first deploy of a version carrying them has to be schema-neutral; the session tables cost nothing in 0001 and are the case the framework refuses later.
Also: internal/crypto → internal/contra/crypto, because rastrillo migration check requires exactly one app package under internal/ and keeping that check in CI is worth more than the old path.
The copy is borrowed, deliberately
The landing page's prose is lifted verbatim from README.md. Copy is a reviewed artifact here and the canary needed a page before that review could happen, so it says only what you have already written. Real console copy belongs with the screens it serves — do not read the current page as a proposal.
One thing worth knowing, unrelated to this item
rastrillo vectors exists: a built-in Go↔JS parity vector generator. It generates the vectors from the Go implementation and checks JS against them, which is the arrangement discussion 6 rules out — it proves the two agree, not that either matches the spec. Ours goes the other way (OpenSSL generates, Go is tested). Not a problem, but somebody will reach for that command later expecting it to be the same thing, and it is not.
Next: web/2 (#8), the sealed schema and the greps. Claiming there.