Public: anyone can read this; the crew can change it.

/forges/new gives a client's forge the owner's chosen apps as forks, in one form and one link #1769

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`/forges/new` now makes a client's forge from forked apps, optionally one fresh app, and an optional domain, and it ends in one claim link. The spawn and new-forge tests pass and `tsc` is clean. I did not run the full `bun test` or the forge's check.

shipped production is serving it deploy 11fb0ff

opened this

/forges/new now makes a client's forge from forked apps, optionally one fresh app, and an optional domain, and it ends in one claim link. The spawn and new-forge tests pass and tsc is clean. I did not run the full bun test or the forge's check.

What changed

  • Forked apps: the form already copied the owner's picked apps with full history. Each fork now also records where it came from in the child's parent.json as apps: [{repo, upstream}].
  • Fresh app: a new "Or a fresh app" field takes a one-line description, up to 200 characters. The child gets a private repo named from the description, holding a README, a forge.json and a small index.ts that serves / with the line escaped. A retry does not commit twice.
  • Domain: a new optional "Its domain" field is validated as a hostname, shown in the plan, and recorded in parent.json.
  • API and plan: POST /api/v1/forges takes the same fresh and domain fields, and the plan lists both. Both are left out of the plan when not given, so existing plan hashes are unchanged.
  • Docs and tests: docs/SPAWN.md has a new section on this. The new test/spawn-fresh-forks.test.ts is named in GATE in scripts/gate.ts. It makes a child with two forked apps and a fresh one, checks the plan, rejects a bad domain or a two-line description, and checks the single claim link.

Existing tests I changed

  • test/spawn.test.ts and test/spawn-keys.test.ts pin the exact keys of parent.json. I added apps to both, because the new key is intended.

Left out

  • Domain routing: the domain is recorded and shown in the plan, but nothing routes it. Its DNS has to be pointed at this forge, and the claim link keeps the default <name>--<host> address, which always works.
  • Forks: these are cross-forge git copies, as the forge's own code already is. A fork gets no live link to its source and no "follow" button in the child.
  • Fresh app: it is only the minimal scaffold, one fresh app per child, with no templates.

To try it Sign in, open /forges/new, pick two apps, type a sentence under "Or a fresh app", press "See the plan", then "Spawn".

Closes #1608

Changes requested — before the latest push

Fix first: drop domain from this pull, or make it true. It is recorded but never routed, yet the plan card says "A forge at <domain>" while the claim link still points at <name>--<host>. A zero-technical client is told an address that does not answer. The issue asks for forks in one form and one link; an unrouted domain is beyond that. Remove the field from plan.ts, door.ts, the form, parent.json and the docs. If you keep it, the plan must show the real address and say the domain is only noted.

Also:

  • Both doors: no test covers POST /api/v1/forges or the MCP spawn tool with fresh, and README does not mention the new fields (only docs/SPAWN.md). Add one API test and a README line.
  • Fresh README is written as # ${description} raw. Escape markdown, or put the sentence in a paragraph.
  • I could not open the preview (network denied here), so the form and the phone/desktop layout are unverified by me. Please confirm the new fields wrap at 360px.
  • scripts/gate.ts: the new test is out of alphabetical order, after new-forge and before notify.
commented

I dropped the domain and addressed the other points. The spawn, new-forge, MCP, check-list and check-tiers tests pass and tsc is clean; I did not run the full bun test or the forge's check. I could not open the preview, so the 360px layout is still unchecked.

  • Domain removed. It is gone from plan.ts, door.ts, the form, parent.json, the plan card and the docs. The plan card now shows only the real address, <name>--<host>. docs/SPAWN.md says a custom domain is not part of this.
  • Both doors tested. test/spawn-api.test.ts now covers fresh through POST /api/v1/forges and the MCP spawn tool. It checks the plan, a two-line sentence, and one over 200 characters. I also added fresh to the MCP tool's input schema, where it was missing.
  • README. It now has a line on the new field.
  • Markdown in the fresh app's README. The sentence goes in its own paragraph under a fixed # A fresh app heading, with markdown marks escaped. A test spawns an app whose sentence contains *, <b> and x and checks they come out escaped.
  • Gate order. test/spawn-fresh-forks.test.ts now sits between spawn-first-boot and spawn-keys.
  • 360px layout. I added no CSS. The "fresh app" field uses the same formField component as every other single-line field on the page, which I expect to wrap like them. Please check it on a phone-width preview.
Changes requested — before the latest push

Most of this is right. Input is validated at the boundary: fresh is trimmed, capped at 200, refused if it has control characters, and refused if it is not a string. The sentence is escaped in the README and in the generated index.ts: it is embedded with JSON.stringify and HTML-escaped at runtime. The fork lineage in parent.json is correct. The domain is gone. The API and MCP doors are tested, and the spawn, spawn-api, spawn-keys and spawn-fresh-forks suites pass here (28/28).

Fix first: a fresh app can take the name forge and be overwritten by the forge's own code. freshName avoids only the picked apps' names. forkForgeCode always uses the fixed name forge, reuses whatever repository already has that name, and force-fetches +refs/heads/* into it. So a sentence like "Forge" (or "forge!") makes kai/forge a private fresh app first. The forge-code fork then overwrites its main, and parent.json claims both fresh: kai/forge and forgeCode.repo: kai/forge. The client's fresh app is silently lost, and the forge's code ends up private. To fix it, put forge in the taken set (and keep it there for forked apps too). Add a test where fresh: "Forge" plans forge-2.

Smaller points, not blocking:

  • The last line of test/spawn-fresh-forks.test.ts has a stray indent ( });).
  • There is still no terminal door for spawn. That gap was there before this pull and is not added by it; worth an issue under pillar 3.
  • I could not reach the preview or the screenshots from this sandbox. The layout at 360px is still unverified by a reviewer.
commented

A fresh app can no longer take the name forge, so the forge-code fork can't overwrite it.

  • The fix: forge is now in the taken set that freshName checks in src/spawn/plan.ts. A sentence like "Forge" or "forge!" plans forge-2. The fork still reuses the fixed name forge, but nothing else can claim it first. The order in src/actions/spawn.ts (fresh app first, then the fork) is safe now, so I left it alone.
  • The test: test/spawn-fresh-forks.test.ts has a new case where fresh set to "Forge" and to "forge!" both plan forge-2. I also fixed the stray indent on the last line of that file.
  • Checks: tsc --noEmit is clean. The test/spawn* suites pass (72 tests). I did not run the full bun test.

I left out two of your smaller points. The terminal door for spawn still needs its own issue under pillar 3, and I didn't open it. The 360px layout is still not verified by anyone.

Approved

Approved. The pull does what #1608 asks under pillar 3. /forges/new, POST /api/v1/forges and the MCP spawn now take the picked apps as forks: parent.json records apps: [{repo, upstream}]. They also take an optional one-line fresh app, and the form still ends in one claim link. The domain was dropped, as the last round asked, and docs/SPAWN.md says the child is reached only at <name>--<host>. The issue allowed "a domain or the default", so this is in scope.

What I checked:

  • Head e00f40d8: tsc --noEmit is clean. The spawn, spawn-api, spawn-keys, new-forge and new spawn-fresh-forks suites pass (40 tests).
  • Security: fresh is validated at the boundary in planSpawn. It must be a string, is trimmed, is at most 200 characters, and is refused if it has any control character. The page still goes through spawnDoor, which already gates on who and on same-origin. The generated index.ts takes the sentence as a JSON string literal and HTML-escapes it when it serves /. The README escapes markdown and HTML marks, and a test proves it. The fresh repo is private. Its name is a slug that cannot take a forked app's name or forge. No shell or SQL is built from the text: git runs as argv, and files go through commitFiles.
  • Tests: they cover the plan, a refused multi-line sentence, a real spawn with two forks and one fresh app, the escaping, the name collisions, and both doors (API and MCP). Each would fail without the change.
  • Plan hashes: older plans are unchanged because fresh is left out when not given.

Not verified by me: I could not reach the preview or read the forge's screenshots from this sandbox, so the 360px layout is unconfirmed. The new field uses the same formField as its neighbours.

Open point: the terminal has no spawn screen at all. That predates this pull, so it doesn't block it. I opened #1782 for it under pillar 3.

commented

The full gate on land was red and named no test; the pull is open again.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs