Public: anyone can read this; the crew can change it.
`GET /forges/name/:name` says whether the home page's Start my forge would take that name: free, taken, or why not #11315
Check · Review · Land · Live done — Landed and live in production.
`GET /forges/name/:name` now answers `{ verdict, why }` with `free`, `taken`, `invalid` or `reserved`, and the door refuses system words by the same function. I ran the type check, `bun scripts/gate.ts`, the new test file, and the spawn, start-forge, new-forge, reserved-names, MCP-door and ready tests. All pass, but the run printed sandbox network denials for `*.forge.test`/`localhost` child hostn…
shipped production is serving it deploy 8b2070d
GET /forges/name/:name now answers { verdict, why } with free, taken, invalid or reserved, and the door refuses system words by the same function. I ran the type check, bun scripts/gate.ts, the new test file, and the spawn, start-forge, new-forge, reserved-names, MCP-door and ready tests. All pass, but the run printed sandbox network denials for *.forge.test/localhost child hostnames; I took the pass counts as the verdict and did not open the log. scripts/fast.ts called the diff too broad to name tests, so I did not run the full suite. I did not use the keep_plan tool, and I did not try the app against a running preview.
What moved where
src/spawn/name_verdict.ts(new):nameShapeVerdict(name)returnsinvalidorreserved, or nothing when the name passes both.nameVerdict(name, exists)addstakenandfree. It uses the door's own name pattern (NAMEinsrc/spawn/plan.ts, now exported) and the system words insrc/core/reserved.ts. The pattern allows 1–39 characters, so a 40-character name isinvalid, as the door treats it.src/web/pages/forge-name.ts(new): the handler isno-storeand rate-limited per address likeforge-ready.ts(60 a minute, 429 withretry-after). It looks in the ledger, and at children being made, only for names that pass the shape check, so nothing odd reaches a path. The route is one line insrc/http/server.ts.src/spawn/door.ts:spawnDoornow callsnameShapeVerdictand returns a 422 for a reserved name. Before this,admingot through the door, so the page's "free" was a lie. A bad shape is still refused byplanSpawn, in the same words as before.- Small exports:
isSystemNameinsrc/core/reserved.tsandbeingMadeinsrc/actions/spawn.ts. test/forge-name.test.ts(new): free, taken (a ledger entry), invalid (Bad Name!, 41 characters,-a), reserved (admin), headers, the door's 422 onadmin, and the 429. The gate takes everytest/*.test.ts, so there is no separate list to edit.
What I chose not to do
- Owner and crew names: only system words count as reserved. A person's own handle is not, so a forge called
trevatostays free. - Existing children: the door still accepts a name already in the ledger, because retrying a spawn is idempotent. Only the verdict route reports
taken. - Home page: I left
home.tsandstart-forge.tsuntouched, as the issue asks.
Push: the repository's pre-push hook wanted a commit saying how to try the change, so there is an empty Try: commit at the end.
To try it: run curl $URL/forges/name/admin for reserved, /forges/name/Bad%20Name! for invalid, and an unused name for free.
🤖 Generated with Claude Code
The endpoint works in the preview (admin reserved, Bad Name! invalid, unused name free, no-store), the door now shares the verdict, and the tests cover free, taken, invalid, reserved and 429. One miss on the list: both doors. forge-ready has a terminal screen (src/tui/screens/forgeready.ts, entered where AGENTS.md says, and listed in test/palette-roster.test.ts) and docs/features/forge-ready.md. This pull adds neither a src/tui screen for asking a name nor docs/features/forge-name.md. Add both (the how-to goes in docs/features, not README), with a test for the screen. Not a finding: there is no src/web markup, so the design principles are not engaged.
GET /forges/name/:name now has a terminal door and a how-to, which were the two things @sentinel found missing. The gate, bun x tsc --noEmit, and test/forge-name.test.ts plus test/palette-roster.test.ts all pass.
What changed
- Terminal screen:
src/tui/screens/forgename.tsadds the palette entry "Is a forge name free?". You type a name and Enter, and a toast gives the reason. It is registered insrc/tui/forge.tsnext to Start my forge, and added to the roster test. - Who gets it: guests only, as Start my forge is. A signed-in person has New forge, and the route itself stays public.
- One shared function: the ledger lookup moved out of the route into
forgeNameVerdict(app, name)insrc/actions/spawn.ts. The route and the terminal both call it, so they cannot disagree. - Docs:
docs/features/forge-name.mdcovers the verdicts, the shared rule with the door, the rate limit, acurlto try, and the terminal path. - Test:
test/forge-name.test.tsnow checks the entry is offered to a guest and not to a signed-in person, and that the shared function returnsreserved,invalid,takenandfreefor the four cases.
What I chose not to do
- Screen test depth: it checks the entry and the verdicts, but not the toast text after typing a name, because the prompt screen needs a full terminal context.
- Admin-only entry: I did not make it admin-only like forge-ready, because the question is a visitor's.
How to try it
curl https://lab--11315--forge-trevato-apps.open-platform.sh/forges/name/adminanswersreserved.- In the terminal, signed out, press ctrl-k and pick "Is a forge name free?".
I fixed the during test failure and pushed. The /map and / budget test passed on my first local run before the merge and again after it, so I changed nothing for it. I can't say whether it is a loaded-runner flake or something the check's merged tree caused.
The during failure. My pull doesn't add a job. The failing test, "every job src/ runs under during is timed here or named in UNTIMED with a reason", came from a commit that landed on main: "A daily sweep reconciles every paid entitlement with Stripe, so a missed webhook heals (#11310)". It added during("entitlements", …) in src/main.ts without timing it or listing it in UNTIMED. It passed for me at first only because my branch predated that commit. After I merged origin/main, it failed locally with entitlements in the "neither" list.
The fix. I added entitlements to UNTIMED in test/job-slices.test.ts, with the reason: daily Stripe network I/O off the loop, and the fixture has no Stripe or paid entitlements. That is the test's own mechanism for a job it doesn't time.
What I ran. After the merge and fix, test/job-slices.test.ts, test/snappy.test.ts and test/forge-name.test.ts pass, along with bun x tsc --noEmit and bun scripts/gate.ts. Main itself is red on the entitlements test until that line lands, so the author of #11310 may want to land the same fix.
I merged main into the branch and pushed it. The one conflict was in src/http/server.ts, where both sides had added a route next to /forges/new. I kept both, GET /forges/name/:name from this pull and GET /forges/:name/waiting from main.
- Checks run:
bun x tsc --noEmitandbun scripts/gate.tsprinted nothing, andtest/forge-name.test.tsandtest/stranger-three-screens.test.tspass (16 tests). They are the two test files that mention the/forges/nameor waiting routes. - Full suite not run here:
scripts/fast.tscould not name a small set of tests for the merge. It reported 2304 of 2788 files and about 195 minutes, far over its 5-minute budget, so I stopped that run. The forge's check runs the whole suite on the push.
Earlier asks (terminal door in src/tui/screens/forgename.ts, docs/features/forge-name.md, nothing added to README) are met. Preview works: admin gives reserved, a fresh name gives free, no-store, rate-limited per address. The door now refuses system words by the same function. The job-slices UNTIMED line is a fix for main's red test and is fine. Non-blocking: GET /forges/name/ready and /forges/name/remove are matched by the earlier /forges/:name/ready and /forges/:name/remove routes and answer 404, so those two names cannot be asked about; move the new route above them in src/http/server.ts, or open an issue.
The full gate on land was red and named no test; the pull is open again.
The gate's log: /api/v1/repos/trevato/forge/gates/1794384871232-6ac91fcfd9b9/log
The full gate on land was red and named no test; the pull is open again.
The gate's log: /api/v1/repos/trevato/forge/gates/1794386738595-4e01c677d7bd/log
The gate failed on a type error, which I fixed. The branch is merged with the latest main and pushed.
- Cause:
test/job-slices.test.tshadentitlementstwice in theUNTIMEDobject. My earlier line and one that came in frommaincollided, and the type check (error TS1117) failed on it. I removed my line and keptmain's wording. - Sentinel's note:
GET /forges/name/:nameis now registered before the/forges/:name/...routes insrc/http/server.ts./forges/name/readyand/forges/name/removeshould no longer be shadowed. I did not add a test for those two names or request them against the preview, so that is untested. - Checks run here:
bun x tsc --noEmitandbun scripts/gate.tsare clean.test/forge-name.test.ts,test/job-slices.test.tsandtest/stranger-three-screens.test.tspass (19 tests). The full suite is left to the forge's check, as before.
Earlier asks met: terminal door (guest palette entry), docs/features/forge-name.md, and the route no longer shadowed by /forges/:name/* — the preview answers free for /forges/name/ready and /forges/name/remove, reserved for admin. The door refuses system words through the same function. Later rounds only fixed the merge and a duplicate UNTIMED key. Non-blocking: add a test for the ready/remove names so the route order stays pinned.
GET /forges/name/:namenow answers{ verdict, why }withfree,taken,invalidorreserved, and the door refuses system words by the same function. I ran the type check,bun scripts/gate.ts, the new test file, and the spawn, start-forge, new-forge, reserved-names, MCP-door and ready tests. All pass, but the run printed sandbox network denials for*.forge.test/localhostchild hostnames; I took the pass counts as the verdict and did not open the log.scripts/fast.tscalled the diff too broad to name tests, so I did not run the full suite. I did not use thekeep_plantool, and I did not try the app against a running preview.What moved where
src/spawn/name_verdict.ts(new):nameShapeVerdict(name)returnsinvalidorreserved, or nothing when the name passes both.nameVerdict(name, exists)addstakenandfree. It uses the door's own name pattern (NAMEinsrc/spawn/plan.ts, now exported) and the system words insrc/core/reserved.ts. The pattern allows 1–39 characters, so a 40-character name isinvalid, as the door treats it.src/web/pages/forge-name.ts(new): the handler isno-storeand rate-limited per address likeforge-ready.ts(60 a minute, 429 withretry-after). It looks in the ledger, and at children being made, only for names that pass the shape check, so nothing odd reaches a path. The route is one line insrc/http/server.ts.src/spawn/door.ts:spawnDoornow callsnameShapeVerdictand returns a 422 for a reserved name. Before this,admingot through the door, so the page's "free" was a lie. A bad shape is still refused byplanSpawn, in the same words as before.isSystemNameinsrc/core/reserved.tsandbeingMadeinsrc/actions/spawn.ts.test/forge-name.test.ts(new): free, taken (a ledger entry), invalid (Bad Name!, 41 characters,-a), reserved (admin), headers, the door's 422 onadmin, and the 429. The gate takes everytest/*.test.ts, so there is no separate list to edit.What I chose not to do
trevatostays free.taken.home.tsandstart-forge.tsuntouched, as the issue asks.Push: the repository's pre-push hook wanted a commit saying how to try the change, so there is an empty
Try:commit at the end.To try it: run
curl $URL/forges/name/adminforreserved,/forges/name/Bad%20Name!forinvalid, and an unused name forfree.🤖 Generated with Claude Code
Closes #11311