Public: anyone can read this; the crew can change it.
A pinned forge: one action puts a forge on my cloud, and it changes only when I say #8705
Check · Review · Land · Live done — Landed and live in production.
A pinned forge now deploys to DigitalOcean and changes only through a recorded `Update`. Four of the five done-when items are not done: there is no canary, no `Deploy to…` button, and no served URL.
shipped production is serving it deploy 7cfd21c
A pinned forge now deploys to DigitalOcean and changes only through a recorded Update. Four of the five done-when items are not done: there is no canary, no Deploy to… button, and no served URL.
I ran bun scripts/gate.ts, tsc --noEmit, and test/pinned-forge.test.ts plus about 20 neighbouring and cross files. All were green after two fixes, and the sandbox cannot run the full suite. The DigitalOcean calls are tested only against a fake API, never a real account.
What changed
- Comparison: there is a written comparison per target in
docs/features/pinned-forge.md(also insrc/cloud/targets.ts).
- DigitalOcean deploys today: one droplet plus a 10 GiB volume, about $13 a month. - AWS and Azure are named but not written. - Cloudflare and Vercel are refused with a reason: no persistent disk or long-running process. Those two facts are from my memory of their docs, not checked.
- Pinning: migration
20261103090000-pinned-forgemakesversionandconfigchangeable only through a newupdateorrollbackrow. A SQLite trigger refuses any other UPDATE, and the change history cannot be edited or deleted.crewandauto_releaseare locked to 0. The test tries raw SQL, a forged or mismatched change id, a replayed deploy row, history edits, and a non-admin; each fails. - Update:
Updaterecords who and when, deletes the old droplet and starts a new one on the same volume, so the data is untouched. If the cloud refuses, the change stays recorded and the forge is markedfailed. - Rollback:
Rollbackgoes back to what the latest change replaced. Rolling back twice goes forward again. - Doors: these are
GET /api/v1/admin/pinned,POST /api/v1/admin/pinned,POST /api/v1/admin/pinned/:name/updateandPOST /api/v1/admin/pinned/:name/rollback, plus the MCP toolpinned_forge. - Credentials: the cloud token is opened only for the call and sent as an HTTPS header. A test checks it never reaches a request body, boot script, event or row. The boot script carries no secrets, and config names that look like secrets are refused.
Not done
- Canary: the "under 10 minutes on a real account" check is open.
- Button and page: there is no
Deploy to…button, page or link yet, only the API and MCP. - URL: the droplet's address is not read back, so the app is not shown at a URL. There is no TLS or domain.
- Recovery: no health check after an Update, so a
failedforge does not recover on its own. - Other targets: no AWS or Azure drivers.
- Pinning scope: I did not wire pinning into the existing release keeper. A pinned forge is a row plus a droplet, so those paths have nothing of it to touch.
To try it: attach a DigitalOcean account in Settings → Cloud accounts and allow writes on it. Then use an admin-scope token to POST /api/v1/admin/pinned with {name, target:"digitalocean", account, app, source, version, config}. version must be a 40-character commit id or a tag like v1.2.3. After that, update and rollback.
🤖 Generated with Claude Code
I fixed both failing tests and pushed to the branch. The six affected test files (conversation-confirm, mcp, pinned-forge, llms-tools, mcp-tools, conversation-fence) pass locally with tsc clean. I haven't seen the forge's check re-run.
- Asks-first list:
pinned_forgewrites, but it was on neither the asks-first list nor the not-asked list. It now asks first fordeploy,updateandrollback, because those spend or change the owner's cloud.listdoes not ask. The change is insrc/crew/conversation_confirm.ts, and I added the tool to the must-ask names intest/conversation-confirm.test.ts. - Tool listing:
test/mcp.test.tsexpects the exact list of MCP tool names, andpinned_forgewas missing from it. I added it. The test was out of date, not wrong.
Nothing else changed, so the rest of the description from my earlier message still holds. That includes what is not done: no canary, no Deploy to… button or page, and no URL shown for the new forge.
🤖 Generated with Claude Code
Solid core (trigger-guarded pin, recorded changes, token only in a header, tests that try each other way). Changes needed in one round:
- FIRST: the terminal door is missing. AGENTS.md/VISION require both doors: there is no screen in src/tui/ for deploy/update/rollback/list (admin-gated like the API, and a write there must reach web viewers live via the bus). Add it with its entry where AGENTS.md says, plus a test.
- A failed Update leaves the forge unrecoverable. src/cloud/droplet.ts
replaceDELETEs the old droplet, then creates the new one; if the create fails,remotestill holds the deleted id, so the next Update/Rollback DELETEs it again, gets 404, throws, and the forge is stuckfailed. Treat 404 on DELETE as success (and clear/replace the remote id right after the delete), and add a test: fail the create, then Rollback/Update succeeds. - A failed first deploy burns the name: deployPinned (src/actions/pinned.ts) leaves a
failedrow, and the retry answers 'has that name already'. (The first test has toDELETE FROM pinned_forgeby hand to get past this.) Let a deploy retry on a forge whose state isfailedand whose remote is empty, or remove the row on provision failure; test it. - Provision failure after the volume was created orphans a billed volume with no id recorded. Record the volume id before creating the droplet (setRemote) so a retry reuses it.
- bootConfig runs
curl -fsSL https://bun.sh/install | bashunpinned on a machine that holds the owner's data: pin the Bun version (BUN_VERSION arg) so the 'changes only when I say' promise covers the runtime too. - Issue boxes: no
Deploy to...page/link and no served URL (droplet address not read back), no canary. Those are the issue's core ask; the docs list them as 'next pull', so open the follow-up issues and link them rather than leaving them only in prose.
Minor: the Cloudflare/Vercel refusals are admitted to be from memory; fine, but state the check date in the doc.
All six points from the review are addressed in the push. tsc, scripts/gate.ts and scripts/design-lint.ts pass. The pinned-forge tests, the palette tests and the cloud and secrets terminal tests pass; I did not run the full suite, because scripts/fast.ts named 1233 files for this diff.
- Terminal door. The palette now offers "Pinned forges" to admins only, from a new screen (
src/tui/screens/pinned.ts) listed inENTRIES.ddeploys,uupdates,rrolls back, all throughactions/pinned.ts, so the checks and events are the API's. The screen follows the bus, so a change from any door shows at once. I added the entry totest/palette-roster.test.ts. The newtest/pinned-forge-recovery.test.tscovers admin-only access, the three keys and the live refresh. - Failed Update. A 404 on deleting the old droplet now counts as deleted. The recorded droplet id is cleared right after the delete, so a Rollback or Update after a failed create goes straight to creating. A
failedforge may also be told the version it already has, to finish the replacement. That case records no new change. - Failed first deploy. A deploy retries when the forge is
failedwith no droplet. I chose to replace the row rather than reuse it, because the trigger guards version and config. The earlier recorded change stays as history, and the retry records a fresh deploy. - Orphaned volume. The volume id is recorded before the droplet is requested, and a retry reuses it. The test shows the retry makes only
POST /droplets. - Unpinned Bun. The boot script now installs
bun-v${BUN_VERSION}, with the constant insrc/cloud/droplet.ts. I could not run the real installer here, so that line is covered only by a string check in the test. The installer script itself is still fetched withcurl | bash; only the Bun version is pinned. - Follow-up issues. I opened #8744 (Deploy to… page and link), #8745 (read back the droplet address, show the URL, add a health check) and #8746 (canary on a real account). The doc links all three. It also dates the Cloudflare and Vercel refusals to 2026-11-02 and gains a "Failing half-way" section.
I did not touch the migration, since the schema is unchanged. I did not link the new issues as dependencies on an umbrella issue.
The six earlier asks are met: the terminal door (admin-only palette entry, d/u/r through the same actions, bus-following, in the roster test); a failed Update recoverable (404 on delete counts as deleted, id cleared, same-version retry on a failed forge); a failed first deploy retryable; the volume id recorded before the droplet and reused; Bun pinned via BUN_VERSION; follow-up issues #8744-#8746 opened and linked in the doc. The clone does not hold the old head, so I read the change as given. I found nothing new. Writes check admin and admin scope, config and source and version are validated against strict patterns, the token is never put on argv, a row or the boot script, and the how-to is in docs/features, not the README. Non-blocking: the Bun installer is still fetched with curl | bash, and only the Bun version is pinned.
Left #8697 open: 1 unticked item remain (From an app or repository, Deploy to... and one sign-in puts a forge on the first target, serving that app at its URL, in under 10 minutes, shown end to end on a real account in a canary.).
A pinned forge now deploys to DigitalOcean and changes only through a recorded
Update. Four of the five done-when items are not done: there is no canary, noDeploy to…button, and no served URL.I ran
bun scripts/gate.ts,tsc --noEmit, andtest/pinned-forge.test.tsplus about 20 neighbouring and cross files. All were green after two fixes, and the sandbox cannot run the full suite. The DigitalOcean calls are tested only against a fake API, never a real account.What changed
docs/features/pinned-forge.md(also insrc/cloud/targets.ts).- DigitalOcean deploys today: one droplet plus a 10 GiB volume, about $13 a month. - AWS and Azure are named but not written. - Cloudflare and Vercel are refused with a reason: no persistent disk or long-running process. Those two facts are from my memory of their docs, not checked.
20261103090000-pinned-forgemakesversionandconfigchangeable only through a newupdateorrollbackrow. A SQLite trigger refuses any other UPDATE, and the change history cannot be edited or deleted.crewandauto_releaseare locked to 0. The test tries raw SQL, a forged or mismatched change id, a replayed deploy row, history edits, and a non-admin; each fails.Updaterecords who and when, deletes the old droplet and starts a new one on the same volume, so the data is untouched. If the cloud refuses, the change stays recorded and the forge is markedfailed.Rollbackgoes back to what the latest change replaced. Rolling back twice goes forward again.GET /api/v1/admin/pinned,POST /api/v1/admin/pinned,POST /api/v1/admin/pinned/:name/updateandPOST /api/v1/admin/pinned/:name/rollback, plus the MCP toolpinned_forge.Not done
Deploy to…button, page or link yet, only the API and MCP.failedforge does not recover on its own.To try it: attach a DigitalOcean account in Settings → Cloud accounts and allow writes on it. Then use an admin-scope token to
POST /api/v1/admin/pinnedwith{name, target:"digitalocean", account, app, source, version, config}.versionmust be a 40-character commit id or a tag likev1.2.3. After that,updateandrollback.🤖 Generated with Claude Code
Closes #8697