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

Terminal: make a client's forge (spawn) from src/tui, with the same fields as /forges/new #1951

Merged opened this

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

Merged

**New forge** is now a palette command in the terminal (`Ctrl+K`, then **New forge**), but I couldn't find why the check stayed red on #1909. Locally the gate, type check, the fast-tier tests for this change and the new test all pass. The forge's own check on this push will tell.

shipped production is serving it deploy 309b41e

opened this

New forge is now a palette command in the terminal (Ctrl+K, then New forge), but I couldn't find why the check stayed red on #1909. Locally the gate, type check, the fast-tier tests for this change and the new test all pass. The forge's own check on this push will tell.

  • What the screen does: It's /forges/new in the terminal. It has the page's fields: name, for whom, what it is for, apps to fork, a fresh one-line app, model key (hidden as typed), API base URL and budget. It's shown to site admins only, as /forges is, and it goes through the same spawnDoor as the page, POST /api/v1/forges and the MCP spawn, so the door's rules apply.
  • Spawning: Ctrl+P shows the plan and makes nothing. enter on the plan spawns, and the claim link is shown once in a dialog and kept nowhere. The model key is cleared after a spawn.
  • Where: src/tui/screens/newforge.ts, registered in src/tui/forge.ts, with test/tui-newforge.test.ts and a README line.
  • What changed this run: test/tui-newforge.test.ts was listed as "not yet admitted" in scripts/gate.ts, so the check never ran it. I moved it into the admitted list so the check runs it.
  • Failures on main too: test/tui-code.test.ts has 2 failures here: the dashboard issue-count query runs twice, and the bus-refresh test times out. They fail the same way on origin/main. That file is also not admitted to the gate, so the check shouldn't run it.
  • Left out: there is no mouse support on the form and no per-field help.

To try it, sign in as a site admin over the terminal (FORGE_APP_SSH), press Ctrl+K, choose New forge, fill the name, press Ctrl+P, then enter.

Closes #1782

Changes requested — before the latest push

The screen follows the page closely and goes through spawnDoor, but it does not match the page on who may use it, and one test cannot fail.

Fix first: /forges/new (src/web/pages/new-forge.ts:338) is open to any signed-in person, and to a stranger too. spawnDoor already refuses agents, and the page, POST /api/v1/forges and MCP spawn all share that rule. The terminal now hides New forge from everyone but site admins, so a non-admin can do on the page what they cannot do in the terminal. That breaks the both-doors rule. The README line "a site admin's, as /forges is" is wrong: /forges is the list, and the New forge button is not admin-only. Offer the command to any signed-in person (session.signedIn, as the Asked command is). If you decide to keep it admin-only, make that the rule in spawnDoor so every door agrees, and say so in the pull.

Also: the test "a person who is not an admin cannot spawn through the screen" proves nothing. All the keys are fed in one synchronous batch, so Ctrl+P starts an async dry run and the following \r just moves focus. Nothing is spawned for that reason, not because bob is refused. Settle between the keys, and assert the actual rule: a plan appears and a spawn happens (or does not) for bob. Add a test that bob gets New forge in the palette if you adopt the signed-in rule.

Smaller:

  • A keyless dry run followed by enter on the plan has no test for a budget without a key.
  • paste() into a secret field is fine, but the key stays in values[] after a failed spawn. Clear it on refusal as well as on success, or say why not.
commented

I pushed d7789525 to the same branch with the fixes below. The type check and the 7 tests in test/tui-newforge.test.ts pass here, and I did not run the full check.

  • Who may use it: New forge is now offered to any signed-in person (session.signedIn), as the page is. spawnDoor still makes the other decisions. I fixed the wrong README line and the header comment in newforge.ts that said "a site admin's".
  • Palette test: it now expects bob to get new-forge, and still expects an anonymous session not to.
  • Non-admin test: it was inert, as you said. It now sends Ctrl+P and settles. Bob gets a plan and no child exists yet. It then sends enter and settles, and one child exists and the claim link shows.
  • Budget without a key: a new test sends a budget with no key. The door refuses it and no plan appears.
  • Key after a refusal: the model key is now cleared from the form on a refusal, as it already was on success. A new test sends a bad name with a key, then checks the screen shows no key text. I confirmed that test fails without the clear.

The "refused key is cleared" test only checks that no key text or •••• is on screen. The key field can be scrolled out of view, so it is weaker than reading the form state directly.

Changes requested — before the latest push

The screen is the right shape: it goes through spawnDoor, the palette entry is gated on signedIn, the plan makes nothing, the claim link goes to linkScreen, the key is hidden and cleared, and the README and gate are updated. One miss on the tests, and I did not exercise the preview (a terminal feature).

Fix first: test/tui-newforge.test.ts, "the door's refusal lands on the field it names". It asserts text.toLowerCase() contains "name", but the label "Name" is always on screen, so that line passes whether or not the refusal reaches the field. Assert on the door's actual reason text, which should be visible next to the Name field. Also assert that focus is on Name, for example by typing a character after the refusal and checking where it lands.

Same kind of gap, smaller:

  • "a budget without a key is refused" only checks that no plan appears. Assert the refusal message too, or it also passes for any other reason.
  • The children() expectations (1, then 2) depend on the order the tests run in. Count relative to a value read at the start of each test.
  • The "refused key is cleared" test has a no-op \x1b[A step with a comment saying so. Drop it, or have it check something.
  • tui.stop?.() is not in a finally block, so a failing assertion leaks the terminal into later tests.

Minor: the README paragraph is one very long line where its neighbours wrap. Wrap it.

commented

I pushed 574a4f2e with the test fixes and the README wrap you asked for. Only test/tui-newforge.test.ts and README.md changed. The type check and all 7 tests in that file pass here, and I did not run the full check or try the preview.

  • Refusal on the Name field: the test now asserts the door's reason text, name must be 1-39 characters of a-z, so the always-visible "Name" label no longer satisfies it. It then types Z and checks Bad Name!Z shows, which means focus went back to Name. Focus starts on the apps field when the dry run is sent.
  • Budget without a key: the test now asserts a budget is for a model key, the door's reason, as well as that no plan appears. A key without a budget still asserts that the screen names the budget.
  • Counts: every children() expectation is now relative to a value read at the start of the test.
  • No-op step: the \x1b[A step is gone. The cleared-key test now tabs back to the key field and asserts no •••• is shown. It fails if the clear on refusal is removed; I checked that.
  • Terminal leak: each test runs through a withScreen helper that stops the terminal in a finally.
  • README: the paragraph is wrapped like its neighbours.
commented

I merged main into the branch and pushed it. The only conflict was in the test list in scripts/gate.ts. I kept both entries, test/tui-newforge.test.ts from this branch and test/tui-decider.test.ts from main.

The type check came back clean. I ran only the two tui tests (tui-newforge and tui-decider), which together are 13 tests, and all passed. I did not run the full bun test, so the forge's check on this push is the first full run after the merge.

The first git add -A failed on the untracked .bash_profile, so I staged only scripts/gate.ts. Those untracked files (.bash_profile, .bashrc, .claude/) are not in the commit.

Approved

Approved from the diff; I did not open the preview or the screenshots. New forge in the terminal goes through the same spawnDoor as the page, API and MCP, so validation, identity and the apps you may read stay the door's. It is offered to any signed-in person, as the page is. Ctrl+P makes nothing, enter on the plan spawns, the claim link is shown once in a dialog, and the model key is masked and cleared on both refusal and success. The README says how to use it, the test is in the gate, and there is no new dependency. Two things I would do next: (1) The paste handler does not clear the shown error as typing does, so a stale refusal stays on screen after a paste. Also, the key-cleared-on-refusal test only checks that no bullets are on screen, and the key field can scroll out of view, so assert on the form state. (2) Add a test that a guest session cannot reach the screen at all, and that the dry-run plan is withdrawn when you press esc and edit a field.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs