carlos / contra Public

START HERE — how to pick up work on CONTRA Idea open

🤖 Read this before claiming anything.

⚠️ Blocker: main is empty

Every design document lives on the unmerged branch intent. A clone of main gets you nothing. Until Paul merges it:

git fetch origin && git checkout intent

and branch from there. Nothing below can be done properly without AGENTS.md and DESIGN.md in front of you — they are the binding documents, and this discussion is only a map of them.

Claiming work — read this, there is no assign verb

amadan has no way to assign or claim a discussion (filed upstream as amadan/amadan discussion 36). Until it exists, the convention is a reply, and it is the only thing stopping two overnight agents building the same thing twice:

  1. amadan discuss show carlos/contra <id> and read the whole thread.
  2. If an unresolved claim reply exists and is less than 24h old, pick something else.
  3. Otherwise reply: 🤖 claiming — <agent> — <branch> — <UTC timestamp>.
  4. Reply again when you stop, finished or not, saying where you got to. A silent abandonment holds the item hostage for the next agent.

Claim one item at a time.

The two tracks

Work is split so the tracks do not collide:

  • Remote agents → the web piece. Discussions 7, 8, 9, 10, 11, 12, 13, 14 (web/*).
  • Local, on Paul's machine → the native piece. Discussions 15, 16, 17 (native/*).
  • Discussion 6 (shared/1, golden vectors) belongs to whoever starts first, and blocks both tracks. Do it before anything else.

Native work is deliberately limited to the parts that do not depend on a moving contract. AGENTS.md decision 6 puts the Mac app after the web console, and that still holds — the crypto core, device keys and the distribution decision are contract-independent, the UI is not.

Order

6 (vectors) ─┬─► 7 ─► 8 ─► 9 ─┬─► 10 ─► 11 │ ├─► 12 ─┬─► 13 │ └─► 14 └─► 15 ─► 16 17 stands alone and wants a human answer

Non-negotiables

Full text in AGENTS.md; these are the ones most easily broken by an unattended session:

  • Never echo a secret, a claim link, or a recovery code into a session. This repo publishes its prompt ledger — what gets typed gets published. Test fixtures are generated, never real exports.
  • Worktree per session, branch before the first edit, push early, keep the branch description true. amadan agent skill has the house rules; they are installed in this repo at .claude/skills/amadan/.
  • Post as your agent account, never as Paul. AMADAN_HOME pointing at your own credentials.
  • Never commit to main. Squash-merge needs all three: done, a human approved, CI green.
  • Review on a deployed canary, never localhost, and report the clickable URL in every reply while iterating.
  • The merciless greps gate every storage change. A new storage surface lands with its canary or it does not land.
  • Additive-only migrations. Never delete data to update.

If you get stuck

Reply on the item's discussion rather than guessing — an unattended wrong turn is more expensive than a wait. Genuine design questions get their own discussion. Do not relitigate a settled decision in AGENTS.md in a drive-by; if one looks wrong, say so with the new fact, dated.

🤖 Overnight run, 2026-09-01 → 02. claude~paul@keymail.dev, session ad0bdb88, remote agent, web track only. Posting here because #18 is the map and the map has moved.

Done, in order, each claimed and reported on its own thread

| Item | Branch | State | |---|---|---| | #6 shared/1 golden vectors | shared/golden-vectors | done | | #7 web/1 bootstrap | web/bootstrap | done, deployed | | #8 web/2 sealed schema + greps | web/sealed-schema | done, deployed | | #9 web/3 signup + sign-in | web/auth | done, deployed | | #12 web/6 vault and items | web/vault | done, deployed |

Canary: https://contra.bdj.oncarlos.com — signup, sign-in, unlock and full item CRUD all work there, verified with the real browser modules against the real deployment, not a mock.

The branches stack in that order, each on the last. make ci is green on the tip: vet, gofmt, the Go suite, the node crypto suite, and rastrillo migration check. A real-engine walk (go test -tags browser ./...) drives signup → unlock → add → reload → edit → delete.

Three things need a human before anything else lands

  1. No CI runner is attached, so amadan ci status cannot be green on anything. .amadan/ci exists and passes, but arranging a runner is refused to an agent — "an agent cannot manage agents". AGENTS.md's bootstrap exception is spent the moment .amadan/ci exists, so from 361cff8 the three-condition merge rule binds and its third condition is currently unreachable. Details on #7.
  2. #19 is load-bearing for all five branches. It records the wire formats DESIGN.md leaves open — HKDF mode, the AAD layout, Crockford packing, the EC rejection loop, the signed-challenge message, and client-minted ids. If one of those is wrong, everything above it rebases. It is cheap to change now and expensive after a second client exists.
  3. #20: carlos deploy returns success, and X-Carlos-Version goes green, up to ~95 seconds before the process has restarted onto the new build. #7 tells the next agent to verify with that header; that check can pass against the previous build. /api/version is the honest one.

Not taken, and why

#10 (Emergency Kit PDFs) is unblocked but I left it. It needs a QR encoder and there is no QR decoder on this machine to verify one against — every other thing I wrote tonight had an independent oracle, and a recovery QR that is subtly wrong is a lost vault. The decision it needs is written up on that thread.

#11, #13, #14 are untouched. #11 (web/5, the fresh-device ladder) is the most valuable next one: the vault console currently tells a device with no cached Account Key that the ladder is not built, which is honest but is a dead end, and DESIGN.md is explicit that no screen may have one.

Worth knowing if you pick this up

  • The crypto is byte-identical in Go and JS, pinned by vectors/contra-v1.json and enforced by make test-js as a hard gate. Argon2id is written out rather than vendored (~290 lines, RFC 9106), agrees with OpenSSL on nine parameter sets, and runs 64 MiB at t=3 in about 1.7s.
  • The greps are watched failing on every CI run, not just once by me. TestGrepsCatchAPlaintextColumn adds the tempting column and asserts the grep finds it; if it ever goes green, every other result in that file is worthless.
  • One real bug was found the only way it could have been: the server was minting wrap ids the client had already bound into its AAD, so every wrap it stored was one nobody could open. Go tests built the request struct by hand and agreed with themselves. It surfaced the first time the real browser module talked to the real server — which is an argument for driving the real client against the canary before calling any item done.
  • A WebKit gap: #12 asks for a WebKit console check and the rig here is Chromium, because Playwright is not installed and adding it is a dependency decision. Given DESIGN.md picks 64 MiB for iOS Safari's WASM heap, this is worth closing before anyone trusts the client on iOS.

🤖 Addendum to my run summary above — two more things landed after it.

#10 web/4, the Emergency Kit — I had declined it for want of a QR decoder to verify an encoder against. Paul installed one, so the objection went away and the item is done: branch web/kit at cda6877, deployed. A QR encoder and a small PDF writer, both written rather than vendored, both checked against two implementations that are not ours — ZXing decodes every symbol, python-qrcode matches every matrix bit-for-bit. 224 symbols pass.

The front page was lying, and a screenshot found it rather than a test. It still said "there is no signup, no vault and no stored data behind it" on a build that had all three — true when written at web/1, never re-read as the app grew. For most products that is a stale sentence; for this one it is the product failing at the only thing it claims. Fixed, with two tests that fail if the page contradicts the routes or if any page offers no way onward. The navigation rail was also empty, so a visitor could not reach signup at all.

The state now

| Item | Branch | | |---|---|---| | #6 golden vectors | shared/golden-vectors | done | | #7 bootstrap | web/bootstrap | done, deployed | | #8 sealed schema + greps | web/sealed-schema | done, deployed | | #9 signup + sign-in | web/auth | done, deployed | | #12 vault and items | web/vault | done, deployed | | #10 Emergency Kit | web/kit | done, deployed |

Six of the nine web items, stacked in that order. #11 (web/5, the fresh-device ladder) is the one I would take next and the one that matters most now: the vault tells a browser with no cached Account Key that the ladder is not built, which is honest and is still a dead end, and DESIGN.md says no screen may have one. #13 (1PUX import) and #14 (sharing) are untouched.

Still needs a human

  1. A CI runner. Unchanged, and now blocking six branches. The client-side guard refuses even amadan ci request, which the shared-CI design says only needs namespace ownership — so this is not something an agent can do at all today, whatever its grants.
  2. #19, now eleven pinned wire formats, all load-bearing.
  3. Reprinting the kit. DESIGN.md is explicit that the Account Key can be reprinted exactly while the recovery code can only be regenerated, and that the flow must be prominent. It does not exist: today the kit is offered once, at signup.

One thing worth taking from tonight

Two of the three real defects were found by looking at the output, not by running the suite. The stale front page, and the Account Key card printing its code straight through the cut line — every test passed on both. The third, the wrap ids, was found by running the real client against the real server.

None of the three would have been caught by more unit tests. Worth remembering when deciding what a "done" item owes.