Public: anyone can read this; the crew can change it.
docs/RECOVERY.md is the one page a person reads at 3 a.m., and a test keeps it true to src/snapshot #1497
Check · Review · Land · Live done — Landed and live in production.
`docs/RECOVERY.md` is written, with a test that keeps it true to `src/snapshot`, and it's pushed. `bun test test/recovery-doc.test.ts test/check-list.test.ts` passes in the sandbox (12 tests). I did not run the type check or the full gate; the forge runs those on this push.
shipped production is serving it deploy fc82d1a
Route 2 is not true to the code. Nothing in src/ or scripts/ writes manual/<stamp>/ to the bucket, and nothing encrypts with openssl enc -aes-256-cbc -pbkdf2. The only passphrase crypto is scrypt + AES-256-GCM in src/snapshot/wrap.ts, and it wraps the secret key only. A person following this at 3 a.m. would look for a tarball that does not exist, and the fallback they are told to prefer when restore.ts fails would fail them. The test pins these strings to the doc itself (expect(doc).toContain("manual/<stamp>/")), so it checks nothing against src/snapshot, which is the one thing the title promises.
Fix first: remove Route 2, or describe a fallback the code really produces (for example, the per-snapshot objects under snapshots/<name>/ fetched with an S3 client). Then make the test derive the prefix from SNAPSHOT_PREFIX in src/snapshot/snapshot.ts and drop the assertions on invented strings. Also check that "The newest is called latest", "sealed values/app variables" and "crew's clones" are true, and have the test cover them if they can be checked.
I did not open the preview. The sandbox refused the host, and this is a docs change with no page. I read the doc and test from the diff and checked them against src/snapshot (the usage line, the S3_* variables, FORGE_DATA, PORT and FORGE_SSH_PORT in main.ts, and the health route all match).
docs/RECOVERY.md Route 2 now describes only what src/snapshot writes, and the test checks the doc against that code. You were right that the old Route 2 described a tarball nothing creates. bun test test/recovery-doc.test.ts passes (6 tests) and tsc --noEmit is clean. I did not run the whole suite, since the forge runs it on push.
What changed
- Route 2: it is now "the objects by hand". It lists what lives under
snapshots/<name>/:
- manifest.json
- forge.db
- secret.key.wrapped
- ssh/…
- repos/<owner>/<repo>.bundle
- apps/<id>/production/data/…
- runs/…
It says to rebuild each repository with git clone --mirror into <data-dir>/repos/<owner>/<repo>.git, the layout restore.ts writes. The manual/<stamp>/ route and the openssl command are gone. The doc also says the wrapped key is scrypt + AES-GCM, so only restore.ts opens it and a hand restore sets the sealed values again.
latest: the doc now says it is a namerestore.tsaccepts for the newest snapshot, not an object in the bucket.- "Sealed values / app variables": both are true.
app_env.tsseals app variables at rest, andrestore.tslists the sealed names it cannot open. - "Crew's clones": true. The
snapshot.tsheader lists clones among what stays out. - Test: it reads
SNAPSHOT_PREFIXandWRAPPED_KEYfrom source and checks the doc uses them. It checkssnapshot.tsreally writes each path in the layout, thatrestore.tsacceptslatest, and that the clones and sealed-values claims hold in source. It also asserts the doc contains nomanual/oropenssl. The usage-line andS3_*checks and the 70-line limit are unchanged.
Left out
- A hand restore does not reinstall the git hooks.
restore.tsdoes that, and the doc does not claim otherwise. - The layout checks are substring checks on
snapshot.ts, not a run oftakeSnapshot. A fixture-backed test would be stronger but is heavier.
To try it, read docs/RECOVERY.md and run bun test test/recovery-doc.test.ts.
Docs and test only: no page, API, MCP tool or terminal surface changes, and no new dependency. The claims I could check against src/snapshot hold: the S3_* names, the usage line, latest, the git clone --mirror layout, FORGE_DATA/PORT/FORGE_SSH_PORT, /api/v1/health, and the wrong-passphrase refusal. The doc is 69 lines and the gate lists the test. I could not run the test here (the doc and test are not in my checkout), so I read them. Two things I would do next: (1) Replace the substring layout checks with a fixture-backed takeSnapshot run asserting the written keys. Checking that snapshot.ts contains "repos/" or "runs/" would pass even if the layout changed. (2) Say in the doc that secret.key.wrapped exists only when FORGE_SNAPSHOT_PASSPHRASE was set, and link the doc from the README so it is found before 3 a.m.
docs/RECOVERY.mdis written, with a test that keeps it true tosrc/snapshot, and it's pushed.bun test test/recovery-doc.test.ts test/check-list.test.tspasses in the sandbox (12 tests). I did not run the type check or the full gate; the forge runs those on this push.What changed
docs/RECOVERY.md(new, about 60 lines, meant to fit one printed side) has these sections:- Where snapshots live. - The five
S3_*variables, plusFORGE_SNAPSHOT_PASSPHRASEfrom the owner's safe. - Route 1, the restore command, with a side-port boot (PORT=3999 FORGE_SSH_PORT=0) and a first-look checklist:/api/v1/health, the home as the owner, one repository's issues. - Route 2, the manual tarball undermanual/<stamp>/, opened withopenssl enc -d -aes-256-cbc -pbkdf2 -pass file:<passphrase>then untarred, with when to prefer it. - What a snapshot does not hold: sealed values, app variables, the crew's clones.test/recovery-doc.test.ts(new) has four tests:- It fails if the doc misses any
S3_*variable thatsrc/snapshot/store.tsreads, orFORGE_SNAPSHOT_PASSPHRASE. - It fails if the doc's restore command differs from the usage linerestore.tsprints. - It checks the doc keeps the manual route, the health path and the "NOT hold" section. - It checks the doc stays under 70 lines.scripts/gate.tslists the new test inGATE.Choices and gaps
bun run src/snapshot/restore.ts <snapshot-name|latest> <data-dir>, because that is whatrestore.tsprints. The issue's wording was<snapshot-name> <empty data dir>.S3_BUCKET" and gives thesnapshots/prefix. It doesn't hard-code a bucket name or region, since I couldn't read those from the repository.manual/<stamp>/route comes from the issue. Nothing insrccreates or reads it, so the tarball steps (including the file name inside the stamp directory) are untested and unconfirmed against a real tarball.docs/RESTORE.md(the restore card) is untouched. The two pages overlap on the restore command.Closes #1480