carlos / contra Public

web/1 — Bootstrap: rastrillo instance, go.mod, CI, first canary Feature Request open

The repo has no code and — importantly — no CI, which means the merge gate in AGENTS.md ("done, approved, and CI green") can never be satisfied by anybody. Nothing else can land cleanly until this does.

Deliverable:

  1. go.mod, a thin main.go dispatch, CGO_ENABLED=0, on rastrillo (github.com/carlosframework/rastrillo). Read rastrillo's own SKILL.md first — it postdates most models' training data, and its packages exist to be used rather than hand-rolled.
  2. .amadan/ci plus .amadan/ci.d/ steps, following the pattern in amadan/amadan: vet, gofmt-clean, go test ./.... It must pass on an almost-empty repo so it is green from the first commit rather than arriving red.
  3. SQLite via modernc.org/sqlite, WAL, busy_timeout set before journal_mode, SetMaxOpenConns(1), additive-only migrations.
  4. A deployed canary on Carloku, and the clickable URL reported.

Done when: amadan ci status is green on the branch, and the canary serves something over a real URL.

Traps: the app is a binary on a unix socket (--socket/--db) — there is no $PORT. Do not hand-roll a router, TLS, replication or restart machinery; that is the platform's job. Verify the deploy with X-Carlos-Version on the canonical host, never with a 200.

Branch: web/bootstrap.

🤖 claiming — claude~paul@keymail.dev (remote agent, session ad0bdb88) — branch web/bootstrap — 2026-09-01T22:50:00Z

Continuing the web track. shared/1 (#6) landed on shared/golden-vectors; this branch takes it as its base so the crypto package comes with it rather than arriving twice. Will reply here when I stop, finished or not.

🤖 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.