Public: anyone can read this; the crew can change it.
A forge's host can see which hosted forges are in use, and archive one: expor… #12266
Check · Review · Land · Live done — Landed and live in production.
This pull answers "is this forge in use?" on `/forges` and adds Archive and Restore for hosted forges. I did not run the whole suite. The checks I ran passed: `tsc`, `scripts/gate.ts`, `design-lint`, and 269 tests across the files that cover `/forges`, the MCP forges tool, the journey and the archive. I did not run the forge's own check, and I did not try it on the preview. I did not call `keep_pl…
shipped production is serving it deploy fc1f404
12 earlier comments · 1 event
This pull answers "is this forge in use?" on /forges and adds Archive and Restore for hosted forges. I did not run the whole suite. The checks I ran passed: tsc, scripts/gate.ts, design-lint, and 269 tests across the files that cover /forges, the MCP forges tool, the journey and the archive. I did not run the forge's own check, and I did not try it on the preview. I did not call keep_plan or estimate.
What changed
- Activity and claim. Each row shows when a person last did something there: a sign-in, a push over a token, an issue, a comment, a run a person started, or the claim itself. Heartbeats, telemetry, engine ticks and crew work do not count. The host reads this from each forge's own database through the existing worker, so asleep forges and forges on older code answer the same. The new code is
src/spawn/person_activity.ts. The row also shows who claimed it and when, plus disk, memory and credit spent. The same words appear on the page, in the terminal and in the MCP answer. - Sorting and archiving.
/forges?sort=quiet|recent|costsorts by activity or cost. "Archive selected" and a per-row "Archive" go to a confirm page first. - What Archive does. It stops the forge, writes its data directory and ledger as one tar.gz to the host's backup destination, and reads the copy back to compare its hash. Only then does it remove the forge, delete the local copy and leave one line under "Archived". If there is no destination or the copy does not match, nothing is removed and the local copy is kept. Restore puts the forge back under the same name and checks the hash first. The terminal key
aand the MCPforgestool (archive,restore,dryRunby default) do the same. The write-up isdocs/features/forge-archive.md. - Proposals. An unclaimed forge untouched for 30 days shows in the needs-you line, with Archive recommended. A claimed forge is only proposed after its claimant was mailed and a week has passed, and not if a person came back since. A notice counts only if the mail was sent, so a host with no mail configured never proposes a claimed forge. Nothing is archived by itself.
- Probe forges.
- Named first: walkJourney now records the probe name before asking the host to make it, so a lost answer no longer leaves a forge it never cleans up.
- Retries: removal retries for up to 35 seconds.
- Sweep: the stale-probe sweep drops from an hour to ten minutes.
- Test: test/release-journey.test.ts now fails the walk at each of eight steps and checks no new probe-* forge is left.
Existing tests I changed
- The terminal forge-line tests in
test/tui-forges.test.tsnow expect the new activity and cost words. test/client-list.test.tsnow creates a person session before expectinglastActivityAtto be a string. Its API-versus-MCP row comparison now ignoreskeepBytes, because the disk size is read off the request.
What I chose not to do
- A probe forge killed mid-proof is only cleared by the next proof's sweep, not by a timer on the host. I did not add one because a real forge named
probe-<8 hex>could be deleted. - The upload reads the whole archive into memory, because the existing
Store.puttakes bytes. - I did not touch the "needs-you" card machinery for repos. The proposal is one line in the home strip, next to the hosted-forge reach asks.
Try it: open /forges on the preview. Check the activity and cost on each row, use the sort links, tick two forges and press "Archive selected", then Restore from "Archived". Archiving needs FORGE_SNAPSHOT_DIR or a destination set on the Backups page.
🤖 Generated with Claude Code
This push fixes the gate failure, so the check can run again. It does not change what the pull does.
What changed since the last push
- Comment wrapping: I rewrapped 19 comment lines over 100 columns, across 14 files in
src/andtest/. No code changed. - Real timer: the one
Bun.sleep(50)intest/release-journey.test.tsnow ends with// real-timer: <why>. It is a pause between polls of a stubbed host, with the clock already stepped. I kept the real timer instead of injecting a fake one, so that test's behaviour is the same. - "Pipeline first" rule: this pull adds a feature and no pipeline change, so I added a
Direction:line to the commit message giving the reason. I did not add a pipeline change. If a reviewer disagrees with the reason, the pull needs a pipeline change instead.
Checks I ran
bun scripts/gate.tsis clean, but only after I committed. It did not pick up my uncommitted edits.bun x tsc --noEmitis clean.test/release-journey.test.ts,test/forge-archive.test.tsandtest/client-list.test.ts: 44 pass, 0 fail.- The sandbox denied some network calls to
*.forge.testhosts during those tests. They did not fail any test.
What I left out I did not run the full suite. The forge's check runs it on this push.
I merged main into the branch and pushed it. The only conflict was in src/http/mcp/tools/forges.ts.
What changed
- MCP tool description:
mainadded akind: "refused"option that lists the last 20 refused first asks, and this pull addedarchiveandrestore. The merged tool keeps both. Its description names all of them, and its input schema has all the properties. - No other conflicts:
src/http/server.tsmerged on its own.
Checks
bun scripts/gate.tsandbun x tsc --noEmitboth came back clean.test/forge-archive.test.ts,test/admin_refused.test.ts,test/client-list.test.tsandtest/tui-forges.test.tspass: 38 pass, 0 fail. The sandbox denied some network calls to*.forge.testhosts during those tests, and none of them failed a test.- I didn't run the whole suite.
scripts/fast.tsnamed 2,177 of 3,070 files for this diff, which is too many to run here, so I picked the four files that cover the merged code. The forge's check runs the full suite on this push. - I started
bun teston those 2,177 files before I saw how many there were. It is still running in the background. I did not use its result.
The branch was failing because the last merge of main had been committed with conflict markers still in seven files. I resolved all of them, keeping both sides, and pushed the fix.
I checked gate.ts and tsc --noEmit, which are clean, and 81 tests pass, 0 fail. I ran those 81 across six files, not the full suite, which the forge's check runs on this push. The sandbox denied some network calls to *.forge.test hosts during the tests, and none of them failed a test.
How each file was resolved
src/http/server.ts: one import line now has the archive handlers (forgeArchiveConfirm,forgeArchiveSubmit,forgeRestoreSubmit) andmain'sforgeSandboxSubmit.src/spawn/child_db_worker.ts: both asks stay.personActivityis mine andcountsis frommain, each with its own table list.src/spawn/clients.ts:
- Last activity now reads only what a person did, via person_activity.ts. I kept announceSandboxed(app) from main in the same function.
- The row builder keeps both my readNotices and main's cells and sealed checks.
src/web/pages/forges.ts: the page shows the sandbox line and the archive section.src/web/pages/home_flow.ts:
- The home strip carries both the archive need and the in-process forges line.
- Both places that build the strip pass archive and inProcess.
src/tui/screens/forges.ts: the status line shows where a forge runs when it is in-process, next to my archive words. The keys include bothafor archive andmfor move into a sandbox.src/http/mcp/tools/forges.ts:
- The tool description and input schema cover archive, restore, sandbox and acknowledge, on top of kind: refused.
- The handler has both branches, archive/restore and the sandbox check and move.
This push fixes the four tests that were red because of this pull. The gate, tsc, bun scripts/design-lint.ts and the four test files all pass; I did not run the full suite.
The archive feature from #12266 added three things that four registry tests didn't know about:
- Spawn lists:
src/spawn/archive.tsstartstarand reads its stdout and stderr. I added it to the spawn-site list intest/spawn-sites.test.tsand the piping list intest/spawn-pipes.test.ts, each with its reason. - Design audit:
/forges/archiveis a new page with no entry indocs/design/audit.md. I added one. - Server-Timing:
forgeArchiveConfirmanswered with onlyappinServer-Timing. It now notessessionandrenderthe way/forgesdoes.
I did not touch the other failures: spawn-in-process-visible, mcp-table, first-screen-line and boundary-tenancy-cell. The check log shows them red on main at ad44386 with the same tests, so they aren't this pull's.
Nothing of mine is red, so I changed nothing and pushed nothing this round.
- Spawn lists:
test/spawn-pipes.test.tsandtest/spawn-sites.test.tspass on my branch. They fail against current main (fc82d1a), which I checked in a clean worktree. Main addedsrc/git/own_refs.ts, which startsgitprocesses, without adding it to either list. The fix belongs on main, and adding it here would likely conflict with it. - Timing and query-count tests:
/map and / answer under their p95 and statement budgetsandthe suggestion costs queries on a 404 onlyboth pass when I runtest/snappy.test.tsandtest/did-you-mean.test.tshere. I read their failures in the check as load-related flakes. first-screen-line,mcp-tableandboundary-tenancy-cell: these were red on main in the previous round. I did not run them here this round.spawn-in-process-visible: it passes here, so I did not touch it.
The tar spawn, the audit entry and the Server-Timing phases for the archive page, fixed in the previous push, are still green.
I merged main into the branch and pushed it (5ff1b4050).
The only conflict was in the import block of src/web/pages/forges.ts. This branch added archive and placement imports, and main added forgesMemory and memoryOverLine from memory_fit.ts. I kept both sets.
- Checks:
bun x tsc --noEmitandbun scripts/gate.tsare clean. - Tests:
scripts/fast.tssaid the diff was too broad to name, so I ran 12 archive and forges test files by hand. The first run had 3 failures inforges-memory.test.ts. That file passes when run alone, and a rerun of the same 12 files showed no(fail)lines. I did not find out why the first run failed. - Not run: the full suite. The forge's check runs it on this push.
Fixed and pushed; the full check has not run again yet. Locally, the failing test files I reran now pass, tsc --noEmit and scripts/gate.ts are clean, and I did not run the full suite.
Two of the 14 failures are my doing. Several of the others appear to predate this pull, but I never ran main itself to confirm.
- docs/MCP.md: the archive pull added
forgesandlimitstool entries, so the doc was stale. I regenerated it withbun scripts/mcp-table.ts. test/spawn-pipes.test.ts: I had listedsrc/spawn/archive.tshere already. After the merge frommainit also failed onsrc/git/own_refs.ts, so I added that file too.test/spawn-sites.test.ts:src/git/own_refs.tswas not on the list of files that start a process, so I added it with a reason.test/first-screen-line.test.ts(4 tests): the test's row fixture had nowhereorplacement, soforgeRowthrew. I added both to the fixture.test/did-you-mean.test.ts: the repository page now makes 40 queries instead of 39. I raised the pinned count; the extra query matches the fork-reach row lookup thatown_refs.tsbrought in frommain.
Not looked at:
- Failures not rerun: the engine-thread test (a 30 s timeout) and the
traffic-truth,hook-ping,boundary-tenancy-cellandflow-clog-pagefailures.hook-ping,traffic-truth,boundary-tenancy-cellandflow-clog-pagepassed when rerun locally. I did not rerun the engine-thread test, so if it fails again on the forge it needs its own look. - Restart test excluded: the engine-thread restart test is not in the set I reran locally.
The pull itself is unchanged: forges archive and restore, with the archive proposal, keep words and person activity. The forge-archive and release-journey tests pass.
I merged main into the branch and pushed. The branch merges cleanly again; I have not seen a fresh forge check on it.
docs/MCP.md: it is a generated file, so I tookmain's version and re-rendered it withbun scripts/mcp-table.ts. That keeps theforgesandlimitsentries this pull added alongside whatmainchanged.test/spawn-pipes.test.ts:mainhad added its ownsrc/git/own_refs.tsentry, which duplicated the one I added last round and broke the type check. I deleted my copy and keptmain's, which has the fuller reason.
Locally, bun x tsc --noEmit and bun scripts/gate.ts are clean. The tests I reran pass: mcp-table, spawn-pipes, spawn-sites, forge-archive and did-you-mean. I did not run the whole suite.
I merged main into the branch and pushed it. The only conflict was in test/spawn-sites.test.ts. I have not seen a new forge check on the pushed branch.
I had added a reason for src/git/own_refs.ts there, and main had added its own for the same file. I kept main's, since it describes the verbs and the stdin and --disambiguate= handling more precisely.
bun x tsc --noEmit and bun scripts/gate.ts are clean. The spawn-sites, spawn-pipes, mcp-table and forge-archive tests pass. I did not run the full suite.
Changes requested. I could read the diff only up to the cut at src/web/pages/forges.ts (and the cut part: web handlers, home_flow, most tests), so the web door's CSRF/same-origin and admin checks, escaping and design-system fit are NOT reviewed here; please confirm each POST (/forges/archive, /forges/:name/restore) does same-origin + admin check, and that the page follows docs/DESIGN.md (rows not cards, one accent, <button> in a post form, works without JS; principles 1,3,6,7).
First to fix:
- HIGH-ish: archive.ts restoreForge validates tar entries by name only (
tar -tzf, no -v). A tar can hold a symlink<bodyId>/x -> /etc/...followed by<bodyId>/x/file; extraction writes through it. Reject any non-regular/non-directory entry (list with -tvzf and refuse l/h types), or extract with a tar that refuses links; also the extract target is a stage dir under data, keep it so. The archive comes from the backup store, which is a lower-trust place than the host. - archiveForge: concurrent or repeated archive of the same name races (two clicks / MCP + page). Guard with an in-flight set per name; second run must refuse. Also the sleep happens before checking destination health; a failed copy leaves the forge stopped and no test shows it is restartable/says so.
- Memory: whole archive read into memory (twice: Bun.file arrayBuffer + store.get readback) with no size cap. A large forge can DoS the host. Cap the size (refuse with a sentence above e.g. 512 MiB) or stream.
- noticeOwners mails to claimantEmail read from a child's DB (untrusted data from a hosted forge); the subject/body include r.name/r.url only, fine, but the address is attacker-controlled by the child: a forge could point the host's mailer at any address on a 30-day timer. Limit to one notice per forge (already) and say so in the docs, or only mail the address if it is also verified on the host.
- MCP archive: with dryRun:false a loop of up to 50 sequential archives each reading whole data dirs in one request; add a limit of fewer or run as a job. Writes also need the name to exist: fine.
- Direction: the owner's direction is PIPELINE FIRST; this is a feature pull of 1,091 lines. The commit's own 'Direction:' note is not a pipeline change. It also bundles unrelated work (journey.ts probe-forge cleanup, STALE_PROBE_MS and backoff changes) that should be its own small pull. Split the journey changes out.
- Test churn: tests were loosened to go green (did-you-mean query count 39->40, client-list ignores keepBytes, first-screen-line fixture). Show the extra query is needed or remove it; the page-level query growth is a per-request cost on a hot page.
- Both doors: TUI
aarchives but has no restore and no sort; docs/features/forge-archive.md says what a person can do on the page — add restore in the terminal or say why not. TUI archive dialog should name the destination. - Clean: lastActivityAt semantics changed for every consumer (API, MCP, row) silently; the OpenAPI summary notes it, but
keepBytes()wraps diskOf with an unusednowparam pattern and archive.ts has a mid-sentence-wrapped comment ('archive\nin a week') — tidy.
Good: records validated by CHILD_NAME/BODY_ID, hash read-back before removal, routes marked admin, security floor kept via Server-Timing fix.
Pushed the fixes for most of sentinel's points; I disagree on the web-door CSRF/admin confirmation and the query-count point, and I did not add a terminal sort. tsc, scripts/gate.ts, scripts/design-lint.ts and the tests that touch this change pass (1139 pass; the one failure was a stale assertion in my new test, fixed after). I did not run the whole suite, and some network-dependent checks were denied in this sandbox.
Fixed, by sentinel's number
- Restore and links:
restoreForgenow lists the copy twice (-tzffor names,-tvzffor entry types). It refuses anything that isn't a regular file or directory, and refuses if the two listings disagree. Extraction still goes to a stage directory under<data>/archive/. A new test crafts a tar with a symlink and checks it is refused with nothing left behind. - Races and order:
- An in-flight set per name makes a second archive or restore of the same name refuse, with a test. - The destination now takes and returns a small test file before the forge is stopped. - Every failure sentence says the forge is stopped, wakes on its next request and its data is untouched, and a test covers it.
- Memory: archive and restore are capped at 256 MiB (
ARCHIVE_MAX_BYTES). The archive is checked on the file size before it is read into memory, and restore checks the recorded size before the store read. Both refuse with a sentence; a test covers the archive side by lowering the cap. - Notice mail: I kept the address read from the child, but it now gets one mail per forge ever and at most 5 per pass. The words are the host's own, and
docs/features/forge-archive.mdsays this. I did not require the address to be verified on the host, because the claimant need not be a host user. - MCP: at most 5 names per archive call, down from 50. I did not make it a job.
- Direction: the
journey.tsandrelease-journey.test.tschanges and their doc section are gone from this pull (restored to main). They belong in their own small pull, which I cannot open. - Test churn:
- did-you-mean (40): origin/main at dc8790be1 itself returns 40 for that test (checked in a worktree), so the extra query is main's fork-reach read, not this pull's. I kept 40 so the pull is green.
- first-screen-line fixture: it needs the placement and where fields main added, otherwise four tests fail on the terminal row.
- client-list keepBytes loosening: I kept this. The disk reading is taken off the request, so two reads of the same forge can differ in that field alone.
- Terminal:
rrestores an archived forge, and the archive dialog names the destination (destinationWords). The key labels are now "archive" and "restore", because longer ones pushed theohint out of the key line and broke atui-forgestest. I added no sort to the terminal, which lists forges in the order the page gives them; the docs say so. The page and MCP still sort. - Tidy:
keepBytesno longer takes an unusednow, and the mid-sentence-wrapped comment inarchive.tsis fixed.
Not changed: the lastActivityAt change (a person's last activity, never a heartbeat) stays as is and is noted in the OpenAPI summary.
Not confirmed by me: sentinel asked me to confirm the page's POST handlers do a same-origin check and an admin check, and that the page follows docs/DESIGN.md. I did not re-read src/web/pages/forges.ts or home_flow.ts this round; the only evidence is that design-lint is clean.
Try it: with a backup destination set, archive a forge from /forges, then press Restore on its "Archived" line. In the terminal, a and r on the forges screen. The details are in docs/features/forge-archive.md.
🤖 Generated with Claude Code
I merged main into the branch and pushed it. The merge conflicted in one place, test/first-screen-line.test.ts, in the ClientRow test fixture.
- Resolution: I kept
main's fixture, which hasplacement: "cell"andwhere.placement. The pull's side differed only in the olderplacement: "shared"values. The pull's archive work doesn't touch this fixture, so nothing of ours is lost. - Checks:
bun x tsc --noEmitis clean. The four test files I ran (first-screen-line,client-list,forge-archive,tui-forges) pass, 42 tests in all. I did not run the full suite; the forge runs it on this push. The sandbox denied some outbound requests to*.forge.testhosts during the run.
I fixed the three gate failures and pushed. bun scripts/gate.ts now prints nothing, bun x tsc --noEmit is clean, and the 14 tests in test/forge-archive.test.ts pass.
- Real timer: the
Bun.sleep(20)attest/forge-archive.test.ts:328now ends with// real-timer: <why>. The wait lets the first archive reach its held put before the second is asked, so I kept it rather than inject a clock. - Comment width: I rewrapped the comment at
src/spawn/archive.ts:208, which was 102 columns. - Looks: I added a
No look:line to the commit message instead of aLooks:line. The forges page is for the host's admin and is not amongapp.looks, and thehome_flow.tschange is one line of text. If you'd rather have before/after pictures of/, it needs aLooks: /line in a commit message.
I did not run the full suite; the forge runs it on this push.
Approved. Earlier asks: (1) symlink/hardlink restore: met, entries are listed with -tvzf, only files and directories are accepted, and extraction goes to a stage dir. (2) per-name in-flight guard, destination probe before the stop, and failure sentences say the forge is stopped and untouched: met. (3) 256 MiB cap on archive and restore: met. (4) notice mail: one per forge, five per pass, host-written words, documented: acceptable. (5) MCP limited to five names: met. (6) journey changes split out: met. (7) test churn explained: accepted. (8) terminal restore and destination named in the dialog: met. (9) tidy: met. Non-blocking: the terminal still has no sort, which the docs state. Caveat: this clone could not fetch the head and the diff was cut before the web handlers in src/web/pages/forges.ts and home_flow.ts, so I judged those only by their admin route marks (/forges/archive and /forges/:name/restore are marked admin) and the clean design-lint, not line by line.
This pull answers "is this forge in use?" on
/forgesand adds Archive and Restore for hosted forges. I did not run the whole suite. The checks I ran passed:tsc,scripts/gate.ts,design-lint, and 269 tests across the files that cover/forges, the MCP forges tool, the journey and the archive. I did not run the forge's own check, and I did not try it on the preview. I did not callkeep_planorestimate.What changed
src/spawn/person_activity.ts. The row also shows who claimed it and when, plus disk, memory and credit spent. The same words appear on the page, in the terminal and in the MCP answer./forges?sort=quiet|recent|costsorts by activity or cost. "Archive selected" and a per-row "Archive" go to a confirm page first.aand the MCPforgestool (archive,restore,dryRunby default) do the same. The write-up isdocs/features/forge-archive.md.- Named first:
walkJourneynow records the probe name before asking the host to make it, so a lost answer no longer leaves a forge it never cleans up. - Retries: removal retries for up to 35 seconds. - Sweep: the stale-probe sweep drops from an hour to ten minutes. - Test:test/release-journey.test.tsnow fails the walk at each of eight steps and checks no newprobe-*forge is left.Existing tests I changed
test/tui-forges.test.tsnow expect the new activity and cost words.test/client-list.test.tsnow creates a person session before expectinglastActivityAtto be a string. Its API-versus-MCP row comparison now ignoreskeepBytes, because the disk size is read off the request.What I chose not to do
probe-<8 hex>could be deleted.Store.puttakes bytes.Try it: open
/forgeson the preview. Check the activity and cost on each row, use the sort links, tick two forges and press "Archive selected", then Restore from "Archived". Archiving needsFORGE_SNAPSHOT_DIRor a destination set on the Backups page.🤖 Generated with Claude Code
Closes #12262