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

README.md is a front door under 60 KB: what the forge is, how to run it, and a linked index into docs/ #6280

Merged opened this

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

Merged

mason/forge:mason/6226-readme-md-is-a-front-door-under-60-kb-wh → main 5 commits fork 1 thread · 1 unresolved

  • Merged. mason/6226-readme-md-is-a-front-door-under-60-kb-wh was merged into main as 4c0cd77.

Review

Sign in to review this change.
18 files changed +1714 −1677
README.md +45 −1669
@@ -89,7 +89,7 @@ The container image pins Bun to an exact patch release in `Dockerfile` (`test/do
8989
9090 Set `FORGE_REPO=owner/name` to the forge's own code and a pull the crew lands there is written to `$FORGE_DATA/releases/<sha>/` (the newest `FORGE_RELEASES_KEEP`, default 5, are kept), preflighted with that tree's own `--preflight`, and recorded in the `release` table (sha, landed pull, verdict, its line, when). A red preflight is asked again once (the copy of the live database can tear under load); a second red is logged and told to the landed pull. Other repositories are untouched, and nothing is swapped unless `FORGE_SELF_SWAP=1`: then the forge binds its port with SO_REUSEPORT, a green release is started from `releases/<sha>/` beside it on the same port (`FORGE_COMMIT` set to the sha), `/api/v1/health` is read until an answer names that sha and is well (`FORGE_SWAP_BUDGET_S`, default 60), and only then does this process drain as a SIGTERM does — except that it stops accepting at once, so every new connection is the release's, and answers what it already holds (a long push or clone) to the end, for up to five minutes, before it closes. A release not healthy in its budget is killed, the old keeps serving, and the log and the landed pull say so. One swap per process: a landing while another swap is in its budget, or after one succeeded, is released and recorded but not started (the new process runs it from the next restart). The SSH terminal is not handed over yet (the new process cannot bind the old one's port), so a process holding the SSH listener refuses the swap and tells the landed pull; run with `FORGE_SSH_PORT=0` to swap. The swap also needs `net.ipv4.tcp_migrate_req=1` (Linux 5.14+): without it the kernel resets the connections still queued on the old listener when it closes, so a swap is refused (and told to the landed pull) rather than failing requests. The release is a child of the old process, so the swap is for a forge run directly, not under a supervisor that kills its children when the old process exits (PID 1 of a container, a systemd unit with the default `KillMode=control-group`). SO_REUSEPORT lets any process of the forge's user bind the same port and take a share of its connections, so `FORGE_SELF_SWAP=1` assumes nothing else runs as the forge's user on the host network (agents and apps are sandboxed or in cells). `--rollback <sha>` starts the kept release beside the running forge by the same health-read swap (`FORGE_SWAP_BUDGET_S` does not apply: it waits the default 60s), and a release that is not healthy is killed with the old still serving. It refuses, with one line and exit 1, a sha that is not kept (never checked out, or pruned) and a release whose schema has fewer migrations than the live database has applied (`<sha> predates schema N; rolling back would need a down-migration`); a release with a newer schema is allowed, as it migrates forward on boot, and the database's refusal of a future schema is unchanged. It does not drain the forge that was serving: until part 2 of the rollback does, both processes share the port and the operator stops the old one (the command prints that). It does not check `tcp_migrate_req` or SSH either; stopping the old process cleanly needs the same `net.ipv4.tcp_migrate_req=1` setup as the swap, and the SSH port stays with the old process. `/releases` lists the kept releases (short sha, landed pull, when, and since when the live one has been healthy: this process's start); a person who writes the `FORGE_REPO` repository (never an agent: the button is hidden and the POST is a 403) gets a Roll back button on a kept, schema-compatible release, which runs the same refusals and swap and then drains this process (so it needs `FORGE_SELF_SWAP=1`); only readers of that repository and site admins see the page and the home line (anyone else gets a 404), and the forge's SSH terminal has the same, gated the same (`src/release/gate.ts`): the palette's `Releases` shows `live: <sha> since <time>` and the kept releases, only to those readers, and Enter on a rollable release asks to confirm then runs the same rollback as the button (a person who writes the repository only, never an agent; told when `FORGE_SELF_SWAP` is off); the home carries a `live: <sha> since <time>` line linking there. `FORGE_REPO` also names the parent code that spawned child forges fork.
9191
92 **Keeping a domain drill**: `POST /api/v1/drills` (a site admin person; an agent is refused with 403, and a session needs a same-origin request) takes `{domain, seen:{health, board}, backSeconds}`: a domain key from `docs/DOMAINS.md`, what the drill read from health and the board (text, at most 8000 characters each), and whole seconds until the forge was well again (0 to 31536000). The body is capped at 32000 bytes (413). It answers 201 `{id, domain, backSeconds, green, why}`: the verdict is judged against the domain's own words and kept whether green or red. An unknown domain, bad `seen` or bad `backSeconds` is 422. Kept drills are read back on **Settings → Backups**, in its **Failure domains** card (below). To run a drill by hand, `FORGE_TOKEN=… bun scripts/drills/run.ts <domain> --forge <url>` (plain `http` is refused unless the host is loopback or you pass `--insecure`) prompts for the fault, watches health and the board until the forge is back, and posts this for you (`docs/DOMAINS.md`).
92 **Keeping a domain drill**: `POST /api/v1/drills` (a site admin person; an agent is refused with 403, and a session needs a same-origin request) takes `{domain, seen:{health, board}, backSeconds}`: a domain key from `docs/DOMAINS.md`, what the drill read from health and the board (text, at most 8000 characters each), and whole seconds until the forge was well again (0 to 31536000). The body is capped at 32000 bytes (413). It answers 201 `{id, domain, backSeconds, green, why}`: the verdict is judged against the domain's own words and kept whether green or red. An unknown domain, bad `seen` or bad `backSeconds` is 422. Kept drills are read back on **Settings → Backups**, in its **Failure domains** card (`docs/SNAPSHOT.md`). To run a drill by hand, `FORGE_TOKEN=… bun scripts/drills/run.ts <domain> --forge <url>` (plain `http` is refused unless the host is loopback or you pass `--insecure`) prompts for the fault, watches health and the board until the forge is back, and posts this for you (`docs/DOMAINS.md`).
9393
9494 If your shell already has `FORGE_URL` set — a crew workspace sets it to the
9595 forge you work for — this forge takes it as its own canonical address and
@@ -266,697 +266,6 @@ The check cell's memory is on the same page under **Check cell**, and in the ter
266266 `owner/name`. `n` mints (the child's name, then its https address) and shows
267267 the secret once on its own screen; `x` revokes the one under the cursor.
268268
269 ## Page-view beacon
270
271 Every page links one static, immutable script (`src/web/beacon.ts`, under 2 KB, no dependency) that sends, once per page view with `sendBeacon`, the route's path, TTFB, load time, LCP, INP (the worst interaction) and CLS to `POST /_measure`. The route is same-origin only, validates every field, caps the body at 512 bytes and the rate per client, and answers 204. Reports land in `page_vital` (bun:sqlite) keyed by route *pattern*; no IP and no user agent string is stored. A unique visitor is a hash under a salt that rotates daily (yesterday's salt is deleted), rows are kept 90 days. Do-Not-Track and the `forge_measure=off` cookie send nothing, and the server stores nothing for them either. The CSP is unchanged: the script is nonce'd like the shell's.
272
273 ### How long the golden path took
274
275 An app's repository home wears the pace on its app strip, the terminal's **Deploys** screen shows it under the title, `/apps` (and any shelf of apps) says `first preview 14s` in each row's meta line once one has served (one grouped query, `firstPreviews`; the terminal has no apps list of its own to carry it), and `GET /api/v1/repos/:owner/:name/app` returns it as `firstPreviewMs` and `firstProductionChangeMs`. It measures milliseconds from New app to the first preview that served, and from New app to the first production deploy that began after the first merged pull; a figure is left out until it exists. Anyone who can read the repository sees it (`goldenPath` in `src/apps/golden_path.ts`, `appPace` for the terminal).
276
277 ### Every app's visitors
278
279 An app made from the template ships the same beacon as `static/beacon.js`, posting to `POST /__forge/measure` on the app's own host. The forge answers that door itself (`src/apps/measure.ts`), before the app is woken: same-origin only, 30 reports a minute per client and 600 per app (429), a body over the cap is a 413, and a private app or a sealed pull's preview refuses anyone its gate refuses. The app is the host's, never the body's. Production reports are kept in `app_vital` (`src/apps/vitals.ts`, TTFB, load, LCP, INP and CLS, a visitor hashed under the day's salt, no address, kept 90 days and removed with the app, at most 20,000 rows per app a day); previews store nothing. Only Do-Not-Track opts a visitor out on an app's host: the `forge_measure=off` cookie is set on the forge's host and never reaches an app's. Read the numbers (visitors today and over 7 days, p75 TTFB, load, LCP, INP and CLS) on the app's settings page under **Visitors and speed**, or as `vitals` in an admin's `GET /api/v1/repos/:owner/:name/app`. Both doors show them only to people who administer the repository (`appVisitors` in `src/actions/apps.ts`), so a reader or an anonymous caller sees no `vitals`. The page also draws p75 load for each of the last 30 days as a sparkline (server-rendered, no script); a day with no reports is a gap in the line, not a zero, and an app with none in 30 days says so in words. The same 30 values, oldest first with `null` for a silent day, are `vitals.loadDays` in the API. In the terminal all five are on the second line of the app's **Deploys** screen over ssh, and a third line there shows the 30 days as block characters with `·` for a silent day, then the range.
280
281 The `app_vital` board source (`src/boards/sources.ts`) charts those reports: one row per app per day with `app` (the app's id), `day`, `load_p75`, `inp_p75` (ms) and `reports` (rows kept that day), the percentiles nearest-rank per app per day as `reach` does. `max` or `avg` over several days summarises daily percentiles, not the percentile of the whole range. Its rows belong to the app's repository, so a query sees only the repos in its scope. Today boards query with `repoIds: "all"` (`src/web/pages/boards.ts`) and only a site admin reaches them; no non-admin scope exists, so nothing yet applies the settings page's `canAdmin` gate per repository, and whoever builds one must. Days older than the 90-day trim are gone, not zero. The slug and the visitor hash are not columns. No dependency was added. Left out: a starting board for it; pick `app_vital` in the `/boards` source picker.
282
283 ## The board's notes
284
285 `/board` opens on its panels. Everything that is not a panel — the forge's
286 health (admins), a planner that declined and waits for you, agent pulls that
287 wait for your review — is folded into one quiet line under the title, "3 notes
288 · 2 need you". It is closed by default; click it (or Tab to it and press
289 Enter) to open the list in place, and it stays open across live updates. A
290 declined planner has its answer box right in its note. The crew's own
291 housekeeping (an issue parked twice) is not a note; it is on `/map`. The
292 terminal's first screen says the declined and review-wait notes too, one line
293 each, under the forge strip; a list cut to three says "+N more".
294 The line is the shared `.quiet-details` disclosure, and the "need you" count
295 carries the attention dot.
296
297 A fork ahead of its parent offers its commits upstream: on its home a signed-in reader of the parent sees "Offer N commits upstream" (into the new-pull form with both branches chosen) or "Offered in #M" once a pull is open; a writer who has the Following card gets its "Propose upstream" instead, one route not two. In the terminal the fork's About card (`I`) has an `offer` row and `o` opens the same pull form (or the open pull). Both read `upstreamOffer` (`src/actions/offer_upstream.ts`), the open pull by one SQL lookup.
298
299 ## Long threads
300
301 An issue or pull page renders the opening post and its newest 50 comments
302 (`THREAD_WINDOW`, `src/core/issues.ts`) with the state-change lines between
303 them; above them a plain link, `Show N earlier items`, goes to the same page
304 with `?all=1` (other query parts kept), which renders everything, with no
305 JavaScript. N counts state-change lines as well as comments. A comment's own
306 anchor is `#c-<id>`; links the forge writes to a comment (the feed's replies)
307 go through `commentAnchor()`, which counts comments the way the page cuts
308 them, and carry `?all=1` when the comment is folded away, so they always
309 land. A hand-typed `#c-…` into the folded part cannot be seen by the server
310 (the fragment never leaves the browser), so it needs `?all=1` too. The live
311 region still appends new comments at the bottom.
312
313 The terminal's issue and pull screens stay whole on purpose: they are a
314 scrolling text view of one thread already in memory, not a page a phone
315 downloads, so there is no weight to cut and no second key to learn.
316
317 ## Live in the terminal
318
319 `ssh` into the forge and you get the whole thing — repositories, code, issues,
320 pull requests with a real diff viewer, review comments, merges, settings,
321 tokens and keys — as a keyboard-driven terminal app. The same port serves git.
322
323 ```sh
324 ssh -p 2222 localhost # the terminal UI
325 git clone ssh://localhost:2222/you/hello.git # git, same port
326 ssh -p 2222 localhost tell make the header blue # tell the crew in one line
327 ```
328
329 `tell <words>` takes the rest of the command line as given (no shell expansion; control characters are refused) and tells the crew in the forge's own repository, as the account the key belongs to, through the same action as the web composer. It prints the told line and the full address of the issue, and exits 0; an unknown key, nothing said, too much, or an unverified email prints the reason on stderr, records nothing and exits 1. (Quote the words if your local shell would expand them.) When SSH is on, `/llms.txt` and `/api/v1/here` (the `ssh` field of the "tell the crew" action) name this exact command, so an agent finds it without being taught; with SSH off they say nothing about it.
330
331 On an app's login, `ssh -p 2222 notes@localhost tell make the header blue` goes through the app terminal's gate (an app you may not enter answers `no app named notes`, and nothing is recorded) and tells the crew of the app's repository, with the app's production address as where it was said: the issue body and `/asked` show `Told from <address>`.
332
333 An app's open pulls, newest five first, one line each (`#n title · green|red|running|none`), need no tty: run `pulls` on the app's login — `ssh <app>@<forge> pulls`, with the app's page printing the exact command as `FORGE_APP_SSH` (the terminal is off when that is unset). A private app answers only a key its gate admits; sealed pulls show to their author, writers and the crew (`src/apps/door_pulls.ts`).
334
335 The app's `/__forge/` door on its own host lists the same five pulls with their check word (`#n title · check green`). A running, asleep or dormant preview links to itself, any other pull to its page on the forge. Who sees which is the rule in `src/apps/door_pulls.ts`; a stranger at a private app gets the same 404 as for an unknown app. When more than five are open the door says so in one line; the rest are on the forge.
336
337 A big repository clones over either transport. To skip its large files until a
338 checkout needs them, run `git clone --filter=blob:none <url>`. See
339 [`docs/GIT.md`](docs/GIT.md).
340
341 **After a GitHub migration** you choose what happens to the original: leave it
342 alone (the default), archive it, or keep it as a read-only mirror. The choice is
343 the "Afterwards" field on `/migrate`, in the terminal's migrate screen, and
344 `after` (`leave`, `archive`, `mirror`) on `POST /api/v1/migrate`. Archive marks
345 the GitHub repository archived using the account you connected. Mirror leaves
346 GitHub untouched at first, then copies later changes on the branches GitHub had
347 at migration back to it; branches made afterwards, such as the crew's work
348 branches and pull heads, stay on the forge. If someone pushes to GitHub
349 meanwhile, the forge stops rather than overwrite them, and the repository's
350 settings page (and the terminal's status line) says why it stopped and when it
351 last pushed. "Stop mirroring" there, or `U` in the terminal, ends it and leaves
352 GitHub as it is. Archiving needs admin
353 rights on GitHub: without them the migration still succeeds and says the
354 repository was not archived, so you can do it there yourself. Only repositories
355 read through a connected origin have an original to change; a pasted URL does not.
356
357 **Migrate from the terminal.** Press `M` on the dashboard (or pick "Migrate a
358 repository" in the palette, `ctrl+k`). The list is your connected origins'
359 repositories, private ones marked; `/` filters it and `u` pastes a URL instead.
360 Enter on a repository lists its branches with their last commit, your own
361 newest first and marked "(suggested)", so Enter again takes it; a pasted URL
362 skips this step. The form asks for name, owner, visibility, agent, autonomy and afterwards
363 (leave the original alone, archive it, or keep it as a mirror; `tab` moves, `space` changes); `ctrl+d` migrates. The job is a checklist of
364 stages: `esc` hides it and never cancels, and a toast says when it lands, on the
365 new repository. It is the same `migrateRepo` as `/migrate` and
366 `POST /api/v1/migrate`: the chosen `branch` becomes the default branch and the
367 crew's brief says the app lives on it. The web form takes it too (it lists the
368 picked repository's branches newest first), as does the API's `branch` field.
369
370 A GitHub migration also carries each milestone (title, description, due date,
371 open or closed) as an issue here, with the due date and GitHub's milestone
372 number kept in `milestone_origin`; carrying again makes no duplicates. Which
373 issues belong to a milestone is not carried yet.
374
375 On a forge made for a client (one that was spawned, signed in by its claim
376 link), the home is their apps, one plain sentence each and one more saying
377 whether its agents are working, about to start, or waiting: no map, board or
378 settings until asked. `/?all=1` is the whole home, and its "Just my apps" link
379 comes back. Each app row also says what its agents are on ("Working on: …")
380 and what was last finished on it ("Finished 3 days ago: …", its newest merged
381 pull however old, read for all rows in one query); an app with neither says
382 nothing more. Both doors show both lines: the terminal's first screen opens
383 with the same lines, worded by the same helpers (`src/core/client-apps.ts`).
384
385 **Reading the home as the eye does (#5735, step 1).** `bun scripts/home-read.ts <url>` fetches a page and prints its visible text, then one line: `chars N · words N · fetched in N ms`. Set `FORGE_COOKIE` (for example `forge_session=…; forge_mode=studio`) to read it as the owner; the cookie is sent and never printed. It exits non-zero on any status but 200. The conversion is `pageText` in `src/web/page_text.ts`: scripts, styles, templates, the head and anything `hidden` or `aria-hidden="true"` are dropped, link text stays, blocks sit on their own lines, entities are decoded. The method then continues: (2) a fresh agent with no context reads only that text and answers in at most 150 words, (3) the steward scores it against ground truth on coverage of the 5 things that mattered, errors, and seconds, (4) repeat after every feed change. The steward runs steps 2–4 and logs them on #5735.
386
387 **Scoring the home (#5805).** `bun scripts/home-eval.ts <url>` is the deterministic half of that loop: it reads `/` with the same text conversion, builds ground truth from the forge's own API (`GET /api/v1/telemetry/home` for landings, the live version, builders and why idle, spend against the ceiling and what waits on the owner; the repository list and each newest commit for repositories quiet 3 days), and prints each of the five facts as `stated`, `missing` or `wrong` with the page's words, then coverage, errors, characters and fetch time. The method and a table of results, one row per feed pull, are in `docs/HOME-EVAL.md`. No dependency was added.
388
389 Under the figures, an admin's home, the terminal's dashboard and its Flow dialog carry one vitals line (#5935, `src/flow/vitals.ts`): open pulls, opened and landed in 1 h and 6 h, and conflicted, against #5924's targets; the autonomy tick line says the same as `pulls` (and `offTarget` when a target is missed). The MCP `flow` tool and `GET /api/v1/telemetry/flow` carry it as `vitals: { says, misses, … }`; the tool's description is unchanged because `tools/list` is at its size ceiling, so an agent finds `vitals` in the result, not the listing. An agent reader's counts cover public repositories only.
390
391 Home shows nine figures first, from `src/flow/kpis.ts`: shipped per hour (with
392 a 24 h sparkline), the live version and since when, the median check time, the crew builders
393 running of their slots, and the crew's spend today of the forge's daily
394 ceiling (`FORGE_CREW_CEILING_USD`; without one, the spend alone and
395 `no ceiling`), and the age of the last off-box backup (the newer of the
396 mirror's newest snapshot, `mirror/state.json`, and the newest that landed in
397 a destination that is a bucket, `snapshot/state.json`; `src/snapshot/off_box.ts`; `3 h ago`, the exact time in its title), linking to
398 `/settings/backups`. The spend tile links to `/settings/crew`, where the meter is.
399 The backup tile past 24 hours, or with none (`never`), takes the card's existing
400 `data-tone="attn"` and says overdue in its label.
401 The last three (`src/flow/reach.ts`, the `reach` board source's rows) are
402 visitors (`5 today`, `31 in 7 days`), accounts (`1 of 2`, signed in today) and
403 page speed (the slowest quarter's page load over 7 days, and `reacts in 180 ms`,
404 the same for the delay after a tap), from the page-view beacon. Each links to
405 the Reach starting board (`/boards/reach`). The visitor hash rotates daily, so
406 the 7-day visitors is each day's distinct count summed.
407 They count every repository, so only a site admin sees them, on both doors;
408 nobody else does, and neither does the lean client home. On the web each is a
409 card linking to `/board`, `/releases`, `/boards` or `/boards/reach`. The terminal's first screen
410 shows the same nine as lines above the forge strip (`kpiLines` in
411 `src/flow/kpi_words.ts`). They are read when the page or screen loads and
412 do not refresh on their own.
413
414 On a host that cannot hold cells or the crew's shell sandbox, a site admin
415 sees `Reduced protection: …` in one line, under the filter on the terminal's
416 first screen and in the web footer; nobody else does, and a fully protected
417 forge shows nothing.
418
419 A new version proves itself once per version: after a boot that changes
420 `FORGE_VERSION`, the forge waits (up to 10 minutes) for its public URL to
421 answer `/healthz` as that version, then runs the smoke checks (`src/release/smoke.ts`) against it and
422 keeps the verdict in `$FORGE_APP_DATA/release/proof.json`. A failure is a
423 `Release X is unproven: …` card on an admin's board and on the terminal's first
424 screen (the same cards), and is paged once; a later passing version clears it.
425
426 **The outsider suite** (#663) is the proof a person reads before flipping signup
427 to open. It adds the proof only: it removes no gate and reopens nothing.
428 `signup-gate.yaml` and `outsider-gate.yaml` in `trevato-apps/gitops` stay up
429 until a person deletes them there.
430
431 It tries what a stranger must not do through all the doors a stranger has:
432
433 - **HTTP API and web form** (`src/release/outsider.ts`): an admin token, a
434 repository or app of their own, any read of the owner's private
435 repositories, and assigning a crew member on a real issue — the JSON
436 `PUT …/issues/:n/assignee` with a token, and the form `POST
437 /:owner/:name/issues/:n/assignee` with the cookie session a browser would
438 hold. A refusal is positively seen: a 403 with its error code, a 404 only for
439 private repositories, or a redirect whose landing page says so. The issue is
440 read back and its assignee and the repository's runs must not have moved,
441 and the owner's own call to the assignee route must answer 422 so a rename
442 turns the suite red.
443 - **MCP** (same module): `tell`, `make_it_yours`, `signup`, and reads of
444 private repositories through `repo`, `usage` and `apps`, each required to
445 answer as an error; `tools/list` must still name them.
446 - **Terminal** (`test/helpers/outsider-tty.ts`, run by
447 `test/outsider-tty.test.ts`): the stranger drives the terminal in process,
448 asks for a new repository, searches for a private one and opens the assign
449 picker on a public issue; the screen must show the refusal and nothing may
450 change. The terminal is a client of the forge's actions, not of HTTP, so it
451 cannot be driven against a live forge: it runs in the gate only.
452
453 `test/outsider-suite.test.ts` and `test/outsider-tty.test.ts` run in the gate
454 and each prove the suite goes red with a door left open.
455 `FORGE_TOKEN=… bun scripts/smoke.ts <url> --outsider` runs the HTTP and MCP
456 doors against a live forge. **It makes a real account on that forge**, so
457 point it only where that is wanted. Its result is valid for reopening signup
458 only together with a green gate run of the terminal test; HTTP and MCP green
459 alone do not cover the terminal door. Without `FORGE_TOKEN`, `--outsider`
460 exits 2 and the suite reports `owner token` as not tried, so a partial run is
461 never read as a pass. Spend and run bounds are proven in process only; this
462 pull adds the proof but does not reopen signup — the gitops gate stays for a
463 person.
464
465 `bun scripts/smoke.ts <url> --standards` prints the website-specification
466 report (`src/release/webspec.ts`, `webspec-site.ts`): it fetches each path in
467 forge.json's `app.looks` from `<url>` and prints one line per finding with the
468 specification.website section it cites. Head and header findings come per
469 path. Once per site it also checks the well-known files (robots.txt and the
470 sitemap it names, with each listed same-origin URL, at most 50, fetched
471 anonymously; `/.well-known/security.txt` with a future `Expires:`; the web
472 manifest with a name and icons; `/llms.txt`; `/.well-known/change-password`
473 redirecting) and the error pages (a random path must answer 404 with the app's
474 own HTML page). `--error-path <path>` names a path that is meant to answer 500
475 with the custom page; the 500 check runs only when it is named, because the
476 report never causes an error on its own. It only reports and always exits 0.
477 `(edge)` marks a finding the edge in front of the app may own (HSTS), so fix it
478 there rather than in the app. Each fetch gives up after 10 seconds.
479
480 `--standards` also runs axe-core (`src/release/axe.ts`) over each path in light
481 and dark and loads each with JavaScript off, reporting an empty `<main>` or a
482 missing `<h1>`; it needs `chromium` on `PATH` or `FORGE_CHROMIUM` (without one
483 it falls back to a regex read of the served HTML). axe-core is a devDependency
484 of this check only, never loaded by the server: its rules need a rendered page
485 and an accessibility tree, which nothing dependency-free gives.
486
487 What you asked for: `Ctrl+K`, then **Asked**, is `/asked` in the terminal — your
488 thoughts, then the issues you can write to that are not shipped, oldest first
489 (`j`/`k` move, `enter` gives the issue's web address, `r` refreshes). It is read
490 only, shows a guest nothing, and names nothing of a placement in a repository
491 you cannot read.
492
493 Review: `/review` lists the asks waiting on you, one row each (title, repository,
494 age, why, and the crew's advice or that it has not advised yet), newest first. A pull's row also carries the check verdict for its head (the pull page's badge: passed, failed, running, cancelled, or none) in the row's trail, and, when its repository is an app, an "Open the preview" link under it (shown for any app, even before a preview is deployed); and, when the pull has a look the viewer may fetch, its preview thumbnail beside production's under that (sized, lazy, never inside the row's link; the pull's is the only link, named for a screen reader, and production's is a named picture; a viewer who may not fetch it sees none); all of it comes from the database, never git. The row's trail also says the pull's size (`3 files changed, +40 −7`), kept per base and head commit (`src/web/studio/review_evidence.ts`); a request never waits on git: until the stat is kept the row says "Diff loading — reload for the size" while the app computes it in the background (at most 4 at once, stopped by closing the app), and a failed stat says "Diff size unavailable" and is not asked again for a minute, so a bad pull cannot spawn git on every visit. Visits while a stat is running share it. Each row also carries its actions under it, as plain form POSTs to `/review/act` (same-origin checked, `rw` scope): on a pull **Merge** (through the same landing gate as anywhere, so a red or unreviewed head is refused with the gate's sentence), **Close**, **Request changes** (with a text box) and **Reply** (with a text box); on an issue **Close** and **Reply**. After an action the page answers 303 to `/review#<the next ask>` (the first that followed it and still waits, else the first left, else `/review`), and the acted ask is gone; a refusal comes back to the same row with its reason. Keys: `j`/`k` move between rows, and on the focused row `m` merges, `c` closes and `r` jumps to the Request-changes box; keys are ignored while a text box has focus, and `?` lists them. `/asks` links to it, and signed out it sends you to sign-in. The terminal has no page of its
495 own for it: the same asks are the dashboard's On you card (acting from the terminal is tracked in #5189). The terminal has no page of its
496 own for it: the same asks are the dashboard's On you card.
497
498 A client's forge: `Ctrl+K`, then **New forge** (any signed-in person's, as the
499 page is), is `/forges/new` in the terminal — name, for whom, what it is for,
500 apps to fork (`owner/name`, comma-separated), a fresh one-line app, model key
501 (hidden as typed), API base URL, budget, how it follows the code and a size (`small` by default, not sent; `medium` or `large` is; left, right or space choose), through the same `spawnDoor` as the
502 page, `POST /api/v1/forges` and the MCP `spawn`. `Ctrl+P` shows the plan and
503 makes nothing (it shows a Size line when one is chosen); `enter` on the plan spawns, and the claim link is shown once, in
504 a dialog, never kept.
505
506 The family across forges: `:` → Family in the terminal, `/family` on the web,
507 `GET /api/v1/family` and the MCP `family` tool all give the same view. The
508 forge that sent this one is shown to anyone; the forges it sent, only to a
509 site admin (docs/SPAWN.md, "The family across forges"). Each sent forge also
510 says how many of its apps run and names those that do not ("1 of 2 apps
511 running — bakery asleep").
512
513 Passkeys: Settings → Passkeys lists, adds and removes them (several per
514 account; `src/auth/webauthn.ts` verifies, no dependency, `none` attestation).
515 The sign-in page's "Sign in with a passkey" gives the same HttpOnly session a
516 password does. Lost your device? "Lost your device?" on the sign-in page leads
517 to `/recover`: give the email on your account and, if it is verified, a link
518 that lasts 30 minutes and works once registers a new passkey and signs you in.
519 The relying party is the address the browser used (or
520 `FORGE_URL`), so a passkey does not follow a forge to another hostname.
521 Sign-up is a username and a password of at least 12 characters (`/signup`, `POST /api/v1/users` with `{username, password, email?}`, the terminal), hashed with argon2id; the email is optional and, when given, gets the verify link (an account with none has nothing to prove). A passkey (`src/web/pages/signup-passkey.ts`, which does take an email) is offered beside the password. Nothing in the path touches an edge, parent or Forgejo account. An operator can still refuse password sign-ups with `passkeyOnlySignup`. The owner claim of a spawned forge (`/claim`, `web/pages/claim.ts`) takes the same: a username and a password with the email optional, or a passkey (which needs an email; challenges from `POST /claim/options`), and a claim with neither a long enough password nor a passkey is refused without spending the link. An admin's everyday account (`web/pages/everyday.ts`) still makes its account with a passkey only (`web/passkey-ceremony.ts`, challenges from `POST /settings/everyday/options`): it reads no password, and without JavaScript the page says a passkey needs it.
522
523 Password checks are bounded (`src/core/hash-gate.ts`): at most 4 argon2 verifications run at once, and a password over 1024 bytes is never hashed and cannot be set (sign-up, claim and change-password refuse it with "Use 1024 bytes or fewer."). A check over the cap is refused at once, not queued: the caller sees the same `throttled` answer as a full sign-in window (429 with `Retry-After: 1` on HTTP Basic, the API and the sign-in page, "try again" in the terminal; an over-long Basic password is a plain 401), and the attempt is given back to the sign-in windows. The 4-hash cap is a deliberate trade-off: an attacker who keeps 4 hashes busy can make real sign-ins get "try again in 1s"; the per-source sign-in window is what limits how long they can.
524
525 A forge with a parent (`FORGE_PARENT_URL`, or the `upstream` in a spawned
526 forge's `parent.json`) lists the parent under "Link an account you have" on `/signin`. The forge
527 registers itself once at the parent's `/oauth/register` (the client id is kept
528 in `rp_client`), sends the person through the parent's authorize page with
529 PKCE, and on return checks state (also bound to the browser by an HttpOnly
530 cookie), nonce, `iss`, `aud`, `exp` and the ES256 signature against the
531 parent's JWKS (`kernel/jwt.ts`; keys are cached ten minutes and an unknown
532 `kid` refetches them once a minute at most). The parent's `sub` — never the
533 username — maps to a local account that is already linked to it; a `sub` nobody
534 holds makes no user and ends on `/signup` (#2883)
535 (`core/federation.ts`; `http/rp.ts` has the two routes). No dependency added.
536 The parent's answers are read by bytes and refused past 16 KiB (64 KiB for the
537 key set), and starting a sign-in is throttled like a password attempt.
538 An account made this way has no password, so the terminal's username-and-password
539 door (`s`) cannot let that person in. They add an SSH key under Settings → Keys
540 once signed in on the web; after that the terminal signs them straight in as
541 that account, as for anyone with a key.
542
543 Your SSH key is your identity. A key the forge knows signs you straight in; any
544 other key browses public repos as a guest and can press `s` to sign in with a
545 username and password — which links that key to your account, so the next
546 connection is instant. `j`/`k` to move, `enter` to open, `esc` to go back, `/`
547 to filter, `?` for the keys, `:` for the command palette, `t` for the theme.
548
549 **Milestones.** `:` → *Milestones in a repository* → `owner/name` lists a
550 repository's limbs in the order the crew takes ready work (`enter` opens a
551 limb's own limbs). A person who administers the repository presses `r` to
552 Raise or `l` to Lower the limb under the cursor: a preview of the new order
553 and of the top of the ready list first, then `enter` to apply (`esc` backs
554 out), then `u` to undo the last move. `p` hands the limb to the planner to
555 re-plan as fewer, larger pieces: it lists what the planner will be given, then
556 `enter` confirms; the row reads *with the planner*, and `p` takes it back
557 until the planner starts. It is the same fleet operation the web
558 milestones page uses, and a move is heard by that page when it is open.
559 Anyone else sees the order and no keys to change it; a guest sees only what
560 the web shows them. Whoever may see owner facts also gets two lines under
561 the list for the limb under the cursor: **Cost and landed** (dollars spent,
562 changes landed) and its costliest three issues; a reader or guest sees none.
563 The web page has the same *Cost and landed* section on a milestone, and the
564 milestones API and MCP carry it as `spend` (`usd`, `landed`, `costliest`),
565 owner only.
566
567 The milestones page's `Server-Timing` names what a plan cost when it had to be
568 built: `plan-graph`, `plan-crew`, `plan-week`, `plan-spend` and `plan-limbs`. A
569 plan kept from an earlier request builds nothing, so those names appear on the
570 request that built it and not on the ones it served; `plan` is the whole phase,
571 which on a kept plan is the one stamp read that says nothing changed. That stamp
572 is itself kept while the forge's write clock has not moved.
573
574 Working on the UI itself? `bun run src/tui/main.ts --user alice` runs it over
575 stdio against your local data directory. That `--user` flag is a development
576 convenience and exists only there: over SSH, the key decides who you are and
577 nothing else can.
578
579 Configuration is environment variables, all with defaults: `FORGE_DATA`
580 (`data/`; the forge refuses to boot when it is the checkout itself or a path
581 inside it that git does not ignore), `PORT` (`3000`), `FORGE_SSH_PORT` (`2222`; `0` turns SSH off), and
582 `FORGE_HOST` (`localhost`, the hostname in clone URLs). `FORGE_DRAIN_LINGER_S`
583 (`10`, 0 to 120) is how long a forge sent SIGTERM keeps answering while
584 routers learn it is going, so a roll refuses nobody (`docs/ops/ROLL.md`).
585 Connecting origins
586 adds `FORGE_URL` (the canonical public base URL, needed for OAuth callbacks),
587 `FORGE_GITHUB_CLIENT_ID` / `FORGE_GITHUB_CLIENT_SECRET`, and
588 `FORGE_ORIGIN_ALLOW` (hosts an operator vouches for on a LAN). Behind a
589 platform edge that signs people in for every app it serves, `FORGE_EDGE_TOKEN`
590 is the edge's secret: a request carrying it and `X-Plat-User` is that person,
591 signed in by the platform if an account of that name exists. An unknown login
592 makes none: it is sent to `/signup`, where the `signup` setting decides, and
593 the name there is pinned to the login the edge vouched for (a visitor the edge
594 has not vouched for is sent through it first), so a platform user's name can
595 never be taken by someone else. The terminal cannot see `X-Plat-User`, so
596 behind an edge sign-up is on the web page only and the terminal says so. Setting
597 `signin.edge` to `off` (Settings → Capacity → "Sign in through the platform
598 edge", boot default `FORGE_SIGNIN_EDGE`, also `/api/v1/admin/capacity` and the
599 MCP `capacity` tool) makes the forge ignore `X-Plat-User` even when the secret
600 matches; people then sign in with a password or a passkey. An admin with
601 neither is refused, so no one locks themselves out. Accounts the edge made
602 cannot sign in while it is off until they add a passkey (a password-less
603 account cannot set a password: `/settings/password` asks for the current one).
604 Account settings show "open-platform.sh: linked as <login>" or "not linked": Link
605 records the login the request carries against the signed-in account, Unlink removes
606 it (same-origin POSTs). An edge sign-in finds the account by its link first, then by
607 name; one login links to one account, and a login that is another account's name
608 cannot be linked. The terminal's "What I can do" screen shows the same line and `u` unlinks (SSH carries no edge header, so linking is on the web page only).
609 On a forge with a parent, settings also show "<parent host>: linked" or "not linked". Link is a same-origin POST that runs the `/signin/parent` PKCE round trip for the signed-in account and, on return, writes its `federated_identity` row (never an account); an identity another account holds, or a second identity for this account, is refused. Unlink removes it. The terminal's "What I can do" shows the line and `p` unlinks; linking needs a browser.
610 Mail out
611 (the link that proves a newcomer's email) is SMTP submission over TLS,
612 configured by the forge-level secrets `MAIL_SMTP_URL`
613 (`smtps://user:pass@host:465`) and `MAIL_FROM`, set live under Settings →
614 Secrets, or at boot by `FORGE_SMTP_URL` / `FORGE_MAIL_FROM`. With neither, no
615 mail is sent and a newcomer waits for the owner to verify them. With
616 `FORGE_TURNSTILE_SECRET` and `FORGE_TURNSTILE_SITEKEY` set (the forge refuses to
617 boot with only the secret), `/signup` renders the Turnstile widget, its CSP
618 allows `https://challenges.cloudflare.com`, and signup asks Cloudflare's
619 siteverify about the posted `cf-turnstile-response` (API: `turnstile_token`),
620 refusing a missing or bad token before it hashes a password or makes an
621 account. Unset, signup is open. The terminal (SSH) cannot show a widget, so
622 with Turnstile on it creates no account, the forge's first neither; everyone
623 is told to sign up on the web page. Accounts made through the platform edge are not checked. Behind a
624 proxy, `FORGE_TRUSTED_PROXIES` (comma-separated CIDR ranges, e.g.
625 `10.42.0.0/16` for a k3s ingress) names the proxies allowed to say who the
626 client is; limits for strangers and the signup audit then see each outside
627 client, read from `X-Forwarded-For` right to left past the trusted hops, or
628 from `FORGE_CLIENT_IP_HEADER` when the edge sets one (`cf-connecting-ip`
629 behind a Cloudflare tunnel, where the ingress rewrites `X-Forwarded-For`;
630 read only from a listed proxy). Once set, only the listed ranges are proxies:
631 this machine is not one unless listed, since every app runs beside the forge.
632 Unset, only this machine may say so, and the named header is never read. Everything a forge
633 instance _is_ lives under the data directory — the SQLite index, the bare
634 repos, and the SSH host key, in plain layout. Tar it up and it moves.
635
636 `/forges/new`, `POST /api/v1/forges` and the MCP `spawn` make a client's forge from forks of the apps you pick, plus optionally one fresh app from a one-line description (`fresh`, at most 200 characters), and end in one claim link; see `docs/SPAWN.md`.
637
638 `FORGE_PARENT_URL` names the forge this one was spawned from. It must be
639 `https://` with no credentials or query (the forge refuses to boot otherwise).
640 The parent serves `/releases.json` as `{"releases":[{"name","url"}]}`, newest
641 first; the child reads it with a 3 s timeout, no redirects and a 256 KB cap,
642 caches it under `FORGE_APP_DATA` for 10 minutes, and says "N releases behind
643 <parent>" on a fork's following card, in the terminal's About card (`I`), and as `parentForge`
644 (`{host, behind, releases, fetchedAt, down}`, null when unset) in
645 `GET /api/v1/repos/:owner/:name/following` and the MCP `following` tool. A
646 page never waits for the parent: it shows the cached copy and refreshes in the
647 background, and a parent that is down or answering garbage leaves a "could not
648 be reached" with the age of the last copy. Unset, nothing is shown. Read only:
649 taking a release is not done here. This forge publishes its own
650 `/releases.json` (the version names, each linking to that document), so any
651 forge can be a parent. The terminal shows the count, not the release names.
652
653 A spawned forge's own code is a fork of its parent's (`<org>/forge`, chosen
654 `followMode` `tell-me` or `take-updates`; in `take-updates` the hourly check puts what the parent gained in a pull.
655 A spawn may also name a `size`: `small`, `medium` or `large` (`src/spawn/sizes.ts` maps each to CPU, memory and disk). It is recorded in the plan only when given, so a plan without one hashes as it always did, and an unknown size is a 422 on `size`. The New forge page and the terminal form both offer it, small preselected and not sent; the page shows a refusal beside the select, linked with `aria-describedby`. Nothing enforces the limits yet, and the table's numbers are provisional. `GET /api/v1/forges/sizes` (no sign-in, ETag, cached for 5 minutes) and the MCP tool `sizes` give the same answer — each size's CPU, memory and disk, read from that table, and one sentence on what it is for — so a page or an agent offering a size reads it there.
656 Once an hour, and
657 on demand, a forge that follows and has not severed fetches its parent's code into
658 `refs/upstream/*` of its own `<org>/forge` (no branch of the fork moves) and keeps
659 `{ behind, parentTip, checkedAt, error }` in `upstream.json` under `FORGE_APP_DATA`;
660 the count shows on the chain page, in the terminal's **Chain** (`c` checks now), and as
661 `upstream` in the API and MCP payloads. `POST /:owner/forge/following/check` (a writer,
662 same-origin) checks on demand; a fetch is cut at 60 s and a failure is recorded, not
663 thrown. In `take-updates` the check then puts `refs/upstream/HEAD` (a fast-forward, or a clean merge commit) on the
664 fork's `forge/take-updates` branch and opens one pull, "Take updates from <parent>", into main, or updates
665 that pull when it is open (`spawn/take.ts`); the fork's check runs on it like on any pull and a person merges it.
666 Main is never moved, so nothing deploys by itself. It is recorded as `taken` in `upstream.json` (the chain line
667 reads "N commits from your parent are in pull #P") and as a `forge.updates_taken` event; a pull closed unmerged
668 is not reopened until the parent moves again. `forge/take-updates` belongs to the check: a commit a person pushes there is overwritten by the next check that finds something different to put on it (open a branch of your own to build on it). On a conflict nothing changes and one open issue,
669 "Take updates from <parent>", names the files. In `tell-me`, a new parent tip makes one inbox card for the owner (an issue on the fork assigned to them, "Your parent forge has N new commits", linking the chain page); `toldTip` in `upstream.json` keeps it to one per tip, and `take-updates` or a severed link makes none. Its chain
670 to the root is at `/:owner/forge/following`, in `chain` of `GET
671 /api/v1/repos/:owner/forge/following` and the MCP `following` tool, and in the
672 terminal's **Chain** command; the owner severs it with the page's Sever,
673 `POST …/following/sever` or the MCP `sever` tool (nothing is deleted). Taking
674 updates into a conflict opens the converge issue at every door. See
675 `docs/SPAWN.md` and `docs/FOLLOWING.md`.
676
677 ## Who can see it
678
679 Every repository page says, beside the Private/Public badge, who can see it in
680 one sentence ("Private: only you and the crew."), and a writer's click goes to
681 Settings. `GET /api/v1/repos/:owner/:name` returns the same words as
682 `audience: { level, sentence }`, the MCP `repo` tool carries it too, and the
683 terminal's About card (`I`) shows it. It gives counts, never names; site
684 admins are not counted. All of them read `audienceFor` in `src/core/audience.ts`.
685
686 ## Archive
687
688 An admin puts a repository away from **Settings → Archive**, with
689 `PATCH /api/v1/repos/:owner/:name {"archived":true|false}` (admins only; anyone
690 else gets 403), or in the terminal with `a` on the About card (`I`). The
691 repository page and the card then say when it was archived, and an admin gets
692 a Wake button or the same key to undo it. Archiving twice keeps the first date.
693 An archived repository starts no new crew run. Assigning an issue to an agent,
694 queueing a run again, asking the crew to propose a resolution, redirecting a
695 wave and converging a fork each answer "This repository is archived; wake it to
696 start a run."; the API answers 409 `archived`, and the terminal and MCP show
697 the same line. The autonomy tick skips it. A run already going finishes, and
698 Wake undoes all of it. Clone, push and links keep working, and the repository
699 page still answers.
700
701 An archived repository leaves the web home's apps, the inbox (work assigned to
702 you there included, and the terminal's and MCP's asks), the stalled-steps
703 reader and the explorer's default view; `/explore` shows it with `archived:yes`
704 (the **Show archived** toggle). It stays in `/apps`, search, the palette and
705 the repository API (`GET /api/v1/repos`).
706
707 `/apps` lists live (unarchived) apps only, with an "N archived" line linking to
708 `/apps?archived=1`, which lists just the archived ones (no All/Live filters, one
709 "Back to apps" link); each view reads its own window of 200 repositories. In
710 the SSH terminal the dashboard's *Yours* leaves archived repositories out too;
711 type in its filter (`/`) to reach them, as the filter searches every
712 repository you can see.
713
714 A site admin archives many at once from the home. In the **Your apps** card,
715 **Select** shows a checkbox per app (they take no room until then); tick some
716 and press **Archive**. This is the fleet's `archive` operation over those
717 repositories (`POST /home/archive`, same-origin, a site admin's), so it has the
718 fleet's receipt: the page that answers lists what was archived and an **Undo**
719 button for the person who ran it (kept for a day), after which the apps are back
720 on the home. The terminal's dashboard archives and undoes the same way (below), and so does the MCP `archive` tool given `repos` (at most 50; it answers the `operation` id, which `limb` with `action: "undo"` reverts for a day); each door refreshes an open web home (the `fleet.operated` event).
721
722 ## Where it came from
723
724 A repository brought here with **Migrate** (`/migrate`) keeps where it came
725 from as links, never as a connection: the source's address, and — when its host
726 says it is a fork — what it forks (GitHub's `parent` and `source`, Forgejo's and
727 Gitea's `parent`; a pasted address's host is asked once, anonymously, after the
728 clone). The repository header then reads "Migrated from
729 github.com/Trevato/SO-ARM100 · fork of TheRobotStudio/SO-ARM100", each part a
730 link up. A fork made here of a migrated repository inherits its parent's
731 trail and reads "forked from … · migrated from … · fork of …" — at every door
732 below, for anyone who may read the parent; only the parent's admin can hide
733 that inherited part, and a fork's own record, if it has one, wins. The
734 terminal's About card (`I`) shows the same
735 as `migrated from` and `fork of` rows, `GET /api/v1/repos/:owner/:name` as
736 `provenance` `{ source, sourceUrl, parents: [{ fullName, url }], migratedAt }`,
737 and the MCP `repo` tool the same field.
738
739 An admin turns it off with **Settings → Show where this came from**,
740 `PATCH /api/v1/repos/:owner/:name {"showProvenance":false}`, `t` on the About
741 card, or the MCP `trail` tool (dry by default, admin scope). Off hides the line
742 on every page, in the terminal, and `provenance` is `null` in the API and MCP;
743 the record is kept, so on brings it back as it was. A source that was private
744 on its connected origin starts off, whatever it is migrated here as: its name
745 and owner on the other host are not published until an admin turns the line
746 on (a later re-record never overrides that choice). It is a person's call:
747 anyone else, an agent included, is refused (the API answers 404 before any
748 other field in the same request is saved). Repositories migrated from a pasted
749 address before this existed were filled in from their migration's `push`
750 event, without a fork chain; one picked from a connected origin then has none.
751
752 ## Big directories
753
754 A directory page lists its first 300 entries, directories first; a "Show all N entries" row links to `?all=1`, and the footer says "Showing 300 of N entries". The side Files column keeps 100 rows per directory (its lazy `?fragment=explorer` too) with the same link. The terminal's file explorer is a scrolling, lazily loaded tree rather than a page, so it is unchanged. The limits are `TREE_WINDOW` and `COLUMN_WINDOW` in `src/web/pages/explorer.ts`.
755
756 ## Search
757
758 `/search` answers repositories and issues in full, and greps code repository by repository. A first search starts no new `git grep` after 30 ms (`codeBudgetMs` in the forge config; a soft stop, since the grep in flight still finishes, one batch of four) and shows what it has with one link, **More code results** (`?code=all`), that greps the rest. The API and the terminal palette always grep everything.
759
760 ## Bug hunters
761
762 `bun scripts/hunt.ts <origin>` runs the hunters' detectors against one running app (GET only, same-origin, one pass) and prints findings as JSON; it exits 1 when there are any. Lanes and what is not built yet: `docs/HUNT.md`.
763
764 ## Health
765
766 `GET /api/v1/health` (and the MCP `health` tool) answer a site admin with
767 `degraded`: one entry per thing that needs a person, `{cause, sentence,
768 since, href}`. `cause` is a stable key (`disk`, `errors`, `internal`,
769 `queue:<kind>`, `stuck:<runId>`, `budget:<runId>`); `since` is when it began, by the oldest
770 evidence (oldest queued run, last heartbeat, oldest error or refused call in
771 the hour) or else the first time the forge saw it, and holds until the cause
772 clears; `href` is the page to look at. The board's forge strip links each
773 entry, and the terminal dashboard prints each with its start. When the guard
774 view holds something that needs a person (a held or stuck rollout, an OOM kill,
775 or a platform view gone stale), the board's strip and the terminal dashboard
776 both lead with one line, `needs you: forge <what>`, and do not repeat it as a
777 rollout line. A drain that runs too long is not on either; it is a push, not a
778 door: an admin with a notify URL is paged once when a drain passes five
779 minutes and once, by the draining process as it exits, when it ends. A process
780 that dies mid-drain says nothing more.
781
782 The same push pages the vitals causes `errors`, `internal`, `queue:*` and `disk`
783 once each has held five minutes, and says Recovered once it has been gone five
784 (a flap in between is no news). A cause present at boot is taken as already
785 paged. Every other cause (`drill`, `mirror`, `stuck:*`, `red:*`, `snapshots`,
786 `budget:*`) is paged elsewhere or is a setting, and is ignored here.
787
788 ## Origins
789
790 Outbound git refuses a host that resolves to a private address. A site admin can vouch for exact host names (the owner's own Forgejo, reached by its in-cluster address): `FORGE_OUTBOUND_TRUSTED_HOSTS=git.example.test` (comma or space separated), plus the same list editable by an admin on **Settings → Origins**. A trusted host may resolve privately for origin sync, mirror, migrate and write-through; trust is by name, never by address range, and webhooks never read it. The origin row says "trusted internal host". In the terminal, the palette's **Trusted hosts** reads and saves the same list (admins only, same audit). An entry that is not a bare host name (an address, a wildcard, a URL) is refused with its name, on the page and in the terminal, never silently dropped.
791
792 Your forge is the index of everything you work on, wherever it actually lives.
793 An **origin** is a connected account on another forge — GitHub, Forgejo, Gitea.
794 Mirror a repository in and its issues, pulls, comments and labels come with the
795 git objects; anything you then do here is **written through** to the origin
796 first and reflected locally after it succeeds. For a mirror the origin is
797 truth, exactly as git is truth for a native repo.
798
799 **Resolving a conflicted pull.** When a pull conflicts with its base, a writer sees **Resolve** in the "Can't merge" box, which opens `/:owner/:name/pulls/:n/resolve`: one file at a time, each conflict block a card with the two sides named by their branch and the taps **Ours**, **Theirs**, **Both** and **Mine** (words typed in the box count as Mine), a running "2 of 3 resolved", and the merged file beneath. The choices ride in the query string until you press **Finish**, which commits them: the resolved file becomes a merge commit on the pull's branch (as you, the base as second parent, built with git plumbing — no clone, no agent), and the pull re-checks as mergeable. Finish is for writers with `rw` scope and refuses, writing nothing, when the branch or base moved since the page was drawn, when a block has no choice, when the pull has more than one conflicted file or a conflict with no text blocks, and for cross-repo or mirrored pulls. In the terminal, `v` on a conflicted pull does the same: `n`/`p` move over the blocks, `o`/`t`/`b` take ours, theirs or both, `e` opens a box for your own text, each file shows "N of M resolved" with its merged file beneath, and `f` finishes with the same refusals in words. On a conflicted pull held by an agent, the same page (and the same view in the terminal) also offers **Have the crew propose** to writers with `rw` scope: it queues one `resolve` run for that agent, on the pull's own branch, with a brief carrying both sides' commits, the conflict blocks and the tests that cover them. The agent merges the base in, commits the resolution and pushes; its report becomes the note on the pull. Until the run settles the card says it is working and refreshes itself on the run's events (in the terminal, `r` looks again and never asks), and a run that fails says why. When it lands the card shows the note, the commit the run left (kept on the run, so a later push is never called the crew's) and the files to edit by hand; once the head moves on, the card goes. The tap is refused, writing nothing, for a reader, a closed pull, a pull with no conflicts, a pull no agent holds, one the crew is already working on, and when the crew's ceiling or breaker has no room. In the terminal `c` in the conflict view is the only key that asks, with the same refusals in the same words. See docs/PULLS.md. Readers and guests get neither.
800
801 A pull from an outsider's fork on the origin is someone else's code, so its
802 check and preview wait. A person with write access lets that exact head run:
803 on the pull page, **Approve to run here**; in the terminal, `A` on the pull
804 ("approve to run here"); over the API, `POST
805 /api/v1/repos/:o/:n/pulls/:n/approve-here`. It is a local record — a review
806 written through to, or imported from, the origin never counts — and a new push
807 waits for a new approval.
808
809 A pull a child forge proposed upstream waits the same way, and the writer who
810 minted the child's token may let it run: **Let it run here** on the pull page,
811 `A` in the terminal, or the same `POST …/approve-here`, which answers
812 `{granted: true, commitOid}`. That grant is recorded apart from reviews: it
813 never counts toward `require_review` or the merge gate, a new push needs a new
814 grant, and a proposed pull never lands on its own, even on an autonomous repo.
815
816 Connect one at **Settings → Origins** (or `A` then `o` in the terminal). Which
817 way in you get is a property of the forge, not a preference:
818
819 - **GitHub** — a device code. The forge shows a short code and a URL; you type
820 the code on whatever device has a browser, and the forge does the polling.
821 This is the flow that works over SSH with no browser on the server. Setting
822 it up is three steps, once, for everyone on the forge:
823 1. Create an OAuth App at <https://github.com/settings/developers>. Name it
824 anything, set the homepage to your forge's URL, set the authorization
825 callback URL to `https://your.forge/settings/origins/callback`, and tick
826 **Enable Device Flow**.
827 2. Start the forge with `FORGE_GITHUB_CLIENT_ID=<client id>`.
828 `FORGE_GITHUB_CLIENT_SECRET` is optional and buys one thing:
829 disconnecting revokes the grant on GitHub too.
830 3. The GitHub button appears. Until it does, both surfaces print these three
831 steps rather than naming an environment variable and leaving you to it.
832
833 - **Forgejo / Gitea** — authorization code with PKCE. They implement no device
834 grant, and every instance needs its own OAuth application, so you paste a
835 client id once per host — or the operator registers the host once for
836 everyone, `FORGE_ORIGIN_CLIENT_ID=git.example=<client id>` (comma-separated
837 for more), and Connect is one click. The page gives you the exact redirect
838 URI to enter (`https://your.forge/settings/origins/callback`); a public
839 client is fine.
840 This flow needs the forge to know its own public URL, since that URL is
841 registered with the other forge and a `Host` header is not evidence: set
842 **`FORGE_URL`** to the canonical base URL (`FORGE_HOST` and loopback are the
843 fallbacks it trusts). Until one of those is true the card says so, and the
844 token path still works.
845 - **A pasted token** — the fallback that needs no registration anywhere. It is
846 validated against the origin before it is stored, encrypted at rest, and
847 never displayed again.
848
849 Both connect screens hand the job to whatever device has a browser: a **QR
850 code** to point a camera at, a **short link** (`https://your.forge/go/<10
851 characters>`, live for ten minutes and gone once used) short enough to type by
852 hand, and — in the terminal — `c` to put the link on _your_ clipboard through
853 your own terminal (OSC 52, so it works over SSH; iTerm2, Ghostty, kitty and
854 WezTerm honour it, Apple Terminal does not). `o` opens a browser only when the
855 forge's terminal is the one in front of you; over SSH it says so rather than
856 appearing to work.
857
858 Then pick what you want: **Repositories** on an origin lists everything the
859 token can see, and you mirror them one at a time. There is no "mirror
860 everything" — a mirror is a repository you own, and a hundred you did not
861 choose is not a feature.
862
863 A mirrored repo wears its origin in the header (`mirrored from owner/name @
864 host`), shows its last fetch and any fetch error in the About rail, and syncs
865 on a worker tick or on demand. Imported issues and comments show the login on
866 the _origin_, with that forge's mark, and link back to the canonical thread.
867 Merging a pull happens on the origin: the merge box says so and links there,
868 and the next sync brings back the merge commit it actually made. Pushes go
869 through too — the pre-receive hook forwards the push upstream first, so a push
870 the origin refuses never lands here either.
871
872 Mirror fetches are vetted and pinned: the origin's host is resolved once, refused
873 if it is a loopback, LAN or other private address, and git is pinned to the
874 address that passed. Git follows no redirects for a mirror, so give the clone
875 URL in its final form (https, with the `.git` suffix the host expects); a URL
876 that redirects fails with git's redirect error.
877
878 Only the person who connected an origin can write through it. Another writer
879 gets `origin_owner_only` rather than a comment appearing upstream under
880 someone else's name with someone else's token.
881
882 **Pins** are the bridge to the next section: the pin toggle on a repository
883 header (`p` on a row in the terminal, `P` on a repo's home) says "keep this on
884 my machine", and the dashboard grows an **On your machine** card listing them.
885 The server remembers only the choice — nothing about the state of a laptop —
886 and `forge sync` below is what acts on it.
887
888 **What an ask row waits for.** Every ask row carries one word, `waitingFor` (`src/core/waiting_for.ts`, computed once), and who it waits on, e.g. "ready to land · on you". The first that applies wins: `conflicts with main` (the last sweep found files stopping the pull, on its author), `ready to land` (a green change to look at and ship), `a review` (a review or a hand-over), `assigned to you` (the pull or issue is assigned to the viewer), otherwise `a decision`. It shows as a chip on each row of `/asks`, as `waitingFor: {word, on}` in `GET /api/v1/asks` and the MCP `asks` tool, and in the terminal's On you card. A grouped row shares one word (the weightiest of its items), and the terminal's card shows its row's word.
889
890 **Waiting.** A pull waits on a person when the crew handed it over, or when no person has acted on it for 24 hours (`WAIT_LIMIT_MS`, `src/core/pull_wait.ts`): a person's opening, comment or review restarts the clock, the crew's own words do not. The row is "Waiting" with how long (`waiting 30 hours`); it is on `/asks`, `GET /api/v1/asks` (`kind: "waiting"`, `why`), MCP `asks`, and in the terminal's On you card (`waiting` before the title). A pull you hold never asks for being idle, and a crew pull still at work or younger than a day stays the crew's. It can be picked, shipped or closed with the other rows (`waiting:<id>`). A crew member can advise by ending a comment with one line, `Recommendation: merge|close|change — why`; the newest such line from a non-person is shown as "The crew advises: merge — why" on the page and the terminal row, and as `recommendation: {verdict, reason}` in the API, on handed-over rows too.
891
892 **Muting a repository from /asks**: each ask row on `/asks` carries a "Mute owner/name" chip. A muted repository's asks leave `/asks`, `/api/v1/asks` and the phone's pushes (the same per-user mute the On you card uses, no new table); `/asks` says "N muted repositories still ask" with a "Listen again" button, and an app's own page still counts them. The home itself renders no asks, no for-you list and no count: the top bar carries a plain Asks link to `/asks`, and the terminal's dashboard keeps its On you card on purpose (a different door).
893
894 **A wave proposal is one card.** With "plans need a nod" on, a plan the planner writes is kept as a proposal and opens nothing. It is one row in the asks list (`/asks`, `/api/v1/asks`, MCP `asks`, the terminal's On you card) with its title, its why and its issue titles, all escaped, and it asks the app's owner only. Three taps, each one POST that leaves you on the page you were on: **Approve** opens the issues with their links, exactly as an unheld plan would; **Redirect** takes a text box and hands your words to the planner as a new run on the direction issue; **Later** hides it until you bring it back, from the "N waves put aside" line under the asks on `/asks` (each with a "Bring back" button). A proposal is decided once. The same four verbs (`approve`, `redirect`, `later`, `again`) are on every door, all calling `actions/proposals.ts`: the page (`/asks/proposal/<id>/<verb>`), the API (`POST /api/v1/asks/proposal/<id>/<verb>`, a `rw` token, `{"words": …}` for redirect), the MCP tool `asks_proposal` (`id`, `verb`, `words`), and in the terminal `y` approve, `R` redirect, `l` later and `B` bring every put-aside wave back, on a wave row of the On you card.
895
896 **Chips on the feed**: kind chips switch a kind of post off: `reviews` (pulls), `decisions` (decisions and choose decks), `crew` (tells, replies, looks) and `releases`; a person chip switches one person or agent off. Saved per user, one filter (`chipHides`) behind the home feed, `/api/v1/feed`, the Asks list and the terminal's inbox, and the MCP `feed` and `asks` tools, which also take `hide_kinds` and `hide_people` to replace the saved filter for that one call (`[]` lifts it). `/live/apps` (your apps) is not feed items, so chips do not apply there. `GET /api/v1/feed/chips` answers `{kinds, people}`; `POST /api/v1/feed/chips` takes `{"kind": "kind"|"person", "value": "reviews"|"<username>", "hidden": true|false}` (scope `rw`, same-origin). In the terminal dashboard `1`–`4` switch the kind chips `h` on an On you row switches its author off (not your own) and the author's rows leave the card, and `P` brings every person back. The `feed_chips` tool reads them (no arguments, any token) and switches one (`kind`, `value`, `hidden`; an `rw` token).
897
898 **Archive many, in the terminal:** on the home's repositories, an admin of a
899 repository marks it with `space`, `a` previews the marked ones by name and
900 archives them through the fleet `archive` verb (`applyFleetIds`), and `u` undoes
901 it (`undoFleetOp`). A non-admin has neither key.
902
903 **On you, in the terminal**, answers to the same mutes and one-move verbs as
904 the web card. Repositories you muted are hidden and counted ("2 repositories
905 muted"). On the On you card: `space` marks a row, `x` closes the marked rows
906 (or the row under the cursor) with one reason, `m` mutes their repositories,
907 `S` ships them, `u` undoes the latest set within its day, and `L` listens
908 again to every muted repository. Each goes through `applyAsksBulk`, so the
909 per-row access checks and refusals are the web's, shown in the toast.
910
911 **What the decider did today.** Today's card on the board carries one line,
912 "The decider closed N, deferred N, asked you about N": each count links to
913 its ledger rows (`/board?ledger=closed|deferred|asked`; any other value is
914 ignored, `?apps=` is kept). A deferral is the decider's own `stop`, never the
915 rule's. The ledger view lists up to 200 rows and says when it left some out.
916 In the terminal the same line sits above the dashboard, `C` (or the palette's
917 Decider) opens the rows, and `1`, `2`, `3` switch pile. Both are for the
918 repositories you own only: a reader sees nothing.
919
920 **Let the decider** answers a hand-over card (an issue the crew parked) when
921 you would rather not: the third tap on the card, `POST /api/v1/asks/decide`
922 (`{"item": "parked:<id>"}`), the MCP tool `asks_decide`, or `D` on the On you
923 card in the terminal. It sees only the direction text, whether you pinned or
924 muted the app, the day's spend against the ceiling, your last ten decisions
925 there and the card's own evidence; the decision ledger row carries exactly
926 that, with a reason and a confidence. A sure answer (70% or more) to set it
927 aside takes the card off your board; an unsure one, a failure, or no decider
928 configured leaves it with you. Undo for a day (`/api/v1/asks/decide/<id>/undo`,
929 `asks_undecide`, `U`). Each tap is a paid call, so one card is handed over
930 once in ten minutes.
931
932 **The standing rule** says who answers a repository's hand-overs without a
933 tap, three ways: `me` (the default: they wait for you), `decider` (the sweep
934 asks the decider about each parked card and its word stands, whatever its
935 confidence), or `decider_tell` (the decider's sure set-asides, 70% or more,
936 take the card off your board; the unsure ones stay with you). The tap on the
937 card follows the same rule. A shelved repository, one whose autonomy is `off`,
938 has its parked cards closed by rule with no model call whenever the rule is
939 not `me`, so turning the crew off on a ruled repository closes what it had
940 parked. The sweep spends at most five model calls a tick and never re-asks a
941 card already answered or undone. Only a repository admin who is a person sets
942 it, never an agent. Set it in the repository's settings (Autonomy → Decisions
943 here); over the API with `PATCH /api/v1/repos/<owner>/<name>`
944 `{"decisionsBy": "me" | "decider" | "decider_tell"}` (admin token; it is in the
945 repository JSON too); over MCP with `decisions_by` (`repo`; without `by` it
946 says how the rule is set, with `by` it plans unless `dryRun: false`); in the
947 terminal with `R` in the repository's workspace, which cycles me → decider →
948 decider_tell.
949
950 **The terminal explorer lists every entry of a directory, on purpose.** The
951 web directory page and its side column are windowed because they send every row
952 as HTML; the workspace's explorer sends nothing. It holds one small node per
953 entry and draws only the rows that fit the screen (`drawTree`,
954 `src/tui/screens/workspace.ts`), so a directory of thousands costs a list in
955 memory, not a page. A window would hide files from a person who is searching
956 the tree with `t` or revealing a path, and it would need a "show the rest" key
957 for no saving. Revisit it if a directory's listing, not its drawing, is ever
958 measured slow.
959
960269 ## On your machine
961270
962271 The forge holds the truth; `forge` keeps a copy of the parts you work on, the
@@ -1006,779 +315,6 @@ something in the 60–110 MB range; `dist/` is gitignored and binaries are never
1006315 committed). Until you build it, `bun run src/cli/main.ts …` is the same
1007316 program.
1008317
1009 ## Apps
1010
1011 A repository that runs. Press **New app**, give it a name and — if you like —
1012 a sentence about what it should do. Seconds later it is serving at
1013 `http://<name>.<your forge's host>` from a small, real app (notes in a SQLite
1014 file), and if you named an agent, the sentence became issue #1, assigned.
1015
1016 Under the box, four starter sentences are plain links (no script):
1017 `/apps/new?description=…` fills the box with that text (plain text up to
1018 200 characters, anything else is ignored; always escaped), and
1019 `&owner=` keeps the chosen owner. The list is `src/web/pages/apps-starters.ts`.
1020
1021 - **Private egress is a site admin's yes.** An app reaches a private
1022 destination (a tailnet or LAN `host:port`) only when `forge.json` `egress`
1023 names it exactly and a site admin has granted it: on the
1024 app's settings page, or in the terminal (About `I`, then `e`, `g`
1025 to grant, `x` to revoke). See `docs/APPS.md`.
1026 - **Egress connections are bounded.** The proxy closes the upstream socket
1027 the moment the cell side closes, errors or stops, and the other way round.
1028 An app holds at most 8 open connections to one destination (the ninth
1029 gets a `429`, logged), and a connection that moves no bytes, either way, for 5 min is
1030 cut, so a tunnel quiet for longer than that (a websocket or SSE stream with
1031 very long gaps) needs a keepalive. Open connections per app and
1032 destination, with the bytes moved, are on the Capacity page
1033 (`/settings/capacity`, admins) and in the terminal: About `I`, then `c`
1034 (site admins).
1035 - **Production is the default branch.** The only way code reaches it is that
1036 branch moving, and an agent cannot move it: the crew proposes through
1037 pulls, a person merges.
1038 - **Every open pull gets a preview** at `<name>--<pull number>.<host>`,
1039 redeployed on each push and gone when the pull closes. Its data starts as
1040 a copy of production's — copy-on-write where the filesystem can — and
1041 never writes back. Merging moves code and touches no data.
1042 - **A preview is held to the website specification, report-only.** Once it is
1043 live, `webspec()` runs over the `app.looks` paths of its `forge.json`. The pull
1044 page shows the result under the Checks line as **Website spec: N findings
1045 (report only)**; open it for one row per finding and its
1046 specification.website section. Findings the edge owns are counted apart
1047 (`, M the edge owns`). The terminal's pull screen ends its checks row with the
1048 same words. Nothing here fails a deploy or the check; see
1049 [`docs/WEBSPEC-PREVIEWS.md`](docs/WEBSPEC-PREVIEWS.md).
1050 - **A streamed page gets the pill too.** An HTML answer with no
1051 `Content-Length` is read up to 4 MiB for at most 100 ms, and only while its
1052 first chunk starts like markup; if it ends in that window it gets the pill
1053 like a sized page. Otherwise what was read is sent first and the rest of the
1054 stream follows unchanged. The cost: a streamed page that is still going
1055 waits up to those 100 ms before its first byte reaches the browser, so a slow
1056 server-rendered page paints up to 100 ms later (a page sent with a
1057 `Content-Length`, or one that starts with something other than markup, does
1058 not wait). Event streams and WebSockets are untouched.
1059 - **A signed-in note needs the panel's one-use proof (#5612).** The grant
1060 cookie alone no longer makes a note `@name`: the forge puts a one-use token in
1061 the panel's script tag on a signed-in page load, the panel sends it as
1062 `x-forge-proof` and each answer carries the next. Without it, a reused one or
1063 one for another grant or host, the sender is a visitor and `admin` is false.
1064 This stops an app's code that merely holds the cookie (a `fetch` from its
1065 page). It does **not** stop app code that runs in the same page and watches
1066 for the tag or patches `fetch` before the panel runs: the token reaches the
1067 panel through the page, so that code can read it. Closing that needs the
1068 panel in a cross-origin frame, which is not built. If a call fails the panel
1069 asks for a reload, since the chain is then spent.
1070 - **The forge button is a feedback door (#4758).** One tap opens *Tell the
1071 builders*: a text box and Send, no account. The note is posted to the app's
1072 own host (`POST /__forge/note`, same-origin only) and filed as an issue on
1073 its repository, labelled `from-app`, with the page, the viewport and the time,
1074 in the owner's name and with the sender's words inside one fence. A visitor
1075 is an anonymous `visitor-xxxxxx` (an HttpOnly cookie); a signed-in person is
1076 `@name`. Moderation: 3 notes a minute per sender, 6 per source address, 20 per
1077 app, a word filter, and the owner's hide or ban, which closes the issue and, for
1078 `ban`, refuses that sender. `GET /__forge/notes` gives the sender's own notes with a live
1079 status (queued, being built by a member, shipped on a date), polled every 15 s
1080 while the panel is open; a public app's note links to its issue. Left out: the
1081 optional screenshot and a version name in "shipped".
1082 From the terminal, a repository screen that is an app has two palette rows
1083 (`:`): *Tell the builders* sends a note as you, and *Notes from the app*
1084 lists yours with their status; the repository's admin sees every sender's and
1085 may hide a note, ban its sender, list the banned senders (*Banned senders*)
1086 and unban one. The gating is the page's: a guest or a
1087 non-reader sees nothing, and a repository that is not an app has no rows.
1088 Who may write: anyone on a public app, but a private app, and any sealed
1089 pull's preview, take notes only from a person the app admits (the condition
1090 `/__forge/measure` uses); a stranger is refused with a 404 before anything is
1091 filed. Moderation
1092 happens on the forge, not on the app: the app host answers only
1093 `POST /__forge/note` and `GET /__forge/notes`, and `/__forge/note/moderate` is
1094 a 404 there, because the app's own code (or a preview's) could call it
1095 same-origin with a visiting admin's cookie. The terminal screen above and the
1096 app's studio page are the owner's tools: the page lists the latest notes
1097 (sender, words, page, when) for those who may admin the repository, each with
1098 Hide and Ban sender, and every banned sender once with Unban, posting to
1099 `POST /:owner/:name/app/notes` on the forge's origin (`web/studio/notes.ts`).
1100 Limits count the socket's client (`http/peer.ts`, with its trusted-proxy
1101 rule), never a header the client wrote, and all three are checked before any
1102 is spent. The request body is read only up to 16 KB. The issue says its text
1103 is untrusted data, and the crew's brief says so for any `from-app` issue.
1104 A title always opens with `From the app: ` (`saysFromApp`), the label is made
1105 before the issue, and a failed labelling closes the issue and refuses the note.
1106 Only a note closed as done reads "Shipped"; the other reasons say what they are.
1107 Beyond the per-minute limits an app takes 30 notes an hour and 100 a day.
1108 The script grew to about 15 KB (cached, immutable); the notes are polled only
1109 while the panel is open.
1110 - **The forge pill acts.** The pill the proxy adds to an app's pages
1111 (`"toolbar": false` in `forge.json` turns it off; to write it in, the proxy asks the app for the page uncompressed, so a page that carries the pill goes out without gzip) links to a forge page,
1112 `/:owner/:name/toolbar?env=production|pull-N`, opened in a tab of its own: the
1113 pill posts nothing and reads nothing of the app's page, and every action is a
1114 same-origin form on the forge. On a preview, everyone who may see the pull sees what it changed (files, +added, −removed, linked to its Files tab) and where its latest check stands (linked to the run); the totals are left out when git cannot give them. Signed out, the page is where you are and Home.
1115 Signed in it shows the live agent run on this app or preview, linked to its
1116 run page (read once when the page opens; runs on work you may not see, such as
1117 a sealed report, are left out); *Report this page* / *Comment on this pull*,
1118 previewed first, then a receipt; *Tell the crew* for those who may direct it
1119 (the one `/:owner/:name/tell`); and *Make it yours* for everyone who cannot
1120 write the app. A production comment opens an issue carrying the page address;
1121 a preview comment goes on its pull. From a terminal or an agent the same
1122 actions are, in the SSH terminal, rows of the palette on a repository
1123 screen: *Working now* (while an agent run is live, left out when it is on work
1124 you may not see), *Tell the crew*, *Report a problem* (one text, an issue
1125 titled by its first line, through `openIssueThrough`) and *Make it yours*
1126 (the fork screen, for those who cannot write the repository); a pull is
1127 commented on from its own screen. Agents have the MCP `tell` and
1128 `make_it_yours` tools and the API's `POST …/issues`,
1129 `…/pulls/:n/comments` and `…/make-it-yours`.
1130 - **A run has a budget** (`src/kernel/budgets.ts`): a check past `gate`
1131 (60 min) or a reviewer's run past `review` (10 min) is killed with
1132 `budget: <what> took <n> min, limit <m>`, queued again once, and failed
1133 the second time, which the vitals carry as a need for a day. `gate` is 60
1134 min because this repository's own gate takes 18–25 min alone and 45 beside
1135 another check; it comes down when the gate does. A check waiting in the lane
1136 past `laneWait` (10 min) or a run silent past `heartbeat` (3 min) turns the
1137 health degraded with one sentence naming the cause ("22 checks wait behind
1138 one lane; the oldest since 04:13; the lane runs #1212 for 31 min (limit
1139 60)"). The sentinel files each such breach at once, not once per tick:
1140 labelled P0 Trust and assigned to the sentinel, whose run the dispatcher
1141 queues on the assignment.
1142 - **Every deploy is a run**, and the run is the check on the pull. Turn on
1143 **require checks** and a pull cannot merge until its head is green.
1144 - **A production deploy records the merged pulls it carries**: one `git
1145 rev-list` of the deployed commit when the run finishes. The run page says
1146 "Carries #3, #5" and `GET /api/v1/repos/:owner/:name/runs/:id` has `carries`.
1147 In the terminal, open a repository and press `d` for its production deploys
1148 (each with what it carries), `enter` for the log, `r` to refresh.
1149 - A **private** app answers only to someone the forge has vouched for
1150 (`canRead` on the repository, then a signed grant); a public one to anyone.
1151 - **Every app has a terminal.** `ssh -p 2222 <name>@<forge>` runs the app's
1152 `tty` command from `forge.json` on a real pty over the environment's own
1153 data; `<name>--<pull>@<forge>` opens a preview's. The scaffold ships one.
1154 - **A new app starts trustworthy.** The scaffold's door asks the app's
1155 contract before a body is read, every change and refused change is an
1156 audit row, the API describes itself at `/openapi.json`, `/healthz` says
1157 up, and the look is design tokens, light and dark, with no colour spelled
1158 out anywhere else ([docs/APPS.md](docs/APPS.md#what-a-new-app-starts-as)).
1159
1160 The repository says how it runs in `forge.json` (`run`, and optionally
1161 `install`, `test`, `health`, `ready`); a `package.json` with a `start` script
1162 is enough on its own. The forge hands the process `PORT`, `FORGE_APP_DATA`
1163 (this environment's directory — keep state there), `FORGE_APP_ENV`,
1164 `FORGE_APP_URL` and `FORGE_APP_COMMIT`, and nothing from its own
1165 environment. Browsers resolve `*.localhost` to loopback, so a laptop forge
1166 needs no DNS at all; elsewhere point a wildcard at it and set
1167 `FORGE_APP_DOMAIN`.
1168
1169 **Move the pill.** If the pill covers something of the app's, drag it (mouse or
1170 touch) and it snaps to the nearest corner on release; with the pill focused, the
1171 arrow keys walk it from corner to corner. The corner is kept in the browser's
1172 `localStorage` for that app's host (bottom-right until then), and the panel opens
1173 toward the inside of the screen, bounded to it at phone width. The script stays
1174 under a 10 KB budget (a test holds it), since it loads on every app page.
1175
1176 The forge pill on an app's pages says where you are: its accessible name reads
1177 `notes, production` or `notes, preview of pull #7`, and pressing it opens a
1178 panel with the app, the environment, and a link home. The pull's title is shown
1179 only to people who may read the pull. A tap outside the open panel, or Escape,
1180 closes it. When a crew run is working on the app (production) or on the preview's
1181 pull, the panel also says `scout is working on this` and links the run; it asks
1182 `/__forge/crew` once as the pill loads, answers only a signed-in person who may
1183 read the repository (and, for a run on a sealed issue, that issue), and shows
1184 nothing when the crew is idle.
1185
1186 **When the pill is not shown** (an app whose own policy refuses foreign
1187 scripts), `/__forge/` on the app's host is the way back: a small page from the
1188 forge, never the app, with no script, saying the app and where you are
1189 (`production` or `preview of pull #N`) and linking Home, the repository (to
1190 who may read it) and *Tell the crew* (the same link `/__forge/crew` gives; signed
1191 out it offers sign in). It also says what is serving, read from the deploy
1192 record (no git): production shows the newest release and the three before it
1193 (a redeploy of the same commit is not another release), a preview shows its
1194 pull and the commit it last served, or "not deployed yet". A viewer who cannot
1195 read the repository sees only the time, never a sha. The read is the `serving`
1196 Server-Timing phase. The prefix was already the forge's, so an app's own
1197 `/__forge/` path is not reachable.
1198
1199 **Tell the crew from the pill.** Signed in as someone who may direct the crew,
1200 the panel also offers *Tell the crew* (`/__forge/crew` adds a `tell` link, only
1201 for them; signed out there is no action). It opens the forge's own
1202 `/:owner/:name/tell?page=<address>&pull=<n>` in a new tab, so the POST is the
1203 forge's same-origin, CSRF-checked form and the app's origin can never send one.
1204 It is the same `tell()` as every door; `page` (an http(s) address) and `pull`
1205 (a number, on a preview) are checked again on send and written into the tell
1206 as the *Where this was told from* block. A receipt links the issue it opened; a
1207 refused tell returns to the box with the words, page and pull kept. The MCP
1208 `tell` takes the same through `context: { page, pull, run, commit, said }`, and
1209 the terminal's palette has *Tell the crew from a page*, which asks for the
1210 page, the pull (optional) and the line, through the same `parseContext` and
1211 `contextNote`.
1212
1213 With a sentence and **Pick for me**, the sentence becomes a plan before the
1214 page comes back: `src/crew/plan.ts` makes one non-interactive call to
1215 `claude` — the quickest model, a fixed JSON shape, at most 3 seconds — for a
1216 short checklist and a crew pick, so issue #1 opens as a checklist already
1217 assigned to whoever the plan named, instead of a paragraph and a dropdown.
1218 It costs one small model call per app made this way (a few tenths of a
1219 cent), never blocks the repository or the app starting, and a bad or slow
1220 answer is the same as none: no pick, no checklist, exactly what naming an
1221 agent yourself already does. `FORGE_APP_PLAN=0` turns it off; so does having
1222 no `claude` on `PATH` — either way **Pick for me** stops being offered and
1223 `New app` behaves exactly as it did before this existed.
1224
1225 ## The crew
1226
1227 In a preview (`FORGE_APP_ENV=preview`, which the platform sets) the crew is
1228 off: no autonomy tick, no dispatch, no spend; checks still run and everything
1229 can still be read. The boot log and the studio's crew card say so. Set
1230 `FORGE_CREW_IN_PREVIEW=1` to run it anyway.
1231
1232 **Per model, per prompt version.** Every agent run records the model it ran on
1233 and a prompt version: a short hash of the agent's brief, its crew role and the
1234 standing system-prompt text (not the issue or branch, so two runs on one brief
1235 share a version and editing the brief starts a new one). Settings → Crew shows
1236 the scorecard cut both ways ("Per model", "Per prompt version": runs, landed,
1237 failed, spent, per run), and the terminal's `Crew scorecard` command shows the
1238 same to an admin. The scorecard of pulls landed, review rounds and red checks
1239 is cut the same way as sub-rows under each agent (see above); a team's pooled
1240 row is not cut by model, and is under the agents on the page and in the
1241 terminal's "Crew scorecard per agent" (see above). Runs from before this was recorded show as *default* /
1242 *not recorded*; they are not backfilled.
1243
1244 **Tell the crew from any page.** In a repository you may direct, the web palette
1245 (`:` in the browser) has a *Tell the crew* row. With the script it opens the thread's own
1246 comment box over the page you are on, without leaving it: `enter` sends (shift+
1247 enter is a new line), `esc` closes. The tell remembers that page (`from`), and
1248 the answer comes back to it. Without a script the row is a link to a page of
1249 its own, `/:owner/:name/tell?from=/path`, which is the same box; `from` must be
1250 a path on this forge. Who may not direct the crew gets a 404 there and no row.
1251 The terminal's palette has the row too, on a repository screen, for the
1252 same people: it takes one line, tells with no `from`, and answers in the
1253 same words.
1254
1255 **The crew's answers.** On an app's page the owner's own tells show under the tell
1256 box, newest first, each with what the crew said back (its last comment, else its
1257 last finished run's summary, else that it is working or waiting). The region
1258 updates live from `GET /:owner/:name/app/answers` (owner only; 404 for anyone
1259 else). In the terminal, `a` in the repository screen opens *Your tells* with the
1260 same rows behind the same gate.
1261
1262 An **agent** is an account of kind `agent`: no password, no session, and it
1263 acts only through a token the forge mints for a run and destroys after it.
1264 An admin creates one (**Settings → Crew**, or `POST /api/v1/agents`) with a
1265 model, a brief in its own voice and a spend budget; a repository admin puts it
1266 on the repository's **crew**, which is the one grant an agent can hold — it
1267 reads and writes that repository and administers nothing.
1268
1269 A **planner** on the crew may also close an issue it is re-planning, in
1270 `actions/issues.ts` so the web, the API and the MCP agree, and only these
1271 ways: `split` into open issues opened since it was handed the issue (since the
1272 issue itself when the crew never parked it twice, or into issues of the same
1273 repository that all closed as done since that hand-over), `dropped` for an issue the
1274 crew wrote, and `done` when commits on main already name the issue. A person's
1275 ask is never dropped by the planner; any other close, any reopen and any edit
1276 to a person's issue stay refused.
1277
1278 Assign an issue to an agent and it starts: the forge clones the repository,
1279 puts the agent on a branch named after the issue, and runs Claude Code
1280 headless with a brief that says who it is, what the task is, how to finish,
1281 and what will be refused. The pull opens at the agent's first push, marked
1282 "In progress" with a link to the run's log, and its preview follows each later
1283 push (`src/crew/early.ts`). When the agent stops, the forge pushes what it
1284 committed and fills in that same pull (the agent's last message is the
1285 description). A run that ends without reporting leaves the pull saying so, with
1286 the log link; a run that is only paused (a limit, an outage) keeps saying "In
1287 progress". A run that declines after its pull opened closes that pull with the
1288 decline sentence and the run link, unless a person has commented on or reviewed
1289 it: then it stays open with a comment. An agent may withdraw (close) a pull it
1290 wrote itself, and nothing else. A body a person has edited is left alone, unless they kept its first
1291 words ("In progress:" or "The run ended"); change those words too. Leave
1292 feedback on the pull — a comment, a request for changes, a line comment — and
1293 the agent picks it up in the background, resuming its session. A failed check re-engages it too, a bounded number of
1294 times before a person must speak.
1295
1296 **Design review.** A pull that changes anything under `src/web/` (a page, the ui
1297 vocabulary, the style) is sent to a crew reviewer on the Design team when there
1298 is one, and its review brief holds the change to `docs/DESIGN.md`. A security
1299 change still goes to the security reviewer; a change only under `src/core/` is
1300 not a design matter.
1301
1302 **How a reviewer is chosen.** Reviews go to the least-busy reviewer: the one
1303 with the fewest review runs in flight (queued or running). A tie goes to the
1304 experience reviewer. A pull about security goes only to a security reviewer
1305 (the least busy of them). A `src/web/` pull goes to the design reviewer until
1306 it has 2 review runs in flight, then spills to the least busy reviewer.
1307
1308 **Three rounds, then a person.** When the crew reviewer has asked for
1309 changes three times on one pull with no approval and no word from a person
1310 who may direct the crew between, no more feedback runs start on it. The
1311 thread keeps a note ("Handed to a person by the crew:") with the three
1312 asks, and the pull becomes a **hand-over** card in asks: *Tell them what to
1313 try*, or *Close it*. Rounds are counted from the reviews, not the runs, so a
1314 restart never adds one. A person resets the count by commenting as a
1315 director (the crew then resumes) or by approving the pull from its page; the
1316 card ends when the pull is approved, closed or merged. Hand-overs can be
1317 closed in bulk like the other pull asks. The terminal's On you card lists
1318 them as "handed over" rows: `enter` opens the pull, `x` closes it.
1319
1320 **A request for changes always moves.** Every tick, a crew pull whose
1321 reviewer last asked for changes is moved on. If the builder has pushed since
1322 and the new head is green, the reviewer is asked again: a verdict on old
1323 code never blocks. If the verdict is on the current head and no run is in
1324 flight, one feedback run is queued to the pull's builder, and a builder is
1325 assigned when the pull has none. That run takes a builder slot like any
1326 other. The tick log names each pull it moved, or why it did not, and
1327 The flow panel on `/boards`, the terminal's Flow dialog and the MCP `flow` tool list the pulls
1328 waiting on a builder and those waiting on a reviewer, oldest first.
1329
1330 The same three also say, per reviewer, how many review runs are in flight (queued or running): `3 reviews in flight: quill 2, sage 1.`, an element `#review-load` on `/boards`; the MCP `flow` tool and `GET /api/v1/telemetry/flow` carry it as `reviewing: { says, reviewers }`. It is `reviewLoad` from `src/crew/review.ts`, the count routing reads (`flow/reviewing.ts` only names it), and it is scoped to the reader: the owner's counts every repository, an agent's only public ones, so a review running on a private repository adds nothing to what an agent reads. No dependency was added.
1331
1332 **Already done.** A builder that finds its issue already built on the base
1333 branch declines with `DECLINED: already done on main in <commit>`, naming the
1334 full or abbreviated id of each commit that did it (at most five). The forge
1335 checks that every commit named is on the base branch; only then is the run
1336 an "already done" outcome, with no strike and nothing parked. The issue stays
1337 open with a note naming the commits, the asked list and the issue page say
1338 "Done on main", and the person who asked closes it (the page's Close button,
1339 or `x` on the issue in the terminal); the close writes "Closed: already done
1340 on main in …" on the thread. A claim with no commit, or with one that is not
1341 on the base, is an ordinary decline and is parked as one.
1342
1343 **Live triage.** On each autonomy tick, an open issue carrying the crew's own
1344 "already done" note (a person's note counts for nothing) is closed as *done*
1345 when every commit the note names is on the base branch; one missing commit
1346 leaves it open. At most 25 per repository per tick, rules only, no run and no
1347 spend. The close is made in the name of the person who set the app autonomous,
1348 so its comment says "Closed by the crew's live triage". Reopening the issue
1349 undoes it and the reopen stands: an issue that already has a close note is
1350 never closed by triage again. The tick line counts them as `triaged=N`.
1351
1352 Live triage also folds duplicates: of two open issues whose titles match after
1353 case and punctuation, the newer closes (reason dropped) with the comment
1354 "duplicate of #N", and whatever waited on it waits on the older. A person's
1355 newer issue is never closed into a crew member's; the older closes instead. A
1356 fold a person reopened stands, the plan's own issues are never folded, a
1357 dependency that would make a loop stays where it was, and folds share the
1358 25-per-tick cap with the already-done closes. Exact match only, no fuzzy
1359 matching.
1360
1361 **Live triage files issues.** On the same tick, an open issue an hour old
1362 that serves no pillar is filed: under the pillar its title, else its body,
1363 names by its short form (`P0`) or whole label (`P0 Trust`) — a bare word like
1364 "trust" does not count — otherwise behind the Later gate. Each gets a comment
1365 marked automatic. Directions, umbrellas and the gate are skipped, as is a
1366 repository with neither pillars nor a gate. To undo it, name a pillar (link
1367 its umbrella to wait for the issue) or unlink the gate; the comment stays, so
1368 triage does not file that issue again. The tick line counts `labelled=N`, and
1369 shows `triageMiss=N` for old issues it could not file (no gate to defer to).
1370
1371 Live triage also folds questions. An open issue opened by a person who can
1372 write here, whose title starts `Question:`, `Wish:` or `Idea:` or ends in `?`,
1373 becomes one `Folded: #N <title>` comment on the direction issue and closes as
1374 *dropped*, with a comment saying how to get it back: reopen it and retitle it
1375 as the task it asks for; the reopen stands. Issues from outsiders, agents and
1376 imported ones are never folded, and nothing is folded before the repository
1377 has a direction issue. Note that a writer's real task titled `Can we add X?`
1378 is folded too, since a title ending in `?` is the rule; phrase tasks as
1379 statements. The tick line counts folds as `folded=N`.
1380
1381 **A red pull.** The decider stops a red check only when it is at least 70%
1382 sure; below that the pull gets another feedback run. A stop parks the pull: it
1383 closes as *parked*, not done, with a comment that says why. The pull page's
1384 badge reads "Parked" instead of "Closed", the terminal's merge box says "Parked
1385 by the crew, not merged", and the lists note it as stopped. Reopening it clears
1386 the mark. Only the crew parks: the API and the page refuse `reason: "parked"`.
1387 In the database the pull's `close_reason` stays NULL (its CHECK predates the
1388 word) and `issue.parked = 1` carries it, so raw SQL never reads it as done.
1389
1390 **The merge queue.** A green, approved pull in an autonomous repository joins
1391 its repository's queue (`src/checks/queue.ts`). The queue builds one
1392 candidate: main, then each queued pull's squash on top of the one before, in
1393 queue order, leaving out any pull that conflicts or has nothing left to land.
1394 A candidate carries at most eight pulls (`MAX_CANDIDATE`), so a gate stays
1395 near one full tier and a red bisects in three; the rest wait their turn.
1396 The log says `[queue] gating N pulls since HH:MM` as a gate starts and
1397 `[queue] gated N pulls: green|red|cut off` as it ends.
1398 The full gate runs once on that candidate. Green lands every pull in order,
1399 and each landing is refused unless main is still where the candidate was
1400 built and the squash writes the tree that was gated, so main ends at the
1401 gated tree. Red is searched: prefixes of the candidate are gated on main
1402 — the front half, plus one more cut per free check slot, all at once — the
1403 longest green prefix lands, the search goes on in the steps up to the first
1404 red prefix, and the red pull is handed back with the failing tests named.
1405 A red batch is never formed again around the same head: with a check slot
1406 free, the head of a red batch of three or more is gated alone beside the
1407 line (`[queue] red batch: its head is gated alone`) while the rest gates
1408 without it, stacked as deep as `queue.depth` goes, so the good pulls land
1409 while the suspect is judged. A pull in two red batches is never in a third:
1410 it waits for a gate of its own. Red alone, it is handed back (`[queue] red
1411 alone`); green alone, it rides at the tail of a later batch, never its head.
1412 While a candidate gates, the next one gates on top of it (`queue.depth`
1413 under Settings → Capacity, `FORGE_QUEUE_DEPTH`, default 2): when the first
1414 lands whole the next lands right after it, on the tree it was gated on; when
1415 it does not, the next is formed again on main. A gate beyond a line's first
1416 starts only on a free check slot and holds it, and two slots are always kept
1417 for pull checks (`PULL_RESERVE`); with no slot spare the queue gates one
1418 candidate at a time and halves a red one, as it always did. A red
1419 waits for a push, not a new main (a pull red on main would otherwise ride a
1420 batch after every landing), except once: after 30 minutes on the same head
1421 it is gated again, and lands if green; a red that reproduces stays red and
1422 is not told twice. The queue remembers what its searches found: a pull
1423 handed back red on top of others of its candidate that landed first is a
1424 clash (the comment names them; the log says `[queue] red on top of its
1425 candidate`, or `red on its own merge`), and the tick line counts them as
1426 `clashes=N`. A remembered pull that joins again at the same head is gated
1427 alone, only once no fresh pull waits, and nothing is stacked on it, so a red
1428 re-check never cuts off a batch of fresh pulls; a push forgets it. A gate cut off by the forge stopping (a
1429 roll, a drain) or by the cluster losing its Job is no red at all: the next
1430 sweep gates it again. So is a gate that has not answered in 15 minutes
1431 (`GATE_DEADLINE_MS`): it is abandoned and its check aborted, the log says
1432 `[queue] candidate abandoned`, and the next sweep forms the line again. Pulls
1433 that turn green while a candidate is gating wait for the next one. Each
1434 queue's numbers — how many pulls are queued, how many are gating, how many
1435 gates are in flight, how many landed in the last hour, and their median
1436 minutes from approval to landing — show in three places: the flow panel on `/boards`, the
1437 terminal's Flow command, and the MCP `flow` tool's `queue` field. The flow
1438 panel and the terminal's Flow command also show the last hour's red batches
1439 (`1 of 3`: candidates gated red, of those gated; search prefixes and
1440 re-checks are not counted) — the panel as a column, the terminal as a
1441 `Red batches, last hour:` line under its table. The MCP `queue` rows do not
1442 carry it: their shape is unchanged.
1443 When a repository requires checks, an approved pull that is green only
1444 against an older main joins the queue as it is: the candidate's full gate
1445 runs on main's tip with that pull merged, which is the proof a re-check of
1446 its own would give, and a red candidate bisects as above. So a landing that
1447 moves main does not send every other approved pull off for a check of its
1448 own first; the batch lands in one gate. A pull's own check runs again only
1449 when its head changes: an approval checks a head that was never checked,
1450 and a green one only when main moved since and the repository is not
1451 autonomous, whose land gates on today's main anyway (`src/checks/approval.ts`). A person's merge, which no candidate gated, still
1452 refuses a stale green. Each sweep reads every open pull, oldest first, not
1453 only the newest page, so an older approved pull is batched with the rest
1454 rather than joining alone on its own event. The autonomy tick line counts the approved pulls the
1455 last sweep saw and why each waits (`approved="3 approved: queued=2 review=1"`,
1456 from `src/crew/waits.ts`), and what the queue is doing now
1457 (`queue="gating 2 pulls since 18:54"`, `queue="1 waiting"` or `queue=idle`,
1458 in UTC).
1459
1460 An autonomous app's planner is **held** while nothing is landing or the plate
1461 is full: no pull has merged in 48 h while one was closed unmerged, or more than
1462 `slots × 3` ready, unassigned issues are waiting. The tick log says which
1463 (`planner: held`), and the board and the terminal's repository list say it in
1464 words. A comment on the direction issue from someone who may write there lifts
1465 the hold; an issue handed to the planner is still owed its pass.
1466
1467 Every tick says why each ready issue a builder cannot start is held, counted
1468 under the first filter that holds it (`crew/held.ts`): `held: running=1
1469 open_pull=10 stalled=12 run_cap=11` — a run on it, an open pull proposing it,
1470 the planner's, found done on main and waiting for a person to close it
1471 (`done_on_main`, never re-planned), stalled, at its run cap, parked twice, covered by a red-main
1472 fix, or a setting's finding. The flow panel on `/boards` draws the same per autonomous app as one
1473 bar, and the terminal's Flow command prints it as a line per app
1474 (`flow/held.ts`, kept 5 s). When nothing is startable while builder slots are free and some of the
1475 held are stalled, capped or parked twice, the planner is owed one pass with
1476 that breakdown in its brief, to re-plan them into smaller, fresh issues; a
1477 full plate and its own unanswered decline do not hold that pass, and the
1478 same breakdown does not ask it twice. A stalled issue does not wait for that
1479 pass: the next tick parks it with who stalled it and why, so another builder
1480 picks it, and a second stall hands it to the planner past its day's cap
1481 (`crew/stalls.ts`, `unstuck=N` on the tick line).
1482
1483 The forge asks the host for one more binary: `claude`, on `PATH` or at
1484 `FORGE_CLAUDE`, authenticated as the operator (an `ANTHROPIC_API_KEY` in the
1485 forge's environment is handed to runs; the machine's own Claude Code login
1486 works when there is none). `FORGE_CREW_CONCURRENCY` (one per 3 GiB above 2 GiB, 2–16) and
1487 `FORGE_CREW_BUDGET_USD` (5) bound it; `FORGE_CREW_CEILING_USD` caps what the
1488 crew may spend across every repository in a UTC day, whatever each
1489 repository's own ceiling says; `FORGE_APPS=0` turns apps and crew off. A preview whose pull nobody has
1490 pushed to or commented on for `FORGE_PREVIEW_IDLE_DAYS` (default 3; `0` keeps
1491 every preview warm) is stopped and reads as asleep, and asking for it wakes it.
1492 An admin with more than one Claude account keeps them under Settings →
1493 Subscriptions, sealed, and grants each to a repository (Settings → Crew) or
1494 an organization (its page), down to one crew member: a work plan pays for
1495 the work repositories and nothing else. Two on one grant take turns, and
1496 one the provider rests hands its runs to the other until the reset.
1497 **Model budget.** A new account has none: its repositories' crew does not run, spends nothing, and says "This repository has no model account". A person adds their own subscription at Settings → Model (or the terminal's **Model budget** command, `a`); it is sealed, granted to their own repositories only, and one per account. A forge admin who is a person gives an account a capped grant, in dollars a month spent on the forge's own credential, from the account's page, the terminal's **Model grant** command, `POST /api/v1/admin/accounts/:name/model-grant` (`{"usdPerMonth": 20}`, `null` takes it back) or the MCP `model_grant` tool; each change is in the audit. The cap is the UTC month's spend in the usage ledger, checked when a run starts and on every spend tick: a run past it stops, and its page says the grant is spent. Two runs that start together each begin with the whole remainder; the tick check ends both once the sum crosses the cap, so the overshoot is at most what they spend between ticks.
1498
1499 **Roles.** An account is a guest, a member or an admin (`docs/ROLES.md` has the table of what each can do; `src/core/roles.ts` holds it). Admin is the admin flag, a member is promoted, everyone else is a guest. A guest is refused creating or forking a repository, making an app their own and starting a crew run, in the words "the owner has not made this account a member" (after the older refusals: unverified, suspended, agent, private); nothing switches it off. A forge admin who is a person lists everyone with their role at Settings → People (`/settings/people`) and promotes a guest to member with one button, with an optional starter model allowance in dollars a month (none by default, given as a model grant and audited as one), or demotes a member (the allowance stays until taken back on the account's page). In the terminal it is the **People** command (`r` changes a role: `member 5`, or `guest`); over the API, `GET /api/v1/admin/people` (`{ items, next_cursor }`, as `/admin/accounts` and `/admin/invites` answer, with `limit` 1-200, 50 by default, and `cursor`) and `POST /api/v1/admin/accounts/:name/role` (`{"role": "member", "usdPerMonth": 5}`); over MCP, the `account_role` tool (a plan unless `dryRun: false`, with an admin token). Anyone signed in sees their own role and its row of the table: the account page (`/settings`) shows "What you can do here"; over MCP, `whoami_can` returns `{username, role, can, abilities}` with any token (with `{"kind": "passkeys"}` it lists your own passkeys instead, `{ items }` of id, name, created, lastUsed, people only; the page is Settings → Passkeys); in the terminal it is the **What I can do** command (read only, agents included).
1500
1501 **Invites.** A forge admin who is a person mints a single-use invite link with `POST /api/v1/admin/invites` (`{"days": 14}`, 1 to 30, 7 by default); the response carries the link once and only its hash is kept. `GET /api/v1/admin/invites` lists them (open, used, revoked, expired) without tokens, and `DELETE /api/v1/admin/invites/:id` revokes one. Minting, revoking and using a link are events (`invite.minted`, `invite.revoked`, `invite.used`; the last names the invite and the account it let in, never the token). The same is the **Invite links** card on `/admin/limits` (a form with expiry days, 1 to 30; the new link is shown once, with a copy button; an open link has a Revoke button) and the terminal's **Invite links** command (`m` makes one and prints the link once, `x` revokes the selected open one). Over MCP, the `mint_invite` tool (`{days?}`, an admin token, a forge admin who is a person) answers with the same link, and the `invites` tool (`{action: "list"|"revoke", id?}`, the same person admin and admin token, for both actions) lists them without links or revokes one. While signup is `invite`, the link opens the door once: `/signup?invite=…` (the form and the passkey door) or `invite` in the `POST /api/v1/users` body. In the terminal, with signup = invite the create-account screen has a fourth, masked **Invite** field that takes the token (not the whole link); a blank one is asked for, and a bad one is refused with the same sentence as `/signup`. The account and the invite's use are one write, so two sign-ups with one link make one account; a used, revoked or expired link, or any link while signup is `closed`, makes none. While signup is `open`, a link is not spent.
1502
1503 **Spend ceilings.** Apart from the grant, a forge admin who is a person may set any account's — a person's, an agent's or an organization's — daily (UTC) and per-run ceilings in dollars: on the account's page (`/settings/accounts/<name>`, an organization's too, by that address; an agent and an admin's own account have no such page), the terminal's **Model ceiling** command, `POST /api/v1/admin/accounts/:name/ceiling` (`{"dailyUsd": 20, "perRunUsd": 5}`) or the MCP `model_ceiling` tool; each change is in the audit. Both empty on the page, `none` in the terminal (its prompt shows the current ceilings, and an empty answer changes nothing), or `null` for both on the API and MCP takes them back. An organization's page is linked from its own page for an admin. An account has no ceiling until one is set, so this forge's own crew is never stopped by a default nobody chose. A run on an account with a ceiling does not start when the day's is spent or the per-run one is too small, is capped at the smaller of the per-run ceiling and what is left of the day, and stops mid-run when the day's is spent.
1504 **Sandbox.** An agent's shell is held to a policy — the network is the forge
1505 and the package registry, writes stay in the clone, the forge's data and the
1506 operator's credentials are unreadable — by bubblewrap and socat on Linux
1507 (both are in the image). Inside it a builder can install the declared
1508 packages, type-check, and run the tests that boot a server: no unix socket is
1509 let through, so the run carries `FORGE_TEST_SOCKET=port` and the forge listens
1510 on a loopback port instead (`bun scripts/sandbox-probe.ts` prints each
1511 verdict). A host that forbids unprivileged user namespaces
1512 (many container platforms' seccomp profiles) cannot hold it, and the policy is
1513 then switched off rather than faked. To tell which a deployment is in: the
1514 boot log carries `agent sandbox not available here` with the reason, Settings →
1515 Crew shows **Unsandboxed** with it, and every agent run's page says
1516 "Sandboxed" or "Unsandboxed" from what it was actually started with (runs
1517 from before this record say nothing). Claude Code bridges that sandbox with
1518 two `socat` relays per session, in the session's process group; when a
1519 session ends, however it ends, whatever is left in that group is killed. A
1520 `processes` line once a minute counts what runs beside the forge by name
1521 (`top="socat=… bwrap=…"`), and kills any bridge whose session is gone
1522 (`reapedBridges=N`). Every start is counted in `boots.json` under the data
1523 directory: the boot line and every autonomy tick line say `restarts=N`, plus
1524 `unclean=N` for starts that ended without an orderly exit (a crash or an OOM
1525 kill, which leave nothing in the log themselves).
1526
1527 `docs/APPS.md` is the contract: the vocabulary, and the rules that are
1528 mechanical rather than advisory.
1529
1530 `docs/FLOW.md` is where every issue stands: `GET /api/v1/repos/:owner/:name/flow` returns each open issue's stage, since, stalled and unblock, plus what went live in the last 24 h.
1531 Where the whole forge waits is the owner's, on `/boards` (the old `/flow` redirects there, query kept; an agent granted `telemetry:read` reads the same from `/api/v1/telemetry/flow` and the MCP `flow` tool): the flow panel shows each stage from ready to live with what waits, the oldest wait and its capacity, under one sentence naming the bottleneck, and beside each what it let out in the last hour with a 24-hour sparkline, under a line of landings and the median ask→live — every count links to the items it counts. A stalled step is a card on the home (Studio mode) for anyone who can write to the repository: an open pull whose check was never asked for, or is red, past its stage's allowance, with its stage, how long it has been stuck, and one button — Run check or Re-run — that asks for a check at the head and returns to the home. The terminal's **Stalled steps** command (palette) lists the same steps, live, and Enter or `r` does the same re-run; both read `flow/stalled.ts` and act through `flow/unblock.ts`, and neither shows anything to someone who cannot write. A conflicted pull never sits: every tick the crew's open pulls that no longer merge get one bring-up-to-date run each (one in flight per pull), oldest pull first and only as many as the check lane has slots for, since each ends in a push and a check: check slots minus the checks queued and running minus the runs already in flight, with one always allowed (`crew/conflict_room.ts`, #4459); after 2 failed tries in the same conflict, or 24 h since the first, the pull is closed with the conflicting files named and its issue goes back to ready, unassigned, with no strike against the builder. Conflicted pulls are listed oldest first on `/boards`. The terminal's Flow command and the MCP `flow` tool read the same: the sentence, the landings line, and each stage's count out in the last hour and in 24 h.
1532
1533 `/resilience` is a forge admin's too (a guest is sent to sign in, anyone else gets 404): each protection in `docs/THREATS.md` as one line, green, red, untested or "no test yet", judged by the test file it cites in the last full gate of the forge's own repository (`FORGE_REPO`; other repositories' gates never touch it). The terminal's **Resilience** command (palette, admins) lists the same lines.
1534
1535 The terminal also has an Access command (`:` → Access), a site admin's: it reads the same last-day non-browser requests as Settings → Access and the `access` MCP tool, newest first, and filters them with `s` (since), `u` (user agent) and `p` (path), `c` to clear.
1536
1537 `/system` is the machine, for the owner (#1789), laid out like btop (#2721): a meter and a 30-reading block-character history per core, memory and swap meters, load, hwmon temperatures, disk use per mount, disk and network rates (`/proc/diskstats`, `/proc/net/dev`), and a table of every process grouped forge / check (`check:<run>`) / app (`app:<slug>/<env>`) / host with CPU, memory and age, sortable by CPU (`c`) or memory (`m`) or the buttons (shown only with a script; without one the order is by CPU). It reads `/proc` and `/sys` and updates in place over one event stream, `/system/stream` (`?pause` holds it still; no reload, no polling); the 2 s push timer runs only while someone is subscribed, so it costs nothing unwatched. Its history is the `machine` board source. A test file running in more than one check at once is counted at the top of `/system` and the `/boards` flow panel, with the checks. Command lines are never kept — only `*.test.ts` arguments.
1538 In the terminal, `:` → System shows the same view (the owner's only): per-core CPU with a block-character history, memory, swap, disk and network rates, and each group's top two processes (no sort; the dialog is read-only), and `:` → Telemetry lists the agents and flips `telemetry:read` for the one you press Enter on.
1539 An agent the owner grants `telemetry:read` (Settings → Telemetry, one switch per agent, revoked the same way) reads the same through `GET /api/v1/telemetry/{system,flow,sources,history}` and the MCP `flow` and `system` tools; private repositories' checks and apps are renamed `check:private-N` and lose their processes and test files, and `flow` (like `system`, an admin agent account needs the grant too: only a person who is an admin reads without it) names only public repositories' conflicted pulls, every read is listed on that page, and an agent without the grant is refused. `/contribute`, `GET /api/v1/contribute` and the MCP `contribute` tool are open to anyone: what the forge is, the bottleneck now, open issues labelled “help wanted” in public repositories, and how to propose (fork, pull, the same gate). The grant is a table, not a token scope, so no token has to be re-issued.
1540
1541 **Raw files and caches.** `GET /:owner/:name/raw/:ref/*path` at a full 40-hex commit id is `immutable` for a year: `public, max-age=31536000, immutable` for a public repository, `private, max-age=31536000, immutable` for any other, so a shared cache never keeps private bytes. At a branch or tag it is `private, max-age=0` and revalidates. Every raw answer has the blob's oid as its `ETag`, and a matching `If-None-Match` gets a 304 with the same `Cache-Control`, `nosniff` and sandbox CSP. The HTML blob page is not cached this way (it carries the viewer and a nonce). A public repository that later turns private keeps its year-long `public` copies in shared caches already holding them. See docs/CACHING.md.
1542
1543 `docs/CACHING.md` is what the forge keeps between requests to make `/map` and
1544 `/` fast: each map lane's plan and each viewer's feed candidates, stamped with
1545 SQLite's own write clock so any write is seen on the next read, keyed so one
1546 person's view never reaches another, and the budgets `test/snappy.test.ts`
1547 holds. `bun run bench/snappy.ts` prints p50/p95 and SQL statements per request
1548 for both pages, warm and cold. The content-addressed assets answer a
1549 revalidation with `304`.
1550
1551 `docs/PLANNING.md` is how issues depend on issues: the edge, the map, and
1552 what "ready now" means to a person and to the crew. The map draws open
1553 issues by default; `?done=1` ("Show done") draws the closed ones on an edge too. The map also shows each
1554 pillar's ask-to-live: the median hours from an issue opening to its change live
1555 in production, over the last 7 days (also in the map and milestones API and MCP).
1556 It reads the `deploy_pull` record a production deploy writes when it finishes,
1557 so a page view runs no git; an issue closed by several pulls counts once, at
1558 its first time live. On boot the forge records what the last 7 days of earlier
1559 production deploys carried, so work that went live before the record is counted.
1560
1561 `docs/STUDIO.md` is the front door from the top of this page in full: the five
1562 words and where each is decided, the box, the three lists, a tell, a run
1563 and a change each on its own page, and the board.
1564
1565 `docs/CONTRACT.md` is who may do what, as a table: an app's `contract.json`,
1566 the one evaluator every app decides with, the audit that reads it from git,
1567 the probe that knocks on the running app, and the wall a person alone
1568 crosses.
1569
1570 `docs/FOLLOWING.md` is a fork that keeps up with its parent: where it
1571 stands, taking updates, and the crew's part when both sides moved.
1572
1573 `docs/FLEET.md` is one change across many repositories, runs or issues: a
1574 query, a dry-run plan of every row before → after, a receipt, and undo.
1575 `archive` (true or false) archives or wakes many repositories; its undo is kept a day.
1576
1577 `docs/MCP.md` is the forge as a connector, and `docs/STANDARDS.md` is what
1578 it answers by default: `.well-known`, feeds, cards, the manifest.
1579
1580 `docs/SNAPSHOT.md` is a forge back from a copy: what a nightly snapshot
1581 holds, where it goes, and the one command that restores it.
1582
1583 A snapshot uploads eight objects at a time. Run logs older than a day go up
1584 bundled per day (cut at 4 MB, so a long history is several objects, never one
1585 huge one); restore unpacks them. While one runs, progress shows on
1586 **Settings → Backups** and as a line in the log every 500 objects or minute.
1587 The terminal has no backups view, so it has no readout.
1588
1589 **Run a drill** on the same page (admins only) restores the newest snapshot
1590 into scratch, opens it, counts what is there and unseals a value; the page
1591 waits for it, up to 10 minutes, and the last drill shows there as one line,
1592 ending with the drill's time against its budget and each part's time, longest first ("6 min
1593 12 s of its 14 min budget: clone 4 min, download 58 s, …"), or only the time against the
1594 budget when no part was timed; drills from before this show none. The budget is 10 min
1595 plus 1 s per MB of snapshot, at most 45 min (`docs/RECOVERY.md`).
1596 The parts are download, decompress, clone, open, and start and migrate (the
1597 whole side-boot of the restored forge, process start included). The same words,
1598 slowest named, are in the drill's log line and in a red drill's card
1599 ("… (download about 8 min, decompress 40 s, …; download was the slowest)").
1600 Only one runs at a time. See `docs/SNAPSHOT.md`.
1601
1602 The forge also runs one itself every Sunday 09:00 UTC (a forge that was down
1603 then runs it five minutes after waking, once). Each snapshot's manifest keeps
1604 the five counts of its database; a restore that counts differently is red at
1605 `count` and, like any red drill, one board line and one push. Snapshots from
1606 before the counts were kept are not compared.
1607
1608 **Failure domains** (a card at the foot of `/settings/backups`; the old
1609 `/settings/resilience` sends there, and `/resilience` is the THREATS.md page): one row per failure domain from
1610 `docs/DOMAINS.md` — what breaks, the words the forge says, and the domain's
1611 last kept drill (when, green or red with its reason, seconds until back) or
1612 the sentence on why it is not drilled. In the terminal it is the **Failure domains**
1613 command (`ctrl+k`; the **Resilience** command is the THREATS.md one), shown to an admin only and read through the same
1614 `listResilience` action. Only the latest drill per domain is shown.
1615 The **Owner's needs** command (admin only, `ownerView`) is the owner's one screen: the forge's needs as the board strip says them, where snapshots go (and any mirror) with the newest snapshot, and every domain with what the owner does.
1616
1617 A red drill, or none in eight days, is one line for the forge's admin on the
1618 board's forge strip, in the terminal's dashboard (same `forgeNeeds`), and in
1619 MCP `health` and `asks`; it names the failed step and the next green drill
1620 clears it. An admin with a phone URL gets one push per cause (a new red
1621 drill, or the silence), also when eight days pass with no event: the notifier
1622 wakes every 10 minutes for it. The time the forge first owed a drill is kept
1623 at boot in `drill/first-seen`; if the disk will not take it, a warning is
1624 logged and a forge with no drill ever is not flagged.
1625
1626 Snapshots written to a directory on the forge's own volume, with no off-box
1627 mirror set, are lost with that volume. That is one line for the forge's admin,
1628 "Snapshots are kept on the forge's own volume and mirrored nowhere: losing the
1629 volume loses them too.", on the board's forge strip (linking Settings →
1630 Backups), in the terminal's dashboard (same `forgeNeeds`), in the asks, in
1631 `GET /api/v1/health` and MCP `health`. Anyone else sees nothing. It clears when
1632 the destination moves to another volume or a bucket, or a mirror is set. Every
1633 one of those reads the vitals through `forgeVitals` in `src/actions/health.ts`.
1634
1635 `docs/EXTRACT.md` is the forge on a machine of its own, and the platform it
1636 grew up in switched off: what a host owes it, the moves in order, and what is
1637 given up.
1638
1639 ### The sentinel
1640
1641 The forge hunts its own faults (`docs/CREW.md`): a hunt an hour on the
1642 autonomy clock, sooner when a held slot is new. Each finding is one issue with
1643 its evidence, filed in `FORGE_SENTINEL_REPO` (default `trevato/forge`) as the
1644 `sentinel` agent, else the first admin. Evidence is scrubbed of anything
1645 credential-shaped before it is filed. Other findings (`held`, `ledger`, `red`)
1646 are filed anew when they return.
1647
1648 A check that has never been green is filed only for the sentinel's own repository. In any other repository it stays on that repository's checks and the degraded list.
1649
1650 A capacity setting away from its default is not a finding: the sentinel files
1651 nothing for it (#3269). On `/settings/capacity` the row of each such setting
1652 carries one muted line, "changed from X N days ago by who", and its **Put back**
1653 button (in the row's own form, beside Save) restores the default through
1654 `changeCapacity` (same validation, record, event and reversal). The line stays
1655 until the value is back at the default; nothing else reminds anyone. The
1656 JSON API and MCP carry the same fact: a setting away from its default has a
1657 `changed` field (`at`, `by`) in the capacity listing (`listCapacity`).
1658
1659 The `setting:<key>` findings filed before that are all closed; none raises an
1660 ask, and the capacity page's Put back is the one answer. The `sentinel_kept`
1661 table they once used is unread; a migration drops it once the additive rule
1662 (`src/core/releases.json`) allows a drop.
1663
1664 - **Board:** a site admin's forge strip shows the open findings and the last
1665 hunt; a guest or member sees nothing.
1666 - **Terminal:** press `ctrl+k`, run **Sentinel** — the same line as a toast.
1667 Offered to site admins only.
1668
1669 ### Passports
1670
1671 A crew run's environment carries `FORGE_PASSPORT`, a 15-minute credential
1672 scoped to `health:read` for the forge alone (`src/kernel/passport.ts`, no
1673 dependency: Node crypto's HMAC and HKDF). It opens one route and nothing else:
1674
1675 curl -H "authorization: Passport $FORGE_PASSPORT" "$FORGE_URL/api/v1/passport/health"
1676
1677 Missing, expired, wrong-audience, wrong-scope or forged passports get a 401. A
1678 passport has exactly one valid encoding (padded or altered variants are refused).
1679
1680 This route proves the mechanism and is not a privilege: it returns what the
1681 anonymous `GET /api/v1/health` already returns. The next edge should guard
1682 something only a passport may reach.
1683
1684 ## Cloud accounts
1685
1686 **Settings → Cloud accounts** (admins) holds provider keys — Cloudflare,
1687 DigitalOcean, AWS, Azure, Fly, Backblaze, GitHub, Vercel — sealed and write-only. Pasting one
1688 makes the forge check it with read-only calls (listed in the header of
1689 `src/cloud/providers.ts`) and say what it can see; a key the provider rejects
1690 is not stored. It is sealed at forge scope as `CLOUD_<PROVIDER>_<NAME>` (secret
1691 names are capitals, so `cloudflare/main` is `CLOUD_CLOUDFLARE_MAIN`). The page
1692 lists names, the last check, and which run last opened each key; "Every read"
1693 lists each open, and a run's page lists the secrets it held. A run gets a key
1694 by declaring it like any secret. **Write allowed** is off by default: a run
1695 that adds `writes: yes` to its front-matter and declares a cloud key whose
1696 switch is off fails with that reason. The switch gates the forge's hand-out,
1697 not the key — a provider key that can write still can, so paste read-only keys.
1698 **Audit** reads one account and opens one `[audit]` issue per finding (unattached
1699 volumes, powered-off droplets, unassociated IPs, never-active zones) with a
1700 monthly saving estimate, skipping findings already open. It runs in the forge,
1701 not as a crew run; AWS audits look at us-east-1 only; over-provisioned
1702 droplets and forgotten DNS records need metrics or resolution and are not
1703 attempted; Backblaze, GitHub and Vercel audits find nothing yet. No dependency was added.
1704
1705 Each account can be **rotated** (the new value is checked, then replaces the
1706 old) and **renamed** (re-sealed under the new name, write switch kept). Each
1707 says in one line what it is used for now — the `cloud.json` declarations on
1708 the default branch of the first 100 repositories the admin can see, cached a minute — and
1709 carries a "bring it home" note (`HOME` in `src/cloud/declared.ts`). Every
1710 vendor request goes through `src/cloud/gate.ts`: an admin's verify or audit is
1711 audited as the operator; a hand-out of a key to a run is allowed only if that
1712 repository's `cloud.json` declares the key's vendor (`VENDOR_OF`: the provider's name, but AWS and Backblaze are `s3`), and an undeclared one is
1713 refused before the key is opened. One `cloud_audit` row per request (account,
1714 purpose, host and path, bytes out, when, allowed or refused — no token, no
1715 query) is listed at **Settings → Cloud accounts → Every vendor call today**.
1716 Rows are pruned past 90 days on every write; `at` is indexed. Left out: one
1717 token per vendor is not enforced (several names per vendor stay allowed), and
1718 an agent's own vendor calls beyond the hand-out are not metered. A forge admin reaches the same accounts in the terminal (`:` → Cloud accounts: each account with what it is used for, verified / last used and today's vendor calls; `a` attach, `v` verify, `r` rotate, `n` rename, `x` remove, `w` flip the write switch, `A` audit into an `owner/repo`, `c` every vendor call today (purpose, host and path, bytes out, allowed or refused), the token typed masked) and over MCP (`cloud_accounts` lists them with today's audit rows; `cloud_account` with `verb` attach, verify, rotate, rename, remove, `allow_writes` (with `allowed`) or `audit` (with `repo`) needs an admin-scope token). Both call `actions/cloud.ts`, neither ever shows a token, and anyone else is refused. A rename is refused when the account or the secret `CLOUD_<PROVIDER>_<NAME>` already exists.
1719
1720 ## Share to the forge
1721
1722 Install the forge to your home screen, then Share a link or text to it from
1723 any app: the home composer opens with it as a draft, and it is only sent when
1724 you tap Post. Signed out, you sign in (or up) first and land on the same
1725 draft. The share target is the web manifest's `share_target`, which only an
1726 **installed PWA on Android and Chrome** offers; iOS Safari does not.
1727
1728 The home composer's app picker also has **the forge**: that sends no tell to
1729 an app, it keeps what you typed as a thought, exactly as typed (works with no
1730 apps too). Your thoughts from the last day stand at the top of the feed as
1731 your own posts, marked "Received". The terminal has no feed or composer, so
1732 thoughts are web-only there; `POST /api/v1/thoughts` is the other way in.
1733 Within minutes the planner places each thought on the map and answers under it
1734 in the feed, saying where it went; the MCP `thought` tool returns that
1735 placement to the thought's author.
1736 A thought that says an earlier one again does not open a duplicate: it links to
1737 the earlier thought (`repeats`), comments on the issue it placed and raises it
1738 to the front of the ready order, unless that issue is deferred or closed, which
1739 the reply says. A close restatement counts only when the planner reads it as
1740 plain work and nothing in it negates or flips the earlier one; "don't make X"
1741 after "make X" is a reversal, never a repeat. A reversal links the thoughts it
1742 takes back (`supersedes`). `/asked` shows "said again, first on …", "takes back
1743 …" and "superseded by …" under each thought, and the MCP `thought` tool returns
1744 `repeats`, `supersedes`, `repeatedBy` and `supersededBy` thought ids.
1745
1746 ## Link-preview cards
1747
1748 `/og/repo/owner~name.svg`, `/og/app/slug.svg`, `/og/issue/owner~name~N.svg`,
1749 `/og/pull/owner~name~N.svg` and `/og/run/owner~name~runid.svg` are 1200×630
1750 social cards. They show only what an anonymous reader can open; anything
1751 private, sealed or unknown is the same 404.
1752
1753 An app card carries the app's latest look (the production thumb, or a desktop
1754 or phone shot taken as nobody) beside the words; a pull card carries the
1755 production look as "before" and the pull's preview look as "after" (the
1756 preview alone if production has none, nothing if the pull has no look). Only
1757 a look an anonymous reader may fetch is used, of a public app and never a
1758 sealed pull's. The picture is decoded, reduced to fit 480×300 and embedded as
1759 a data URI (no dependency; the forge's own PNG codec), and the look's id and
1760 taking name the card, so a new look is a new card. A look whose file cannot be
1761 read leaves the card as it was.
1762
1763 Two of them carry a figure beside the words:
1764
1765 - **Map** — `/og/map/owner~name.svg`: the repository's dependency graph as a
1766 thumbnail, laid out exactly as `/owner/name/map` lays it out (ready green,
1767 working solid accent, blocked grey, waiting/held purple, parked amber,
1768 deferred and done faint), the critical path in the accent; beside it the
1769 open count, the critical path's length and ends, and the next issue a free
1770 hand would take; along the foot `ready · working · blocked · waiting · done`.
1771 Drawn from what a signed-out reader sees: no sealed issue, no run on one.
1772 - **Audit** — `/og/audit.svg`: the policy score as a ring, the share of apps
1773 that hold (sound `contract.json` and no disagreeing probe; an app with no
1774 contract does not hold — 3 sound, one disobeying, one unread is 2 of 4,
1775 50%), with the top three findings worst first. Public repositories' apps only,
1776 as `/audit` shows them signed out.
1777
1778 The map page (public repositories) and `/audit` name their `.png` as
1779 `og:image` with `twitter:card=summary_large_image`; every other page keeps the
1780 forge icon.
1781
1782318 ## Develop
1783319
1784320 ```sh
@@ -1965,58 +501,6 @@ carries today, each entry under the issue that owns it; an entry that is no
1965501 longer hollow turns the check red, so the list only shrinks.
1966502 `bun scripts/hollow.ts` prints the same lines on their own.
1967503
1968 ## Following an upstream
1969
1970 A followed repository's home shows admins "N commits behind owner/name" (or "conflicts with owner/name: resolve") from the last stored check, with an **Update** button: a same-origin POST form to the settings page's check action that returns to the home, no JS. The terminal's About card (`I`) has the same words as a `behind` row, and `u` is the same update (`behindWords`, `src/actions/upstream_follow.ts`).
1971
1972 A repository can follow an outside git branch (`repo_upstream`, https or
1973 http only, no credentials in the address). The clock checks each following
1974 repository every 30 minutes (`checkDue`); the result is the follow row:
1975 upstream tip, commits behind, the pull carrying the last proposal, and
1976 `error`. In the terminal, a repository's About card (`I`) shows an
1977 `upstream` row (address, branch, last check, standing; `off` when following
1978 is off) to an admin of the repository and to nobody else. `u` checks now,
1979 while following is on, through the settings page's own action. `f` turns
1980 following on or off through `setFollowUpstream`; it keeps the saved address
1981 and branch, and when none is saved it refuses with "set the upstream address
1982 in the web settings first" (the address is set on the settings page). A check fetches only when the tip moved, into
1983 `refs/upstream-follow/<branch>` (not a branch), then:
1984
1985 - **Nothing new:** nothing happens.
1986 - **New commits that merge cleanly into the default branch:** the forge
1987 writes `upstream/<tip7>` at the fetched head and opens one pull from it,
1988 as the person who turned following on. A second check with the same tip
1989 opens nothing, and a proposal someone closed is not reopened.
1990 - **A newer tip while that pull is open:** the pull is moved to
1991 `upstream/<newtip7>` and its title and body rewritten; the old
1992 `upstream/<old7>` branch stays, as a person may have built on it.
1993 - **A conflict:** the row's `error` reads `conflict`; no branch, no pull. With
1994 autonomy on, the check also opens one issue for that head and queues one
1995 converge run for the crew to resolve it; with autonomy off, the note on the
1996 row is all there is.
1997 - **A planted branch:** if `upstream/<tip7>` already exists anywhere but at
1998 the fetched head, the check fails (`squatted`), says so on the row, opens
1999 no pull and leaves the branch as it was.
2000
2001 ## Ranked forks
2002
2003 `/:owner/:name/forks` and the terminal's forks screen (`f` in a repository)
2004 list a repository's direct forks the same way, so the best of a fork tree
2005 rises. Order: commits ahead of the parent's default branch (most first),
2006 then fewest behind, then the newest tip commit. Each row reads `N ahead · M
2007 behind` and when the fork last moved; a fork that is ahead has a **Compare**
2008 button (on the page) to its `/pulls/new`, which proposes it to the parent.
2009 Each fork's own forks are listed indented beneath it (one more level, on the
2010 page and in the terminal alike); the forks below those are a count, `and N more
2011 forks below`, linking on the page to that fork's forks page. A private fork is
2012 never named or counted to someone who cannot read it.
2013
2014 Forks with nothing ahead (or whose standing git cannot read) follow under
2015 `N forks with nothing new`. At most 60 forks are ranked per view; past that
2016 the page says `Ranking the first 60 of N forks`. A fork's standing is cached
2017 by the pair of tips (`actions/forks.ts`), so a view of forks that have not
2018 moved costs one `git log` per fork.
2019
2020504 ## Shape
2021505
2022506 ```
@@ -2164,166 +648,58 @@ a cookie — even that works script-free. Still **zero runtime dependencies**.
2164648 Not yet: containers around apps, custom domains, and a gate that lands a
2165649 pull on its own once review and checks agree.
2166650
2167 ## Fuzz and chaos
651 ## Where the rest is

This index and the 60 KB limit are the point of the pull, and no test protects them. Add a test asserting README.md < 60 KB, every docs/ link here resolves, and every top-level docs/*.md is listed here. Without it the next feature pull that appends a paragraph quietly undoes this one.

2168652
2169 Invariants are proven by faults, not by hope. No dependencies were added.
653 This page is the front door. Everything else is a file of its own under
654 `docs/`, each about one thing:
2170655
2171 - **Fuzz** (`test/fuzz-parsers.test.ts`, helper `test/helpers/fuzz.ts`): the
2172 manifest and check parsers, `contract.json`, hostnames, slugs, markdown, the
2173 diff reader and the API's JSON bodies take a seeded generator's cases. A
2174 failure prints `FUZZ_SEED` and the shrunk case; rerun with
2175 `FUZZ_SEED=<n> FUZZ_RUNS=<n> bun test test/fuzz-parsers.test.ts` and keep the
2176 case as a fixed test in `test/fuzz-found.test.ts`.
2177 - **Chaos** (`src/chaos/`): a side instance on a fresh temporary data directory
2178 (it cannot name the live forge or a customer's app) takes four faults —
2179 SIGKILL mid-run, a dropped SQLite connection, a temp area that refuses writes
2180 (a stand-in for a full disk; nothing is actually filled), a delayed loopback —
2181 and after each asserts: no run vanished and all are in a known state, the
2182 database is whole and every open pull's head is in its repo, a snapshot taken
2183 afterwards restores, no sealed value is in any file in the clear.
2184 `test/chaos.test.ts` runs a small version in the gate (`CHAOS_SEED=<n>`).
2185 - **Nightly**: `bun src/chaos/run.ts [--seed N] [--rounds N] --report` with
2186 `FORGE_URL`, `FORGE_TOKEN`. A broken invariant files an issue (fault, seed,
2187 log) and stands it under the umbrella `FORGE_CHAOS_PILLAR_ISSUE` (the
2188 P0 Trust issue waits for it); a quiet night comments one line on
2189 `FORGE_CHAOS_RECOVERY_ISSUE`. Scheduling it is the operator's: this repo
2190 ships the command, not a timer.
656 **Using it**
2191657
2192 ## Check tiers
658 - [`docs/VISION.md`](docs/VISION.md) — what the forge is for, and the order of work.
659 - [`docs/STUDIO.md`](docs/STUDIO.md) — the front door in full: the five words and where each is decided, the box, the three lists, a tell, a run and a change each on its own page, and the board.
660 - [`docs/TOUR.md`](docs/TOUR.md) — every other page and screen, with each of its doors: the terminal over SSH, the home and its figures, review, signing in, the page-view beacon, the board's notes, long threads, who can see a repository, archive, where a repository came from, big directories, search, sharing to the forge, link-preview cards and choosing among variants.
661 - [`docs/ASKS.md`](docs/ASKS.md) — what waits on you and how to answer it: waiting pulls, mutes, wave proposals, feed chips and the decider.
662 - [`docs/FEED.md`](docs/FEED.md) — the feed: what happened, ranked, each item with why it is there.
663 - [`docs/PULLS.md`](docs/PULLS.md) — resolving a conflicted pull without a clone.
664 - [`docs/GIT.md`](docs/GIT.md) — cloning over HTTPS and SSH, and partial clones of big repositories.
665 - [`docs/ROLES.md`](docs/ROLES.md) — guest, member and admin, and what each may do.
666 - [`docs/MCP.md`](docs/MCP.md) — the forge as a connector; [`docs/STANDARDS.md`](docs/STANDARDS.md) is what it answers by default: `.well-known`, feeds, cards, the manifest.
2193667
2194 `forge.json`'s `check` may declare `fast` beside `run` (the full gate; `full` names it explicitly). `bun scripts/test-shards.ts --tier fast --diff <base>..<head>` runs the tests that touch the diff (changed, named for a changed source file, or importing one within two hops), every test the times table records under 1 s, and any named by `--learned a,b`. `src/checks/tiers.ts` decides; it adds no dependency.
668 **Apps and the crew**
2195669
2196 In the forge itself: a pull's check runs `fast` when the manifest has one, in a clone holding the fork point and the head as `FORGE_CHECK_BASE` and `HEAD` (`origin/main` is not a ref there), with `FORGE_CHECK_LEARNED` naming tests earlier land attempts taught the pull (`src/checks/history.ts`, `learned.ts`). Landing (`landIfGreen`) runs `full` once on the merge with its base before merging (`src/checks/landing.ts`); red comments on the pull with the tests named, is remembered per head and base, and the files ride the pull's next fast run. The pull page's checks line shows the tier words. While a check runs, the line (page and terminal, one reader: `runningProgress`, `src/checks/progress.ts`) adds `61 of 170 files · 812 passed, 0 failed`, taken only from the shard runner's own log lines and left out when not yet known; on the page it is a live region (`GET …/pulls/:number/checks-line`) that re-asks every 5 seconds only while the check is queued or running; the same line is drawn on the studio's pull request page (`/pulls/:number`), and the terminal's pull screen re-reads on a 5-second tick while the check is queued or running. The checks line (page and terminal) also names the base tip the fast check merged with ("checked against main at abc1234"); `GET …/pulls/:number/checks` returns it as `run.baseOid`, empty for a default-branch push and for the `full` land gate. When the base has moved since that green, the page's line reads "Checked against an older main" (a clock, not a green mark) and the terminal's reads `checks: passed against an older main`. `GET /api/v1/repos/:owner/:name/pulls/:number` and the MCP `pull` tool carry `checkStale`, true only for an open, unmerged pull whose last check recorded a base that is no longer the base branch's tip. With `require_checks` on, such a stale green counts as a check still to come (`checksHold` in `src/checks/stale.ts`): the page reads "Waiting on checks" and disables **Merge pull request**, the terminal refuses `m` the same way, and the inbox's `look` ask and an autonomous repository's `review` ask wait for the re-check. The ask side works from the stale-ask sweep's last read of the base tip, so it drops after the next sweep, not the instant the base moves. `mergePull` itself does not yet refuse a stale green (API and MCP merges are not held).
670 - [`docs/APPS.md`](docs/APPS.md) — the contract for apps, previews and runs: making an app, the vocabulary, and the rules that are mechanical rather than advisory.
671 - [`docs/CONTRACT.md`](docs/CONTRACT.md) — who may do what, as a table: an app's `contract.json`, the one evaluator every app decides with, the audit that reads it from git, the probe that knocks on the running app, and the wall a person alone crosses.
672 - [`docs/WEBSPEC-PREVIEWS.md`](docs/WEBSPEC-PREVIEWS.md) — the website specification run over every preview, report-only.
673 - [`docs/CREW.md`](docs/CREW.md) — the crew: agents, how a run works, review, live triage, the merge queue, spend, roles and invites, the sandbox, passports, and the direction a vision pull will write.
674 - [`docs/SENTINEL.md`](docs/SENTINEL.md) — the forge hunting its own faults; [`docs/HUNT.md`](docs/HUNT.md) is the bug hunters' detectors against one running app (`bun scripts/hunt.ts <origin>`).
675 - [`docs/FLOW.md`](docs/FLOW.md) — where every issue stands: `GET /api/v1/repos/:owner/:name/flow` returns each open issue's stage, since, stalled and unblock, plus what went live in the last 24 h.
676 - [`docs/PLANNING.md`](docs/PLANNING.md) — how issues depend on issues: the edge, the map, and what "ready now" means to a person and to the crew.
677 - [`docs/FLEET.md`](docs/FLEET.md) — one change across many repositories, runs or issues: a query, a dry-run plan of every row before → after, a receipt, and undo.
678 - [`docs/HOME-EVAL.md`](docs/HOME-EVAL.md) — scoring the owner's home with a reader who has no context.
679 - [`docs/PROFIT.md`](docs/PROFIT.md) — the profit slider: cost against responsiveness.
2197680
2198 A check's run page (`/:owner/:name/runs/:id`) opens with three rows, install, typecheck and tests, each `waiting`, `running`, `passed`, `failed`, `stopped` or `unknown`, always present so nothing shifts (`checkSteps`, `src/checks/summary.ts`). The log has no step boundaries, so they are inferred from it: the echoed `$ command` lines and the shard runner's banner (typecheck precedes it). A log longer than the 512 KB window whose tests cannot be seen reads `unknown` rather than wrong; readers who may not see the log get no rows; while the run is open the page fetches the rows again when such a line lands and when the run ends. No per-step duration is shown: the log carries no timestamps. The terminal's pull screen appends the same rows to its checks line (`install passed · typecheck passed · tests running`, `stepsLine`, `src/checks/progress.ts`), read through the same `checkSteps` and withheld from a reader who may not read the log.
681 **Other forges, and the forges this one spawns**
2199682
2200 Under the rows, once the tests have begun, the run page shows a progress bar of files started out of the banner's count, the passes and failures so far, the file(s) of the live shard running now, and each failure pinned with its first three lines of output (`parseTestsRow`, `src/checks/tests-row.ts`). It reads only the shard runner's lines and `bun test`'s `(fail)` markers; a figure the log does not give is left out. Batched shards (run side by side) report whole when they finish, so they name no running file. The words line and a slot for the pins are always rendered (the slot reserves height only while the run is open, and scrolls past 9em) so the log never shifts; while the run is open the bar is named "Test files started" and is indeterminate (no value) when the count is unknown, and once the run has ended an unknown count shows no bar; only the rows, the bar and the words are a live region, the pins are outside it, and a slot with pins is a focusable labelled region the refetch patches in place. The terminal's checks line appends the same: `tests 2 of 9 files started · 812 passed, 1 failed · failing: <names>` (`testsRowLine`, read in `stepsLine`, withheld from a reader who may not read the log).
683 - [`docs/ORIGINS.md`](docs/ORIGINS.md) — one workbench over GitHub, Forgejo and Gitea: connecting, mirroring, writing through, migrating, and following an outside branch.
684 - [`docs/SPAWN.md`](docs/SPAWN.md) — a forge births a forge: the claim link, the family, sizes and the child quota.
685 - [`docs/FOLLOWING.md`](docs/FOLLOWING.md) — a fork that keeps up with its parent: where it stands, taking updates, the crew's part when both sides moved, and ranked forks.
2201686
2202 Below them the run page has a per-look web-vitals table: LCP, CLS and TBT (`webvitals()`, `src/release/webvitals.ts`) each beside its good/poor thresholds and grade, a vital with no recorded entry as `n/a` (never zero), and a look run without a browser as one `skipped: no browser` line (`vitalsRows`, `src/checks/summary.ts`, as plain words; `vitalsTable`, `src/web/studio/vitals.ts`, escapes and renders them as a dense `.table` in a `.table-scroll` wrapper, so a phone scrolls it sideways). It is report-only: it has no verdict, so nothing in it can turn a check red. The terminal's checks line appends the same as `vitals home: LCP 3100 ms needs-improvement, …` (`vitalsLine`, `src/checks/progress.ts`, via `stepsLine`, withheld with the log). Both read one file beside the run's log, `runs/<id>.vitals.json` under `FORGE_DATA` (`src/checks/looks.ts`): an array of `{look, entries}` or `{look, skipped}`, validated and bounded on read (bad JSON, a file over 1 MB, or a malformed look is dropped). The looks run writes it (`startLooks`, `src/looks/service.ts`): the browser it already launches for a look (`src/looks/capture.ts`) opens on `about:blank` with a loopback DevTools port, installs a `PerformanceObserver` for `largest-contentful-paint`, `layout-shift` and `longtask` before the page's first script, navigates through the grant door, and after load reads the entries back and takes the picture over the same socket (no new process, no dependency, no Lighthouse; a type the browser lacks is an empty list). The phone and desktop looks are filed under the commit they picture (`runs/commit-<repo>-<oid>.vitals.json`, `writeCommitLooks`), so they become rows of that commit's check whenever it exists, even if it was made after the look (a run's own `runs/<id>.vitals.json` wins); with no browser they are `skipped: no browser` and the check is untouched. Until a check has a file, the table and the clause are absent.
687 **Running it**
2203688
2204 While the default branch's own last check is red, the forge holds its pulls' checks queued rather than fail each one on the same test (`src/checks/red_main.ts`): the vitals carry one card naming the failing test and how many checks wait, and main going green releases them. A check a person asks for again runs at once, and no check is held more than 20 minutes, so the pull that fixes main can always show green.
689 - [`docs/OPERATING.md`](docs/OPERATING.md) — configuration, health, how a release proves itself, flags, limits, check tiers, fuzz and chaos, and writing a database guard.
690 - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — the principles and the reasoning behind the shape above.
691 - [`docs/CACHING.md`](docs/CACHING.md) — what the forge keeps between requests to make `/map` and `/` fast: each map lane's plan and each viewer's feed candidates, stamped with SQLite's own write clock so any write is seen on the next read, keyed so one person's view never reaches another, and the budgets `test/snappy.test.ts` holds. `bun run bench/snappy.ts` prints p50/p95 and SQL statements per request for both pages, warm and cold. The content-addressed assets answer a revalidation with `304`.
692 - [`docs/METRICS.md`](docs/METRICS.md) — `GET /metrics` for Prometheus.
693 - [`docs/THREATS.md`](docs/THREATS.md) — who would attack the forge, what each wants, and the test behind every protection (`/resilience` reads it).
694 - [`docs/DOMAINS.md`](docs/DOMAINS.md) — the ways the forge stops, and what it says when each does.
695 - [`docs/SNAPSHOT.md`](docs/SNAPSHOT.md) — a forge back from a copy: what a nightly snapshot holds, where it goes, the drill, and the one command that restores it.
696 - [`docs/RECOVERY.md`](docs/RECOVERY.md) and [`docs/RESTORE.md`](docs/RESTORE.md) — the forge back at 3 a.m., and on a fresh machine; print them.
697 - [`docs/CLOUD.md`](docs/CLOUD.md) — clouds are capacity, never dependency, and the cloud accounts an admin keeps.
698 - [`docs/CLUSTER.md`](docs/CLUSTER.md) — the check lane as Kubernetes Jobs.
699 - [`docs/EXTRACT.md`](docs/EXTRACT.md) — the forge on a machine of its own, and the platform it grew up in switched off: what a host owes it, the moves in order, and what is given up.
700 - [`docs/ops/ROLL.md`](docs/ops/ROLL.md) — rolling a release.
2205701
2206 `GET /api/v1/repos/:owner/:name/commits/:oid/check` answers the default branch's check of one full commit id, `{commit, checkedCommit, state, runId, summary, finishedAt}` with `state` `ok`, `failed`, `pending`, `superseded` (every check of it was cancelled, as when main moved on before it ran, and none will follow) or `none`, to whoever may read the repository's runs (`src/checks/commit_check.ts`). When the commit's own check is `superseded` or `none`, the newest green (or else running) check of a later main commit that contains it answers instead, and `checkedCommit` names that commit: main moving on never holds a release (#3966). It is half of the forge's release gate: the release workflow builds only once it reads `ok` for the tagged commit, and the build boots the image it made (`docs/ops/ROLL.md`, "The gate of record").
702 **Design and records**
2207703
2208 A test file the shard gate clears because it passed alone is still a failure: when the default branch's own check log says `fails only beside others: <files> (shard-mates: <files>)`, the runner opens one issue per file (at most 5 per run, the rest named in the log as not filed) or comments on the one already open. The issue is authored as the repository owner and its body says it was filed by the check runner. A pull or other branch's check files nothing.
2209
2210 ## Flags
2211
2212 A forge admin sees every declared flag at `/settings/flags` (Settings → Flags): why it exists, its default, the owner issue and its live value. Set it for the whole forge, for listed user ids or roles, or for a percentage of people; Clear returns it to the default. A change is validated, kept in the change record and live on the next request, with no restart. In the terminal the same admin opens the command palette (`ctrl+k`), picks Flags, and presses enter to set a flag from a line such as `forge=on users=u1,u2 roles=admin percent=30` or `x` to clear it; an agent is never an admin there either.
2213
2214 Settings → Usage is one page for a forge admin, opening at `/settings/spend`, with four tabs: Spend, Routes (`/settings/usage`), Capacity (`/settings/capacity`) and Telemetry (`/settings/telemetry`); each tab keeps its own URL and permission. Spend is admin-only (anyone else gets 404) and holds what the crew's runs spent, by credential, app, agent, issue and run kind; it used to sit on Subscriptions, which now links to it. Routes lists every route the router knows: its requests over the last 30 days, by role and by surface, and the hour of its last use. Routes with no request in the window are listed first, never-used ones before the longest-unused, as candidates to drop; the rest follow by count. Counts are per pattern, not per method. `usage` is also a boards source, for an admin only (an agent is refused); its `hour` column is text like `2026-10-16T13`, so group by a column such as `pattern` or `role`, not by a time bucket. In the terminal the same admin opens the command palette (`ctrl+k`), picks Usage, and reads the Routes list only — the spend breakdown, capacity and telemetry tabs are web-only, and the terminal has never shown spend; an agent is never an admin there either.
2215
2216 ## Limits
2217
2218 Every limit the owner can hit is declared in `src/settings/limits.ts` with its default, bounds and reason, and changed live by a forge admin at `/admin/limits`, `PUT /api/v1/admin/limits/:key` or the `limits` MCP tool (never by an agent). Each change is kept with who made it.
2219
2220 Per-account quotas (`src/settings/quotas.ts`) — how many `repositories` and `apps` one account may own, how many `previews`, `runs` and `checks` it may have going at once — are set the same way: **Account quotas** on `/admin/limits`, `GET`/`PATCH /api/v1/admin/settings` (`{"quotas": {"repositories": 5, "apps": null}}`) or the `quotas` MCP tool (dry by default, admin scope, never an agent). A value is a positive whole number, or empty/`null` for no limit, which is the default; anything else is a 422. Every change is kept with who made it and listed beside the form. The `apps` limit is enforced (`src/actions/app_limit.ts`): with one set, an account owning that many apps is refused one more, unless the actor is a forge admin; with none set nothing is asked. The check and the insert are separate statements, so two concurrent creates can pass by one. The refusal reads "This account is at its limit: 3 of 3 apps. Remove one to make room." and each door answers it its own way: the API (`POST /api/v1/apps` and `POST /api/v1/repos/:owner/:name/app`) a 403 `{"error": "app_limit", "message", "limit", "count"}`; the new-app page and the app settings form a 422 with the sentence on the form; make-it-yours the same sentence as an invalid error, before the fork, so no repository is left behind. The `repositories` limit is enforced too (`src/actions/repos.ts`): with one set, an account owning that many repositories (sealed forks are not counted) is refused one more by create, fork, make-it-yours, mirror, migrate and new app, before anything is made on disk. Forge admins and agents are exempt, and creating under an organisation is not counted: the quota is per account, so only a repository the actor owns itself is asked. The refusal reads "2 of 2 repositories"; the API answers 403 `{"error": "repository_limit", "message"}`, the pages say "Not made: 2 of 2 repositories." on the form (a 422), and the terminal shows it as a toast. The check and the insert are separate statements here as well, so two concurrent creates can pass by one. The `storage` limit (megabytes, `src/core/storage.ts`) bounds the bare repositories an account owns plus its apps' data: a push, create, fork, make-it-yours, mirror, migrate or new app by a non-admin, non-agent account at the limit is refused before anything is written (the account asked is the owner the repository would belong to, an organisation included); a push is refused before git runs, as a git error over HTTP and SSH ("storage limit reached: 1.9 GB of 2 GB"), and a push that would pass it is capped inside git (`receive.maxInputSize` = the room left) so it is stopped in quarantine and the repository is as it was. Every other door answers a refusal as `storage_limit`, not `repository_limit`: the API a 403 `{"error": "storage_limit", "message"}` with the message "…storage limit: 1.9 GB of 2 GB", the pages "Not made: …" on the form (a 422), the terminal a toast. A mirror's periodic sync fetch and a migrate import mark the figure stale without walking the disk, so the next check measures again and no account can mirror past the limit unseen; the one fetch that crosses the limit is let in, the next create is refused. The figure is cached per account in memory and re-measured in the background after each push, walked on the request path only when it must be: the first look at an account waits for one walk, and so does the first check after a push, create, fork, mirror or delete changed the disk (a walk already under way is joined, not repeated). An account found at its limit is measured again before it is refused, so a deletion frees it with no restart, but at most once every 5 seconds, so retries by a locked-out client do not scan the disk. Two pushes at once can each pass. Three more limits bound what an account has going at once, each unset by default and never asked of a forge admin: `previews` (live previews), `runs` (concurrent agent runs) and `checks` (concurrent checks), counted across the account's repositories and asked before the thing starts. A preview past its limit does not start: its deploy run — the check the pull shows — fails with "not started: 1 of 1 live previews", and a sleeping preview stays asleep when woken at the limit. A run or check past its limit stays queued, with "queued, not started: 1 of 1 concurrent runs" (or "concurrent checks") in its log, and starts when one of the account's own ends; none is dropped. An admin sets the three running limits exactly like the other two: the `previews`, `runs` and `checks` fields on `/admin/limits`, or `{"quotas": {"previews": 3, "runs": 2, "checks": 1}}` to `PATCH /api/v1/admin/settings`. In the terminal the same admin opens the command palette (`ctrl+k`), picks Account quotas, and sees every key with its limit and last change; enter sets one (empty for no limit), `x` clears it, both through the same validation.
2221
2222 Crew teams (`src/core/teams.ts`, `src/actions/teams.ts`) — a named group of agents with a charter (what it owns, what it reviews) and a dial (a whole number from 0 to 100, its share of builder slots and budget) — start as four: design, data science, security and platform, dials at 0. A forge admin who is a person sets each team's charter, dial and members at `/settings/teams` (linked from the Crew page), with `GET /api/v1/admin/teams` and `PUT /api/v1/admin/teams/:slug` (`{"charter", "dial", "members"}`, each optional, a left-out field is kept; members are agent ids), the `teams` MCP tool (dry by default, admin scope), or **Crew teams** in the terminal palette (enter sets the dial, `c` the charter, `m` the members). The dials of all teams together may not pass 100%; an agent may be on several teams, whatever its model. A refusal carries the code `bad_dial`, `bad_charter`, `over_100` or `bad_member` and the same sentence on every door: the API a 422, the page a message on the form, the MCP tool an error (its dry run answers the refusal the real call would, not a plan), the terminal the sentence in the prompt. Who is let in differs by door: the page and its stream answer a non-admin or an agent the 404; the API answers a non-admin a 403 and an admin agent a 404; the MCP tool says `not_found` to both. Each change is kept with who made it (listed on the page) and appended as a `team.changed` event for the audit; an open `/settings/teams` follows it over `GET /settings/teams/stream` (a save from any door updates the total, the cards and the change list in place, leaving a card alone while one of its fields has focus). **Dials at work** (`src/crew/team_share.ts`): an issue belongs to the team whose slug is one of its labels (`design`, `security`, …). Dispatch orders queued agent runs so each team's share of the last day's starts follows its dial; unlabelled work takes the share the dials leave, and with every dial at 0 the queue runs oldest first as before. A planning run is told each team above zero, its dial, its charter and how many labelled issues are open, and is asked to plan and label work for every one of them. A zero-dial team's label owns nothing extra: its issues count as unlabelled, so they are not starved. Not yet: no page, terminal or MCP view shows the effective share; fairness counts every agent run started (planner and reviewer runs too), not builders only; the dial does not limit budget, and nothing but the planner's brief enforces that each team has planned work each day.
2223
2224 Cross-team work (`src/core/issue_teams.ts`, `src/actions/issue_teams.ts`) — an issue names up to two teams, and the pull whose body closes it (`Closes #N`) is sent to review once per named team. A writer names them on the issue page (Details → Teams), with `PUT /api/v1/repos/:owner/:name/issues/:number/teams` (`{"teams": ["design", "data-science"]}`; `GET` reads them back; a non-list is a 400, more than two or an unknown slug a 422, a mirror a 409), with the `issue_teams` MCP tool, or with `t` on an issue in the terminal — all through `nameTeams`, so the refusals and their sentences are the same. For each named team without a current verdict, a crew reviewer who is a member of it gets its own run (one reviewer on both teams gets one run, and their verdict answers both). The pull page and its Files step show each team's verdict, or that it is awaited; the terminal's pull screen shows a row per team in the Merge box (`Design: approved by quill`, `Data science: awaited`, with `(on an older head)` when the verdict is stale). A team nobody on the crew can ask is not a reason to skip review: when no named team has a reviewer here (teams start empty) the ordinary single review is sent, and when only some do, the rest wait and lapse to a person with the hour like any review. Not done: a team's approval is shown but not yet required by the merge gate.
2225
2226 The forge setting `signup` (`src/settings/signup.ts`) is `open`, `invite` or `closed`, and `open` is what a forge with nothing stored reads as. A new forge — a fresh data directory or a spawned child — stores `invite` when it first boots; a forge that already has a stored value, or has run before without one, keeps what it had (no migration rewrites it). The seeding is done by `createApp` right after it makes the database, so a crash between those two steps would leave that forge `open`. **A new forge therefore refuses its first signup too**: make the first admin with `--first-admin` (a spawned forge's claim does it for the child), or set `FORGE_SIGNUP=open` before the first boot. A forge admin who is a person changes it live — **Signup** on `/admin/limits`, `PATCH /api/v1/admin/settings` (`{"signup":"closed"}`) or the `signup` MCP tool (dry by default, admin scope, never an agent) — with no restart; any other value is a 422. `FORGE_SIGNUP` sets it at boot, and a wrong value stops the boot with a plain message; while it stays set, every restart or deploy writes it again over a change made on the page (kept as a change by `boot`), so unset it to let the page decide. Every change is kept with who made it and when, and listed beside the form. While signup is `closed` (and `invite`, for anyone without an open invite link), `/signup` is one sentence with no form, the API and passkey signup answer 403 `signup_closed`, the terminal offers no Create account and refuses (with `invite` it asks for the token instead), and a first edge sign-in for a new name is 401. Existing accounts still sign in. A fresh forge's first account is held to the setting too; the operator's `--first-admin` link and a spawned forge's claim make the admin whatever it says.
2227
2228 ### A cell's memory ceiling
2229
2230 A check, test or app cell asks for memory in `forge.json` (`check.limits.memory`, `app.limits.memory`, MiB) and gets it up to the machine's ceiling. The ceiling is `FORGE_CELL_MEMORY_MAX_MIB` when set; otherwise 75% of the pod's `/sys/fs/cgroup/memory.max`; otherwise 4096 where the host states no limit. A declaration above the ceiling is held to it, and a check's log shows (before its command runs) `warning: forge.json declares memory 8192 MiB; this machine grants at most 4096 (…)`. A cell stopped for memory says whether its limit was the ceiling, `forge.json` or the default. The warning is for checks only: an app declaring `app.limits.memory` above the ceiling is held to it without a line saying so, and the settings page does not show the cut either (#3134).
2231
2232 ## Choosing among variants
2233
2234 A tell with `variants: n` starts n builders whose pulls share a variant group.
2235 `/:owner/:name/variants/:group` is the deck: one card per open sibling with its
2236 look and preview, linked from each sibling's change page. A writer likes or
2237 dislikes each (plain form posts; on touch a mostly-horizontal swipe, with a
2238 confirm before a dislike), through `POST /api/v1/repos/:owner/:name/pulls/:number/variant`
2239 (`{"answer","reason"}`), the `variant` MCP tool, or `+`/`-` on a pull in the
2240 terminal. Each person has one vote per pull, kept as a `lean` row in the
2241 decision ledger. A dislike closes the pull and leaves its one-line reason as a
2242 comment the builder reads; the last open sibling shows Ship. Readers see the
2243 cards and no buttons.
2244
2245 While two or more siblings are open and none has shipped, a writer's feed holds
2246 one **Choose** item for the group ("choose among N open variants of …") that
2247 links to the deck, whatever the number of siblings; it is never muted away.
2248 Once a sibling ships or all but one close, the item leaves the feed. The
2249 terminal has no feed; the item is in `GET /api/v1/feed` and the MCP feed tool
2250 as kind `choose`.
2251
2252 ### OG cards as PNG
2253
2254 `/og/{repo,app,issue,pull,run,map}/<ref>.png` and `/og/audit.png` are the same public cards as the `.svg`,
2255 rasterized (1200×630) for the link previews that refuse SVG. Same refs, same
2256 404s, same ETag-per-inputs (suffixed `-png`). Each PNG is rendered once and kept
2257 under `$FORGE_APP_DATA/og-png/<sha256 of the inputs>.png`; the cache holds at
2258 most 512 files (`OG_PNG_CAP` in `src/web/og/png-cache.ts`), least recently used
2259 out first. Cold is about 120 ms, warm a file read. If the rasterizer will not
2260 load or render, the route answers 503 with `retry-after: 30` and logs; nothing
2261 else notices, since the rasterizer is only loaded on the first PNG request.
2262
2263 **Dependencies (one, vendored, not installed):** `package.json` `dependencies`
2264 stays empty — the preview has no install step, so the files are committed under
2265 `src/web/og/vendor/`, unmodified.
2266
2267 - *resvg* (`@resvg/resvg-wasm` 2.6.2, `resvg.mjs` + `resvg.wasm`): an SVG
2268 renderer compiled to WebAssembly, so no native binary or platform build. Why:
2269 rasterizing text and gradients correctly is not a 20-line file. 2.4 MB wasm +
2270 17 KB glue. Licence MPL-2.0.
2271 - *DejaVu Sans, Sans Bold, Sans Mono, Sans Mono Bold* (2.37): the fonts the PNG
2272 is drawn in, so its text is the same on every host (the SVG names system
2273 fonts; the card clamps text with a deliberately wide estimate, which these
2274 fit). 2.0 MB. Free licence, `vendor/DEJAVU-LICENSE`.
2275
2276 Not done: the SVG route is unchanged and still the card's source of truth; the
2277 rasterizer runs on the main thread (about 100 ms once per distinct card).
2278
2279 Every read of a flag is counted in memory by UTC day and variant (on or off) and written in one batch every ten seconds and at exit, so a request runs no extra query. Beside each flag, Settings → Flags shows its reads per variant for the last 7 and 30 days. When one variant leads, "Make on/off the default" sets the flag to that value for everyone (user, role and percentage rules are cleared), and opens an issue on the forge's repository asking the crew to remove the losing path and the flag; if the issue cannot be opened, the flag is left as it was. The button shows only when the 30-day leader has at least 50 reads in all, the variants are not tied, and the flag's forge value is not already the leader. In the terminal the Flags screen shows the same `Uses, 7 days / 30 days` line for the flag under the cursor, and `w` does the same as the button (same admin, non-agent gate). Counts are of reads, not of people.
2280
2281 ## Writing a database guard
2282
2283 A trigger's `RAISE(ABORT, '…')` is answered as a 403 to every caller,
2284 anonymous ones included. Start the message with `forge: ` and write it for
2285 anyone: that is what is shown, verbatim. Any other message is answered with
2286 `refused` and logged in full (`refused by the database`, `reason=…`) for the
2287 operator. (The three guards of migrations 62, 70 and 81 predate the prefix and
2288 are known by name in `src/http/refusal.ts`.) The terminal's toasts follow the
2289 same rule. A guard installed by hand without the prefix answers `refused`
2290 until it is re-created with `forge: ` in front of its message.
2291
2292 `reach` is a boards source for an admin only (anyone else gets 403, an agent too): `visitors` per day, `lcp_p75` and `inp_p75` per route per day, `server_p50` and `server_p95` per route per day (the beacon's time to first byte, the only per-route server timing the forge keeps), and `accounts_active_1d`, `accounts_active_7d` and `accounts_total` as of now. Filter by `metric`, group by `route` or a time bucket. Percentiles are nearest-rank within one route and day, so a `max` or `avg` over several days is a summary of daily percentiles, not a percentile of the period. Account activity comes from a session's last-seen time, so it is a count as of now, not a history. No dependency was added.
2293
2294 A client's first screen offers "Ask for something" on each app they may direct
2295 (`mayDirectCrew`): on the web a link to `/:owner/:name/tell?from=/&ask=1` (no
2296 script), in the terminal the `t` key on the app under the cursor. Both go
2297 through the same `tell()` and answer "Sent. The agents will pick it up."; the
2298 web returns to `/` with that line.
2299
2300 An app that is running also shows "Open" (web: a link to
2301 `/:owner/:name/app/production/open`, the apps page's own) and, in the terminal,
2302 a "(running, o opens it)" marker with `o` showing its live address to open or
2303 copy. Both read `live` on the row from `clientAppRows`, which is set only while
2304 production runs; an app that is not running offers neither and says nothing
2305 about why.
2306
2307 `GET /metrics` (site admin only; repository names are in the labels) serves
2308 `forge_egress_connections`, `forge_egress_bytes_in` and `forge_egress_bytes_out`
2309 per `repo` and `destination` as Prometheus text, so a leaked upstream socket
2310 shows as a gauge that never falls (#3719).
2311
2312 The here-manifest (`GET /api/v1/here?path=…`) reads the same for guests and
2313 members. On `/explore` and `/search` it names the page's own read, `?q=` carried
2314 over and encoded: `GET /api/v1/explore?q=…` (MCP tool `explore`) or
2315 `GET /api/v1/search?q=…` (MCP tool `search`), listed once.
2316
2317 ### Child quota
2318
2319 `FORGE_CHILD_CPUS`, `FORGE_CHILD_MEMORY_MIB` and `FORGE_CHILD_DISK_MIB` declare
2320 what all children together may hold; each is the sum of the sizes in the spawn
2321 ledger (`spawn/sizes.ts`). **Unset means no quota.** A spawn that would pass a
2322 declared limit is a 409 `refused` with one sentence naming it, and the New
2323 forge page shows that sentence above the form when even a small forge would
2324 not fit (`spawn/quota.ts`).
2325
2326 ### Seeing the direction a vision pull will write
2327 An open vision pull's page shows, to an admin of the repository, "Direction after the merge": today's direction beside the one an admin's merge will write, computed by the same `nextDirection` the merge uses (`src/crew/vision.ts`), so the two cannot differ. Repository settings (Crew) marks the direction's fenced block `follows docs/VISION.md` and the words around it `the owner's`. Today and After the merge stack vertically at every width.
2328
2329 The same read is a second door: the MCP `vision` tool takes an optional `number`, and for an admin returns `preview` (`{ today, after }`, or `{ why }` the direction would stay) for that open vision pull, plus `directionParts` (the direction cut into `vision` and `owner` parts). The terminal (TUI) has no such view; it is not touched by this change.
704 - [`docs/DESIGN.md`](docs/DESIGN.md) — the design system's spine; its sections are under `docs/design/sections/`.
705 - [`docs/SPIKE-CLOUDFLARE.md`](docs/SPIKE-CLOUDFLARE.md), [`docs/AUDIT-MANY-HANDS.md`](docs/AUDIT-MANY-HANDS.md), [`docs/BACKLOG.md`](docs/BACKLOG.md) and [`docs/research/`](docs/research/) — a spike, an audit, a backlog and research notes, kept as written.
docs/APPS.md +219 −1
@@ -14,6 +14,222 @@ using it need not be a developer. They know what an app is, they can hold
1414 asks nothing more of them than that. Developers and agents use the same
1515 primitives underneath; nobody gets a second system.
1616
17 ## Making an app, and what it does
18
19 A repository that runs. Press **New app**, give it a name and — if you like —
20 a sentence about what it should do. Seconds later it is serving at
21 `http://<name>.<your forge's host>` from a small, real app (notes in a SQLite
22 file), and if you named an agent, the sentence became issue #1, assigned.
23
24 Under the box, four starter sentences are plain links (no script):
25 `/apps/new?description=…` fills the box with that text (plain text up to
26 200 characters, anything else is ignored; always escaped), and
27 `&owner=` keeps the chosen owner. The list is `src/web/pages/apps-starters.ts`.
28
29 - **Private egress is a site admin's yes.** An app reaches a private
30 destination (a tailnet or LAN `host:port`) only when `forge.json` `egress`
31 names it exactly and a site admin has granted it: on the
32 app's settings page, or in the terminal (About `I`, then `e`, `g`
33 to grant, `x` to revoke). The rule is under "Rules that are mechanical" below.
34 - **Egress connections are bounded.** The proxy closes the upstream socket
35 the moment the cell side closes, errors or stops, and the other way round.
36 An app holds at most 8 open connections to one destination (the ninth
37 gets a `429`, logged), and a connection that moves no bytes, either way, for 5 min is
38 cut, so a tunnel quiet for longer than that (a websocket or SSE stream with
39 very long gaps) needs a keepalive. Open connections per app and
40 destination, with the bytes moved, are on the Capacity page
41 (`/settings/capacity`, admins) and in the terminal: About `I`, then `c`
42 (site admins).
43 - **Production is the default branch.** The only way code reaches it is that
44 branch moving, and an agent cannot move it: the crew proposes through
45 pulls, a person merges.
46 - **Every open pull gets a preview** at `<name>--<pull number>.<host>`,
47 redeployed on each push and gone when the pull closes. Its data starts as
48 a copy of production's — copy-on-write where the filesystem can — and
49 never writes back. Merging moves code and touches no data.
50 - **A preview is held to the website specification, report-only.** Once it is
51 live, `webspec()` runs over the `app.looks` paths of its `forge.json`. The pull
52 page shows the result under the Checks line as **Website spec: N findings
53 (report only)**; open it for one row per finding and its
54 specification.website section. Findings the edge owns are counted apart
55 (`, M the edge owns`). The terminal's pull screen ends its checks row with the
56 same words. Nothing here fails a deploy or the check; see
57 [`docs/WEBSPEC-PREVIEWS.md`](WEBSPEC-PREVIEWS.md).
58 - **A streamed page gets the pill too.** An HTML answer with no
59 `Content-Length` is read up to 4 MiB for at most 100 ms, and only while its
60 first chunk starts like markup; if it ends in that window it gets the pill
61 like a sized page. Otherwise what was read is sent first and the rest of the
62 stream follows unchanged. The cost: a streamed page that is still going
63 waits up to those 100 ms before its first byte reaches the browser, so a slow
64 server-rendered page paints up to 100 ms later (a page sent with a
65 `Content-Length`, or one that starts with something other than markup, does
66 not wait). Event streams and WebSockets are untouched.
67 - **A signed-in note needs the panel's one-use proof (#5612).** The grant
68 cookie alone no longer makes a note `@name`: the forge puts a one-use token in
69 the panel's script tag on a signed-in page load, the panel sends it as
70 `x-forge-proof` and each answer carries the next. Without it, a reused one or
71 one for another grant or host, the sender is a visitor and `admin` is false.
72 This stops an app's code that merely holds the cookie (a `fetch` from its
73 page). It does **not** stop app code that runs in the same page and watches
74 for the tag or patches `fetch` before the panel runs: the token reaches the
75 panel through the page, so that code can read it. Closing that needs the
76 panel in a cross-origin frame, which is not built. If a call fails the panel
77 asks for a reload, since the chain is then spent.
78 - **The forge button is a feedback door (#4758).** One tap opens *Tell the
79 builders*: a text box and Send, no account. The note is posted to the app's
80 own host (`POST /__forge/note`, same-origin only) and filed as an issue on
81 its repository, labelled `from-app`, with the page, the viewport and the time,
82 in the owner's name and with the sender's words inside one fence. A visitor
83 is an anonymous `visitor-xxxxxx` (an HttpOnly cookie); a signed-in person is
84 `@name`. Moderation: 3 notes a minute per sender, 6 per source address, 20 per
85 app, a word filter, and the owner's hide or ban, which closes the issue and, for
86 `ban`, refuses that sender. `GET /__forge/notes` gives the sender's own notes with a live
87 status (queued, being built by a member, shipped on a date), polled every 15 s
88 while the panel is open; a public app's note links to its issue. Left out: the
89 optional screenshot and a version name in "shipped".
90 From the terminal, a repository screen that is an app has two palette rows
91 (`:`): *Tell the builders* sends a note as you, and *Notes from the app*
92 lists yours with their status; the repository's admin sees every sender's and
93 may hide a note, ban its sender, list the banned senders (*Banned senders*)
94 and unban one. The gating is the page's: a guest or a
95 non-reader sees nothing, and a repository that is not an app has no rows.
96 Who may write: anyone on a public app, but a private app, and any sealed
97 pull's preview, take notes only from a person the app admits (the condition
98 `/__forge/measure` uses); a stranger is refused with a 404 before anything is
99 filed. Moderation
100 happens on the forge, not on the app: the app host answers only
101 `POST /__forge/note` and `GET /__forge/notes`, and `/__forge/note/moderate` is
102 a 404 there, because the app's own code (or a preview's) could call it
103 same-origin with a visiting admin's cookie. The terminal screen above and the
104 app's studio page are the owner's tools: the page lists the latest notes
105 (sender, words, page, when) for those who may admin the repository, each with
106 Hide and Ban sender, and every banned sender once with Unban, posting to
107 `POST /:owner/:name/app/notes` on the forge's origin (`web/studio/notes.ts`).
108 Limits count the socket's client (`http/peer.ts`, with its trusted-proxy
109 rule), never a header the client wrote, and all three are checked before any
110 is spent. The request body is read only up to 16 KB. The issue says its text
111 is untrusted data, and the crew's brief says so for any `from-app` issue.
112 A title always opens with `From the app: ` (`saysFromApp`), the label is made
113 before the issue, and a failed labelling closes the issue and refuses the note.
114 Only a note closed as done reads "Shipped"; the other reasons say what they are.
115 Beyond the per-minute limits an app takes 30 notes an hour and 100 a day.
116 The script grew to about 15 KB (cached, immutable); the notes are polled only
117 while the panel is open.
118 - **The forge pill acts.** The pill the proxy adds to an app's pages
119 (`"toolbar": false` in `forge.json` turns it off; to write it in, the proxy asks the app for the page uncompressed, so a page that carries the pill goes out without gzip) links to a forge page,
120 `/:owner/:name/toolbar?env=production|pull-N`, opened in a tab of its own: the
121 pill posts nothing and reads nothing of the app's page, and every action is a
122 same-origin form on the forge. On a preview, everyone who may see the pull sees what it changed (files, +added, −removed, linked to its Files tab) and where its latest check stands (linked to the run); the totals are left out when git cannot give them. Signed out, the page is where you are and Home.
123 Signed in it shows the live agent run on this app or preview, linked to its
124 run page (read once when the page opens; runs on work you may not see, such as
125 a sealed report, are left out); *Report this page* / *Comment on this pull*,
126 previewed first, then a receipt; *Tell the crew* for those who may direct it
127 (the one `/:owner/:name/tell`); and *Make it yours* for everyone who cannot
128 write the app. A production comment opens an issue carrying the page address;
129 a preview comment goes on its pull. From a terminal or an agent the same
130 actions are, in the SSH terminal, rows of the palette on a repository
131 screen: *Working now* (while an agent run is live, left out when it is on work
132 you may not see), *Tell the crew*, *Report a problem* (one text, an issue
133 titled by its first line, through `openIssueThrough`) and *Make it yours*
134 (the fork screen, for those who cannot write the repository); a pull is
135 commented on from its own screen. Agents have the MCP `tell` and
136 `make_it_yours` tools and the API's `POST …/issues`,
137 `…/pulls/:n/comments` and `…/make-it-yours`.
138 - **A run has a budget** (`src/kernel/budgets.ts`): a check past `gate`
139 (60 min) or a reviewer's run past `review` (10 min) is killed with
140 `budget: <what> took <n> min, limit <m>`, queued again once, and failed
141 the second time, which the vitals carry as a need for a day. `gate` is 60
142 min because this repository's own gate takes 18–25 min alone and 45 beside
143 another check; it comes down when the gate does. A check waiting in the lane
144 past `laneWait` (10 min) or a run silent past `heartbeat` (3 min) turns the
145 health degraded with one sentence naming the cause ("22 checks wait behind
146 one lane; the oldest since 04:13; the lane runs #1212 for 31 min (limit
147 60)"). The sentinel files each such breach at once, not once per tick:
148 labelled P0 Trust and assigned to the sentinel, whose run the dispatcher
149 queues on the assignment.
150 - **Every deploy is a run**, and the run is the check on the pull. Turn on
151 **require checks** and a pull cannot merge until its head is green.
152 - **A production deploy records the merged pulls it carries**: one `git
153 rev-list` of the deployed commit when the run finishes. The run page says
154 "Carries #3, #5" and `GET /api/v1/repos/:owner/:name/runs/:id` has `carries`.
155 In the terminal, open a repository and press `d` for its production deploys
156 (each with what it carries), `enter` for the log, `r` to refresh.
157 - A **private** app answers only to someone the forge has vouched for
158 (`canRead` on the repository, then a signed grant); a public one to anyone.
159 - **Every app has a terminal.** `ssh -p 2222 <name>@<forge>` runs the app's
160 `tty` command from `forge.json` on a real pty over the environment's own
161 data; `<name>--<pull>@<forge>` opens a preview's. The scaffold ships one.
162 - **A new app starts trustworthy.** The scaffold's door asks the app's
163 contract before a body is read, every change and refused change is an
164 audit row, the API describes itself at `/openapi.json`, `/healthz` says
165 up, and the look is design tokens, light and dark, with no colour spelled
166 out anywhere else ([below](#what-a-new-app-starts-as)).
167
168 The repository says how it runs in `forge.json` (`run`, and optionally
169 `install`, `test`, `health`, `ready`); a `package.json` with a `start` script
170 is enough on its own. The forge hands the process `PORT`, `FORGE_APP_DATA`
171 (this environment's directory — keep state there), `FORGE_APP_ENV`,
172 `FORGE_APP_URL` and `FORGE_APP_COMMIT`, and nothing from its own
173 environment. Browsers resolve `*.localhost` to loopback, so a laptop forge
174 needs no DNS at all; elsewhere point a wildcard at it and set
175 `FORGE_APP_DOMAIN`.
176
177 **Move the pill.** If the pill covers something of the app's, drag it (mouse or
178 touch) and it snaps to the nearest corner on release; with the pill focused, the
179 arrow keys walk it from corner to corner. The corner is kept in the browser's
180 `localStorage` for that app's host (bottom-right until then), and the panel opens
181 toward the inside of the screen, bounded to it at phone width. The script stays
182 under a 10 KB budget (a test holds it), since it loads on every app page.
183
184 The forge pill on an app's pages says where you are: its accessible name reads
185 `notes, production` or `notes, preview of pull #7`, and pressing it opens a
186 panel with the app, the environment, and a link home. The pull's title is shown
187 only to people who may read the pull. A tap outside the open panel, or Escape,
188 closes it. When a crew run is working on the app (production) or on the preview's
189 pull, the panel also says `scout is working on this` and links the run; it asks
190 `/__forge/crew` once as the pill loads, answers only a signed-in person who may
191 read the repository (and, for a run on a sealed issue, that issue), and shows
192 nothing when the crew is idle.
193
194 **When the pill is not shown** (an app whose own policy refuses foreign
195 scripts), `/__forge/` on the app's host is the way back: a small page from the
196 forge, never the app, with no script, saying the app and where you are
197 (`production` or `preview of pull #N`) and linking Home, the repository (to
198 who may read it) and *Tell the crew* (the same link `/__forge/crew` gives; signed
199 out it offers sign in). It also says what is serving, read from the deploy
200 record (no git): production shows the newest release and the three before it
201 (a redeploy of the same commit is not another release), a preview shows its
202 pull and the commit it last served, or "not deployed yet". A viewer who cannot
203 read the repository sees only the time, never a sha. The read is the `serving`
204 Server-Timing phase. The prefix was already the forge's, so an app's own
205 `/__forge/` path is not reachable.
206
207 **Tell the crew from the pill.** Signed in as someone who may direct the crew,
208 the panel also offers *Tell the crew* (`/__forge/crew` adds a `tell` link, only
209 for them; signed out there is no action). It opens the forge's own
210 `/:owner/:name/tell?page=<address>&pull=<n>` in a new tab, so the POST is the
211 forge's same-origin, CSRF-checked form and the app's origin can never send one.
212 It is the same `tell()` as every door; `page` (an http(s) address) and `pull`
213 (a number, on a preview) are checked again on send and written into the tell
214 as the *Where this was told from* block. A receipt links the issue it opened; a
215 refused tell returns to the box with the words, page and pull kept. The MCP
216 `tell` takes the same through `context: { page, pull, run, commit, said }`, and
217 the terminal's palette has *Tell the crew from a page*, which asks for the
218 page, the pull (optional) and the line, through the same `parseContext` and
219 `contextNote`.
220
221 With a sentence and **Pick for me**, the sentence becomes a plan before the
222 page comes back: `src/crew/plan.ts` makes one non-interactive call to
223 `claude` — the quickest model, a fixed JSON shape, at most 3 seconds — for a
224 short checklist and a crew pick, so issue #1 opens as a checklist already
225 assigned to whoever the plan named, instead of a paragraph and a dropdown.
226 It costs one small model call per app made this way (a few tenths of a
227 cent), never blocks the repository or the app starting, and a bad or slow
228 answer is the same as none: no pick, no checklist, exactly what naming an
229 agent yourself already does. `FORGE_APP_PLAN=0` turns it off; so does having
230 no `claude` on `PATH` — either way **Pick for me** stops being offered and
231 `New app` behaves exactly as it did before this existed.
232
17233 ## Vocabulary
18234
19235 - **App** — a repository that runs. A repository becomes an app explicitly
@@ -302,7 +518,9 @@ own process is not lowered.
302518 Where the host cannot hold cells (or the crew's shell sandbox), the forge
303519 runs uncontained and says so in one line: a boot warning, and, in the foot of
304520 every page a site admin reads, `Reduced protection: apps (<reason>) runs
305 uncontained.` (`kernel/protection.ts`). A fully protected forge shows nothing.
521 uncontained.` (`kernel/protection.ts`), and the same line under the filter on
522 the terminal's first screen. Nobody else sees it, and a fully protected forge
523 shows nothing.
306524
307525 A private destination (a tailnet or LAN address) is reached only when
308526 `egress` names it exactly, `"100.89.95.68:8101"` (an address or host with its
docs/ASKS.md added +60
@@ -0,0 +1,60 @@
1 # Asks
2
3 What waits on a person, and how to answer it without opening each thing: `/asks`, the terminal's On you card, `GET /api/v1/asks` and the MCP `asks` tool. The board's own notes are in `docs/TOUR.md`.
4
5 **What an ask row waits for.** Every ask row carries one word, `waitingFor` (`src/core/waiting_for.ts`, computed once), and who it waits on, e.g. "ready to land · on you". The first that applies wins: `conflicts with main` (the last sweep found files stopping the pull, on its author), `ready to land` (a green change to look at and ship), `a review` (a review or a hand-over), `assigned to you` (the pull or issue is assigned to the viewer), otherwise `a decision`. It shows as a chip on each row of `/asks`, as `waitingFor: {word, on}` in `GET /api/v1/asks` and the MCP `asks` tool, and in the terminal's On you card. A grouped row shares one word (the weightiest of its items), and the terminal's card shows its row's word.
6
7 **Waiting.** A pull waits on a person when the crew handed it over, or when no person has acted on it for 24 hours (`WAIT_LIMIT_MS`, `src/core/pull_wait.ts`): a person's opening, comment or review restarts the clock, the crew's own words do not. The row is "Waiting" with how long (`waiting 30 hours`); it is on `/asks`, `GET /api/v1/asks` (`kind: "waiting"`, `why`), MCP `asks`, and in the terminal's On you card (`waiting` before the title). A pull you hold never asks for being idle, and a crew pull still at work or younger than a day stays the crew's. It can be picked, shipped or closed with the other rows (`waiting:<id>`). A crew member can advise by ending a comment with one line, `Recommendation: merge|close|change — why`; the newest such line from a non-person is shown as "The crew advises: merge — why" on the page and the terminal row, and as `recommendation: {verdict, reason}` in the API, on handed-over rows too.
8
9 **Muting a repository from /asks**: each ask row on `/asks` carries a "Mute owner/name" chip. A muted repository's asks leave `/asks`, `/api/v1/asks` and the phone's pushes (the same per-user mute the On you card uses, no new table); `/asks` says "N muted repositories still ask" with a "Listen again" button, and an app's own page still counts them. The home itself renders no asks, no for-you list and no count: the top bar carries a plain Asks link to `/asks`, and the terminal's dashboard keeps its On you card on purpose (a different door).
10
11 **A wave proposal is one card.** With "plans need a nod" on, a plan the planner writes is kept as a proposal and opens nothing. It is one row in the asks list (`/asks`, `/api/v1/asks`, MCP `asks`, the terminal's On you card) with its title, its why and its issue titles, all escaped, and it asks the app's owner only. Three taps, each one POST that leaves you on the page you were on: **Approve** opens the issues with their links, exactly as an unheld plan would; **Redirect** takes a text box and hands your words to the planner as a new run on the direction issue; **Later** hides it until you bring it back, from the "N waves put aside" line under the asks on `/asks` (each with a "Bring back" button). A proposal is decided once. The same four verbs (`approve`, `redirect`, `later`, `again`) are on every door, all calling `actions/proposals.ts`: the page (`/asks/proposal/<id>/<verb>`), the API (`POST /api/v1/asks/proposal/<id>/<verb>`, a `rw` token, `{"words": …}` for redirect), the MCP tool `asks_proposal` (`id`, `verb`, `words`), and in the terminal `y` approve, `R` redirect, `l` later and `B` bring every put-aside wave back, on a wave row of the On you card.
12
13 **Chips on the feed**: kind chips switch a kind of post off: `reviews` (pulls), `decisions` (decisions and choose decks), `crew` (tells, replies, looks) and `releases`; a person chip switches one person or agent off. Saved per user, one filter (`chipHides`) behind the home feed, `/api/v1/feed`, the Asks list and the terminal's inbox, and the MCP `feed` and `asks` tools, which also take `hide_kinds` and `hide_people` to replace the saved filter for that one call (`[]` lifts it). `/live/apps` (your apps) is not feed items, so chips do not apply there. `GET /api/v1/feed/chips` answers `{kinds, people}`; `POST /api/v1/feed/chips` takes `{"kind": "kind"|"person", "value": "reviews"|"<username>", "hidden": true|false}` (scope `rw`, same-origin). In the terminal dashboard `1`–`4` switch the kind chips `h` on an On you row switches its author off (not your own) and the author's rows leave the card, and `P` brings every person back. The `feed_chips` tool reads them (no arguments, any token) and switches one (`kind`, `value`, `hidden`; an `rw` token).
14
15 **On you, in the terminal**, answers to the same mutes and one-move verbs as
16 the web card. Repositories you muted are hidden and counted ("2 repositories
17 muted"). On the On you card: `space` marks a row, `x` closes the marked rows
18 (or the row under the cursor) with one reason, `m` mutes their repositories,
19 `S` ships them, `u` undoes the latest set within its day, and `L` listens
20 again to every muted repository. Each goes through `applyAsksBulk`, so the
21 per-row access checks and refusals are the web's, shown in the toast.
22
23 **What the decider did today.** Today's card on the board carries one line,
24 "The decider closed N, deferred N, asked you about N": each count links to
25 its ledger rows (`/board?ledger=closed|deferred|asked`; any other value is
26 ignored, `?apps=` is kept). A deferral is the decider's own `stop`, never the
27 rule's. The ledger view lists up to 200 rows and says when it left some out.
28 In the terminal the same line sits above the dashboard, `C` (or the palette's
29 Decider) opens the rows, and `1`, `2`, `3` switch pile. Both are for the
30 repositories you own only: a reader sees nothing.
31
32 **Let the decider** answers a hand-over card (an issue the crew parked) when
33 you would rather not: the third tap on the card, `POST /api/v1/asks/decide`
34 (`{"item": "parked:<id>"}`), the MCP tool `asks_decide`, or `D` on the On you
35 card in the terminal. It sees only the direction text, whether you pinned or
36 muted the app, the day's spend against the ceiling, your last ten decisions
37 there and the card's own evidence; the decision ledger row carries exactly
38 that, with a reason and a confidence. A sure answer (70% or more) to set it
39 aside takes the card off your board; an unsure one, a failure, or no decider
40 configured leaves it with you. Undo for a day (`/api/v1/asks/decide/<id>/undo`,
41 `asks_undecide`, `U`). Each tap is a paid call, so one card is handed over
42 once in ten minutes.
43
44 **The standing rule** says who answers a repository's hand-overs without a
45 tap, three ways: `me` (the default: they wait for you), `decider` (the sweep
46 asks the decider about each parked card and its word stands, whatever its
47 confidence), or `decider_tell` (the decider's sure set-asides, 70% or more,
48 take the card off your board; the unsure ones stay with you). The tap on the
49 card follows the same rule. A shelved repository, one whose autonomy is `off`,
50 has its parked cards closed by rule with no model call whenever the rule is
51 not `me`, so turning the crew off on a ruled repository closes what it had
52 parked. The sweep spends at most five model calls a tick and never re-asks a
53 card already answered or undone. Only a repository admin who is a person sets
54 it, never an agent. Set it in the repository's settings (Autonomy → Decisions
55 here); over the API with `PATCH /api/v1/repos/<owner>/<name>`
56 `{"decisionsBy": "me" | "decider" | "decider_tell"}` (admin token; it is in the
57 repository JSON too); over MCP with `decisions_by` (`repo`; without `by` it
58 says how the rule is set, with `by` it plans unless `dryRun: false`); in the
59 terminal with `R` in the repository's workspace, which cycles me → decider →
60 decider_tell.
docs/CLOUD.md +36
@@ -35,3 +35,39 @@ A key attached under Settings → Cloud accounts is handed to a run only when
3535 the run's repository declares the key's vendor. A provider's vendor is
3636 `VENDOR_OF` in `src/cloud/providers.ts`: the provider's own name, except that
3737 AWS and Backblaze B2 are `s3` (both speak the S3 interface).
38
39 ## Cloud accounts
40
41 **Settings → Cloud accounts** (admins) holds provider keys — Cloudflare,
42 DigitalOcean, AWS, Azure, Fly, Backblaze, GitHub, Vercel — sealed and write-only. Pasting one
43 makes the forge check it with read-only calls (listed in the header of
44 `src/cloud/providers.ts`) and say what it can see; a key the provider rejects
45 is not stored. It is sealed at forge scope as `CLOUD_<PROVIDER>_<NAME>` (secret
46 names are capitals, so `cloudflare/main` is `CLOUD_CLOUDFLARE_MAIN`). The page
47 lists names, the last check, and which run last opened each key; "Every read"
48 lists each open, and a run's page lists the secrets it held. A run gets a key
49 by declaring it like any secret. **Write allowed** is off by default: a run
50 that adds `writes: yes` to its front-matter and declares a cloud key whose
51 switch is off fails with that reason. The switch gates the forge's hand-out,
52 not the key — a provider key that can write still can, so paste read-only keys.
53 **Audit** reads one account and opens one `[audit]` issue per finding (unattached
54 volumes, powered-off droplets, unassociated IPs, never-active zones) with a
55 monthly saving estimate, skipping findings already open. It runs in the forge,
56 not as a crew run; AWS audits look at us-east-1 only; over-provisioned
57 droplets and forgotten DNS records need metrics or resolution and are not
58 attempted; Backblaze, GitHub and Vercel audits find nothing yet. No dependency was added.
59
60 Each account can be **rotated** (the new value is checked, then replaces the
61 old) and **renamed** (re-sealed under the new name, write switch kept). Each
62 says in one line what it is used for now — the `cloud.json` declarations on
63 the default branch of the first 100 repositories the admin can see, cached a minute — and
64 carries a "bring it home" note (`HOME` in `src/cloud/declared.ts`). Every
65 vendor request goes through `src/cloud/gate.ts`: an admin's verify or audit is
66 audited as the operator; a hand-out of a key to a run is allowed only if that
67 repository's `cloud.json` declares the key's vendor (`VENDOR_OF`: the provider's name, but AWS and Backblaze are `s3`), and an undeclared one is
68 refused before the key is opened. One `cloud_audit` row per request (account,
69 purpose, host and path, bytes out, when, allowed or refused — no token, no
70 query) is listed at **Settings → Cloud accounts → Every vendor call today**.
71 Rows are pruned past 90 days on every write; `at` is indexed. Left out: one
72 token per vendor is not enforced (several names per vendor stay allowed), and
73 an agent's own vendor calls beyond the hand-out are not metered. A forge admin reaches the same accounts in the terminal (`:` → Cloud accounts: each account with what it is used for, verified / last used and today's vendor calls; `a` attach, `v` verify, `r` rotate, `n` rename, `x` remove, `w` flip the write switch, `A` audit into an `owner/repo`, `c` every vendor call today (purpose, host and path, bytes out, allowed or refused), the token typed masked) and over MCP (`cloud_accounts` lists them with today's audit rows; `cloud_account` with `verb` attach, verify, rotate, rename, remove, `allow_writes` (with `allowed`) or `audit` (with `repo`) needs an admin-scope token). Both call `actions/cloud.ts`, neither ever shows a token, and anyone else is refused. A rename is refused when the account or the secret `CLOUD_<PROVIDER>_<NAME>` already exists.
docs/CREW.md +350 −3
@@ -1,4 +1,329 @@
1 # The crew's shell
1 # The crew
2
3 In a preview (`FORGE_APP_ENV=preview`, which the platform sets) the crew is
4 off: no autonomy tick, no dispatch, no spend; checks still run and everything
5 can still be read. The boot log and the studio's crew card say so. Set
6 `FORGE_CREW_IN_PREVIEW=1` to run it anyway.
7
8 **Per model, per prompt version.** Every agent run records the model it ran on
9 and a prompt version: a short hash of the agent's brief, its crew role and the
10 standing system-prompt text (not the issue or branch, so two runs on one brief
11 share a version and editing the brief starts a new one). Settings → Crew shows
12 the scorecard cut both ways ("Per model", "Per prompt version": runs, landed,
13 failed, spent, per run), and the terminal's `Crew scorecard` command shows the
14 same to an admin. The scorecard of pulls landed, review rounds and red checks
15 is cut the same way as sub-rows under each agent (see the README's opening); a team's pooled
16 row is not cut by model, and is under the agents on the page and in the
17 terminal's "Crew scorecard per agent" (see the README's opening). Runs from before this was recorded show as *default* /
18 *not recorded*; they are not backfilled.
19
20 **Tell the crew from any page.** In a repository you may direct, the web palette
21 (`:` in the browser) has a *Tell the crew* row. With the script it opens the thread's own
22 comment box over the page you are on, without leaving it: `enter` sends (shift+
23 enter is a new line), `esc` closes. The tell remembers that page (`from`), and
24 the answer comes back to it. Without a script the row is a link to a page of
25 its own, `/:owner/:name/tell?from=/path`, which is the same box; `from` must be
26 a path on this forge. Who may not direct the crew gets a 404 there and no row.
27 The terminal's palette has the row too, on a repository screen, for the
28 same people: it takes one line, tells with no `from`, and answers in the
29 same words.
30
31 **The crew's answers.** On an app's page the owner's own tells show under the tell
32 box, newest first, each with what the crew said back (its last comment, else its
33 last finished run's summary, else that it is working or waiting). The region
34 updates live from `GET /:owner/:name/app/answers` (owner only; 404 for anyone
35 else). In the terminal, `a` in the repository screen opens *Your tells* with the
36 same rows behind the same gate.
37
38 An **agent** is an account of kind `agent`: no password, no session, and it
39 acts only through a token the forge mints for a run and destroys after it.
40 An admin creates one (**Settings → Crew**, or `POST /api/v1/agents`) with a
41 model, a brief in its own voice and a spend budget; a repository admin puts it
42 on the repository's **crew**, which is the one grant an agent can hold — it
43 reads and writes that repository and administers nothing.
44
45 A **planner** on the crew may also close an issue it is re-planning, in
46 `actions/issues.ts` so the web, the API and the MCP agree, and only these
47 ways: `split` into open issues opened since it was handed the issue (since the
48 issue itself when the crew never parked it twice, or into issues of the same
49 repository that all closed as done since that hand-over), `dropped` for an issue the
50 crew wrote, and `done` when commits on main already name the issue. A person's
51 ask is never dropped by the planner; any other close, any reopen and any edit
52 to a person's issue stay refused.
53
54 Assign an issue to an agent and it starts: the forge clones the repository,
55 puts the agent on a branch named after the issue, and runs Claude Code
56 headless with a brief that says who it is, what the task is, how to finish,
57 and what will be refused. The pull opens at the agent's first push, marked
58 "In progress" with a link to the run's log, and its preview follows each later
59 push (`src/crew/early.ts`). When the agent stops, the forge pushes what it
60 committed and fills in that same pull (the agent's last message is the
61 description). A run that ends without reporting leaves the pull saying so, with
62 the log link; a run that is only paused (a limit, an outage) keeps saying "In
63 progress". A run that declines after its pull opened closes that pull with the
64 decline sentence and the run link, unless a person has commented on or reviewed
65 it: then it stays open with a comment. An agent may withdraw (close) a pull it
66 wrote itself, and nothing else. A body a person has edited is left alone, unless they kept its first
67 words ("In progress:" or "The run ended"); change those words too. Leave
68 feedback on the pull — a comment, a request for changes, a line comment — and
69 the agent picks it up in the background, resuming its session. A failed check re-engages it too, a bounded number of
70 times before a person must speak.
71
72 **Design review.** A pull that changes anything under `src/web/` (a page, the ui
73 vocabulary, the style) is sent to a crew reviewer on the Design team when there
74 is one, and its review brief holds the change to `docs/DESIGN.md`. A security
75 change still goes to the security reviewer; a change only under `src/core/` is
76 not a design matter.
77
78 **How a reviewer is chosen.** Reviews go to the least-busy reviewer: the one
79 with the fewest review runs in flight (queued or running). A tie goes to the
80 experience reviewer. A pull about security goes only to a security reviewer
81 (the least busy of them). A `src/web/` pull goes to the design reviewer until
82 it has 2 review runs in flight, then spills to the least busy reviewer.
83
84 **Three rounds, then a person.** When the crew reviewer has asked for
85 changes three times on one pull with no approval and no word from a person
86 who may direct the crew between, no more feedback runs start on it. The
87 thread keeps a note ("Handed to a person by the crew:") with the three
88 asks, and the pull becomes a **hand-over** card in asks: *Tell them what to
89 try*, or *Close it*. Rounds are counted from the reviews, not the runs, so a
90 restart never adds one. A person resets the count by commenting as a
91 director (the crew then resumes) or by approving the pull from its page; the
92 card ends when the pull is approved, closed or merged. Hand-overs can be
93 closed in bulk like the other pull asks. The terminal's On you card lists
94 them as "handed over" rows: `enter` opens the pull, `x` closes it.
95
96 **A request for changes always moves.** Every tick, a crew pull whose
97 reviewer last asked for changes is moved on. If the builder has pushed since
98 and the new head is green, the reviewer is asked again: a verdict on old
99 code never blocks. If the verdict is on the current head and no run is in
100 flight, one feedback run is queued to the pull's builder, and a builder is
101 assigned when the pull has none. That run takes a builder slot like any
102 other. The tick log names each pull it moved, or why it did not, and
103 The flow panel on `/boards`, the terminal's Flow dialog and the MCP `flow` tool list the pulls
104 waiting on a builder and those waiting on a reviewer, oldest first.
105
106 The same three also say, per reviewer, how many review runs are in flight (queued or running): `3 reviews in flight: quill 2, sage 1.`, an element `#review-load` on `/boards`; the MCP `flow` tool and `GET /api/v1/telemetry/flow` carry it as `reviewing: { says, reviewers }`. It is `reviewLoad` from `src/crew/review.ts`, the count routing reads (`flow/reviewing.ts` only names it), and it is scoped to the reader: the owner's counts every repository, an agent's only public ones, so a review running on a private repository adds nothing to what an agent reads. No dependency was added.
107
108 **Already done.** A builder that finds its issue already built on the base
109 branch declines with `DECLINED: already done on main in <commit>`, naming the
110 full or abbreviated id of each commit that did it (at most five). The forge
111 checks that every commit named is on the base branch; only then is the run
112 an "already done" outcome, with no strike and nothing parked. The issue stays
113 open with a note naming the commits, the asked list and the issue page say
114 "Done on main", and the person who asked closes it (the page's Close button,
115 or `x` on the issue in the terminal); the close writes "Closed: already done
116 on main in …" on the thread. A claim with no commit, or with one that is not
117 on the base, is an ordinary decline and is parked as one.
118
119 **Live triage.** On each autonomy tick, an open issue carrying the crew's own
120 "already done" note (a person's note counts for nothing) is closed as *done*
121 when every commit the note names is on the base branch; one missing commit
122 leaves it open. At most 25 per repository per tick, rules only, no run and no
123 spend. The close is made in the name of the person who set the app autonomous,
124 so its comment says "Closed by the crew's live triage". Reopening the issue
125 undoes it and the reopen stands: an issue that already has a close note is
126 never closed by triage again. The tick line counts them as `triaged=N`.
127
128 Live triage also folds duplicates: of two open issues whose titles match after
129 case and punctuation, the newer closes (reason dropped) with the comment
130 "duplicate of #N", and whatever waited on it waits on the older. A person's
131 newer issue is never closed into a crew member's; the older closes instead. A
132 fold a person reopened stands, the plan's own issues are never folded, a
133 dependency that would make a loop stays where it was, and folds share the
134 25-per-tick cap with the already-done closes. Exact match only, no fuzzy
135 matching.
136
137 **Live triage files issues.** On the same tick, an open issue an hour old
138 that serves no pillar is filed: under the pillar its title, else its body,
139 names by its short form (`P0`) or whole label (`P0 Trust`) — a bare word like
140 "trust" does not count — otherwise behind the Later gate. Each gets a comment
141 marked automatic. Directions, umbrellas and the gate are skipped, as is a
142 repository with neither pillars nor a gate. To undo it, name a pillar (link
143 its umbrella to wait for the issue) or unlink the gate; the comment stays, so
144 triage does not file that issue again. The tick line counts `labelled=N`, and
145 shows `triageMiss=N` for old issues it could not file (no gate to defer to).
146
147 Live triage also folds questions. An open issue opened by a person who can
148 write here, whose title starts `Question:`, `Wish:` or `Idea:` or ends in `?`,
149 becomes one `Folded: #N <title>` comment on the direction issue and closes as
150 *dropped*, with a comment saying how to get it back: reopen it and retitle it
151 as the task it asks for; the reopen stands. Issues from outsiders, agents and
152 imported ones are never folded, and nothing is folded before the repository
153 has a direction issue. Note that a writer's real task titled `Can we add X?`
154 is folded too, since a title ending in `?` is the rule; phrase tasks as
155 statements. The tick line counts folds as `folded=N`.
156
157 **A red pull.** The decider stops a red check only when it is at least 70%
158 sure; below that the pull gets another feedback run. A stop parks the pull: it
159 closes as *parked*, not done, with a comment that says why. The pull page's
160 badge reads "Parked" instead of "Closed", the terminal's merge box says "Parked
161 by the crew, not merged", and the lists note it as stopped. Reopening it clears
162 the mark. Only the crew parks: the API and the page refuse `reason: "parked"`.
163 In the database the pull's `close_reason` stays NULL (its CHECK predates the
164 word) and `issue.parked = 1` carries it, so raw SQL never reads it as done.
165
166 **The merge queue.** A green, approved pull in an autonomous repository joins
167 its repository's queue (`src/checks/queue.ts`). The queue builds one
168 candidate: main, then each queued pull's squash on top of the one before, in
169 queue order, leaving out any pull that conflicts or has nothing left to land.
170 A candidate carries at most eight pulls (`MAX_CANDIDATE`), so a gate stays
171 near one full tier and a red bisects in three; the rest wait their turn.
172 The log says `[queue] gating N pulls since HH:MM` as a gate starts and
173 `[queue] gated N pulls: green|red|cut off` as it ends.
174 The full gate runs once on that candidate. Green lands every pull in order,
175 and each landing is refused unless main is still where the candidate was
176 built and the squash writes the tree that was gated, so main ends at the
177 gated tree. Red is searched: prefixes of the candidate are gated on main
178 — the front half, plus one more cut per free check slot, all at once — the
179 longest green prefix lands, the search goes on in the steps up to the first
180 red prefix, and the red pull is handed back with the failing tests named.
181 A red batch is never formed again around the same head: with a check slot
182 free, the head of a red batch of three or more is gated alone beside the
183 line (`[queue] red batch: its head is gated alone`) while the rest gates
184 without it, stacked as deep as `queue.depth` goes, so the good pulls land
185 while the suspect is judged. A pull in two red batches is never in a third:
186 it waits for a gate of its own. Red alone, it is handed back (`[queue] red
187 alone`); green alone, it rides at the tail of a later batch, never its head.
188 While a candidate gates, the next one gates on top of it (`queue.depth`
189 under Settings → Capacity, `FORGE_QUEUE_DEPTH`, default 2): when the first
190 lands whole the next lands right after it, on the tree it was gated on; when
191 it does not, the next is formed again on main. A gate beyond a line's first
192 starts only on a free check slot and holds it, and two slots are always kept
193 for pull checks (`PULL_RESERVE`); with no slot spare the queue gates one
194 candidate at a time and halves a red one, as it always did. A red
195 waits for a push, not a new main (a pull red on main would otherwise ride a
196 batch after every landing), except once: after 30 minutes on the same head
197 it is gated again, and lands if green; a red that reproduces stays red and
198 is not told twice. The queue remembers what its searches found: a pull
199 handed back red on top of others of its candidate that landed first is a
200 clash (the comment names them; the log says `[queue] red on top of its
201 candidate`, or `red on its own merge`), and the tick line counts them as
202 `clashes=N`. A remembered pull that joins again at the same head is gated
203 alone, only once no fresh pull waits, and nothing is stacked on it, so a red
204 re-check never cuts off a batch of fresh pulls; a push forgets it. A gate cut off by the forge stopping (a
205 roll, a drain) or by the cluster losing its Job is no red at all: the next
206 sweep gates it again. So is a gate that has not answered in 15 minutes
207 (`GATE_DEADLINE_MS`): it is abandoned and its check aborted, the log says
208 `[queue] candidate abandoned`, and the next sweep forms the line again. Pulls
209 that turn green while a candidate is gating wait for the next one. Each
210 queue's numbers — how many pulls are queued, how many are gating, how many
211 gates are in flight, how many landed in the last hour, and their median
212 minutes from approval to landing — show in three places: the flow panel on `/boards`, the
213 terminal's Flow command, and the MCP `flow` tool's `queue` field. The flow
214 panel and the terminal's Flow command also show the last hour's red batches
215 (`1 of 3`: candidates gated red, of those gated; search prefixes and
216 re-checks are not counted) — the panel as a column, the terminal as a
217 `Red batches, last hour:` line under its table. The MCP `queue` rows do not
218 carry it: their shape is unchanged.
219 When a repository requires checks, an approved pull that is green only
220 against an older main joins the queue as it is: the candidate's full gate
221 runs on main's tip with that pull merged, which is the proof a re-check of
222 its own would give, and a red candidate bisects as above. So a landing that
223 moves main does not send every other approved pull off for a check of its
224 own first; the batch lands in one gate. A pull's own check runs again only
225 when its head changes: an approval checks a head that was never checked,
226 and a green one only when main moved since and the repository is not
227 autonomous, whose land gates on today's main anyway (`src/checks/approval.ts`). A person's merge, which no candidate gated, still
228 refuses a stale green. Each sweep reads every open pull, oldest first, not
229 only the newest page, so an older approved pull is batched with the rest
230 rather than joining alone on its own event. The autonomy tick line counts the approved pulls the
231 last sweep saw and why each waits (`approved="3 approved: queued=2 review=1"`,
232 from `src/crew/waits.ts`), and what the queue is doing now
233 (`queue="gating 2 pulls since 18:54"`, `queue="1 waiting"` or `queue=idle`,
234 in UTC).
235
236 An autonomous app's planner is **held** while nothing is landing or the plate
237 is full: no pull has merged in 48 h while one was closed unmerged, or more than
238 `slots × 3` ready, unassigned issues are waiting. The tick log says which
239 (`planner: held`), and the board and the terminal's repository list say it in
240 words. A comment on the direction issue from someone who may write there lifts
241 the hold; an issue handed to the planner is still owed its pass.
242
243 Every tick says why each ready issue a builder cannot start is held, counted
244 under the first filter that holds it (`crew/held.ts`): `held: running=1
245 open_pull=10 stalled=12 run_cap=11` — a run on it, an open pull proposing it,
246 the planner's, found done on main and waiting for a person to close it
247 (`done_on_main`, never re-planned), stalled, at its run cap, parked twice, covered by a red-main
248 fix, or a setting's finding. The flow panel on `/boards` draws the same per autonomous app as one
249 bar, and the terminal's Flow command prints it as a line per app
250 (`flow/held.ts`, kept 5 s). When nothing is startable while builder slots are free and some of the
251 held are stalled, capped or parked twice, the planner is owed one pass with
252 that breakdown in its brief, to re-plan them into smaller, fresh issues; a
253 full plate and its own unanswered decline do not hold that pass, and the
254 same breakdown does not ask it twice. A stalled issue does not wait for that
255 pass: the next tick parks it with who stalled it and why, so another builder
256 picks it, and a second stall hands it to the planner past its day's cap
257 (`crew/stalls.ts`, `unstuck=N` on the tick line).
258
259 The forge asks the host for one more binary: `claude`, on `PATH` or at
260 `FORGE_CLAUDE`, authenticated as the operator (an `ANTHROPIC_API_KEY` in the
261 forge's environment is handed to runs; the machine's own Claude Code login
262 works when there is none). `FORGE_CREW_CONCURRENCY` (one per 3 GiB above 2 GiB, 2–16) and
263 `FORGE_CREW_BUDGET_USD` (5) bound it; `FORGE_CREW_CEILING_USD` caps what the
264 crew may spend across every repository in a UTC day, whatever each
265 repository's own ceiling says; `FORGE_APPS=0` turns apps and crew off. A preview whose pull nobody has
266 pushed to or commented on for `FORGE_PREVIEW_IDLE_DAYS` (default 3; `0` keeps
267 every preview warm) is stopped and reads as asleep, and asking for it wakes it.
268 An admin with more than one Claude account keeps them under Settings →
269 Subscriptions, sealed, and grants each to a repository (Settings → Crew) or
270 an organization (its page), down to one crew member: a work plan pays for
271 the work repositories and nothing else. Two on one grant take turns, and
272 one the provider rests hands its runs to the other until the reset.
273 **Model budget.** A new account has none: its repositories' crew does not run, spends nothing, and says "This repository has no model account". A person adds their own subscription at Settings → Model (or the terminal's **Model budget** command, `a`); it is sealed, granted to their own repositories only, and one per account. A forge admin who is a person gives an account a capped grant, in dollars a month spent on the forge's own credential, from the account's page, the terminal's **Model grant** command, `POST /api/v1/admin/accounts/:name/model-grant` (`{"usdPerMonth": 20}`, `null` takes it back) or the MCP `model_grant` tool; each change is in the audit. The cap is the UTC month's spend in the usage ledger, checked when a run starts and on every spend tick: a run past it stops, and its page says the grant is spent. Two runs that start together each begin with the whole remainder; the tick check ends both once the sum crosses the cap, so the overshoot is at most what they spend between ticks.
274
275 **Roles.** An account is a guest, a member or an admin (`docs/ROLES.md` has the table of what each can do; `src/core/roles.ts` holds it). Admin is the admin flag, a member is promoted, everyone else is a guest. A guest is refused creating or forking a repository, making an app their own and starting a crew run, in the words "the owner has not made this account a member" (after the older refusals: unverified, suspended, agent, private); nothing switches it off. A forge admin who is a person lists everyone with their role at Settings → People (`/settings/people`) and promotes a guest to member with one button, with an optional starter model allowance in dollars a month (none by default, given as a model grant and audited as one), or demotes a member (the allowance stays until taken back on the account's page). In the terminal it is the **People** command (`r` changes a role: `member 5`, or `guest`); over the API, `GET /api/v1/admin/people` (`{ items, next_cursor }`, as `/admin/accounts` and `/admin/invites` answer, with `limit` 1-200, 50 by default, and `cursor`) and `POST /api/v1/admin/accounts/:name/role` (`{"role": "member", "usdPerMonth": 5}`); over MCP, the `account_role` tool (a plan unless `dryRun: false`, with an admin token). Anyone signed in sees their own role and its row of the table: the account page (`/settings`) shows "What you can do here"; over MCP, `whoami_can` returns `{username, role, can, abilities}` with any token (with `{"kind": "passkeys"}` it lists your own passkeys instead, `{ items }` of id, name, created, lastUsed, people only; the page is Settings → Passkeys); in the terminal it is the **What I can do** command (read only, agents included).
276
277 **Invites.** A forge admin who is a person mints a single-use invite link with `POST /api/v1/admin/invites` (`{"days": 14}`, 1 to 30, 7 by default); the response carries the link once and only its hash is kept. `GET /api/v1/admin/invites` lists them (open, used, revoked, expired) without tokens, and `DELETE /api/v1/admin/invites/:id` revokes one. Minting, revoking and using a link are events (`invite.minted`, `invite.revoked`, `invite.used`; the last names the invite and the account it let in, never the token). The same is the **Invite links** card on `/admin/limits` (a form with expiry days, 1 to 30; the new link is shown once, with a copy button; an open link has a Revoke button) and the terminal's **Invite links** command (`m` makes one and prints the link once, `x` revokes the selected open one). Over MCP, the `mint_invite` tool (`{days?}`, an admin token, a forge admin who is a person) answers with the same link, and the `invites` tool (`{action: "list"|"revoke", id?}`, the same person admin and admin token, for both actions) lists them without links or revokes one. While signup is `invite`, the link opens the door once: `/signup?invite=…` (the form and the passkey door) or `invite` in the `POST /api/v1/users` body. In the terminal, with signup = invite the create-account screen has a fourth, masked **Invite** field that takes the token (not the whole link); a blank one is asked for, and a bad one is refused with the same sentence as `/signup`. The account and the invite's use are one write, so two sign-ups with one link make one account; a used, revoked or expired link, or any link while signup is `closed`, makes none. While signup is `open`, a link is not spent.
278
279 **Spend ceilings.** Apart from the grant, a forge admin who is a person may set any account's — a person's, an agent's or an organization's — daily (UTC) and per-run ceilings in dollars: on the account's page (`/settings/accounts/<name>`, an organization's too, by that address; an agent and an admin's own account have no such page), the terminal's **Model ceiling** command, `POST /api/v1/admin/accounts/:name/ceiling` (`{"dailyUsd": 20, "perRunUsd": 5}`) or the MCP `model_ceiling` tool; each change is in the audit. Both empty on the page, `none` in the terminal (its prompt shows the current ceilings, and an empty answer changes nothing), or `null` for both on the API and MCP takes them back. An organization's page is linked from its own page for an admin. An account has no ceiling until one is set, so this forge's own crew is never stopped by a default nobody chose. A run on an account with a ceiling does not start when the day's is spent or the per-run one is too small, is capped at the smaller of the per-run ceiling and what is left of the day, and stops mid-run when the day's is spent.
280 **Sandbox.** An agent's shell is held to a policy — the network is the forge
281 and the package registry, writes stay in the clone, the forge's data and the
282 operator's credentials are unreadable — by bubblewrap and socat on Linux
283 (both are in the image). Inside it a builder can install the declared
284 packages, type-check, and run the tests that boot a server: no unix socket is
285 let through, so the run carries `FORGE_TEST_SOCKET=port` and the forge listens
286 on a loopback port instead (`bun scripts/sandbox-probe.ts` prints each
287 verdict). A host that forbids unprivileged user namespaces
288 (many container platforms' seccomp profiles) cannot hold it, and the policy is
289 then switched off rather than faked. To tell which a deployment is in: the
290 boot log carries `agent sandbox not available here` with the reason, Settings →
291 Crew shows **Unsandboxed** with it, and every agent run's page says
292 "Sandboxed" or "Unsandboxed" from what it was actually started with (runs
293 from before this record say nothing). Claude Code bridges that sandbox with
294 two `socat` relays per session, in the session's process group; when a
295 session ends, however it ends, whatever is left in that group is killed. A
296 `processes` line once a minute counts what runs beside the forge by name
297 (`top="socat=… bwrap=…"`), and kills any bridge whose session is gone
298 (`reapedBridges=N`). Every start is counted in `boots.json` under the data
299 directory: the boot line and every autonomy tick line say `restarts=N`, plus
300 `unclean=N` for starts that ended without an orderly exit (a crash or an OOM
301 kill, which leave nothing in the log themselves).
302
303 `docs/FLOW.md` is where every issue stands: `GET /api/v1/repos/:owner/:name/flow` returns each open issue's stage, since, stalled and unblock, plus what went live in the last 24 h.
304 Where the whole forge waits is the owner's, on `/boards` (the old `/flow` redirects there, query kept; an agent granted `telemetry:read` reads the same from `/api/v1/telemetry/flow` and the MCP `flow` tool): the flow panel shows each stage from ready to live with what waits, the oldest wait and its capacity, under one sentence naming the bottleneck, and beside each what it let out in the last hour with a 24-hour sparkline, under a line of landings and the median ask→live — every count links to the items it counts. A stalled step is a card on the home (Studio mode) for anyone who can write to the repository: an open pull whose check was never asked for, or is red, past its stage's allowance, with its stage, how long it has been stuck, and one button — Run check or Re-run — that asks for a check at the head and returns to the home. The terminal's **Stalled steps** command (palette) lists the same steps, live, and Enter or `r` does the same re-run; both read `flow/stalled.ts` and act through `flow/unblock.ts`, and neither shows anything to someone who cannot write. A conflicted pull never sits: every tick the crew's open pulls that no longer merge get one bring-up-to-date run each (one in flight per pull), oldest pull first and only as many as the check lane has slots for, since each ends in a push and a check: check slots minus the checks queued and running minus the runs already in flight, with one always allowed (`crew/conflict_room.ts`, #4459); after 2 failed tries in the same conflict, or 24 h since the first, the pull is closed with the conflicting files named and its issue goes back to ready, unassigned, with no strike against the builder. Conflicted pulls are listed oldest first on `/boards`. The terminal's Flow command and the MCP `flow` tool read the same: the sentence, the landings line, and each stage's count out in the last hour and in 24 h.
305
306 `/resilience` is a forge admin's too (a guest is sent to sign in, anyone else gets 404): each protection in `docs/THREATS.md` as one line, green, red, untested or "no test yet", judged by the test file it cites in the last full gate of the forge's own repository (`FORGE_REPO`; other repositories' gates never touch it). The terminal's **Resilience** command (palette, admins) lists the same lines.
307
308 The terminal also has an Access command (`:` → Access), a site admin's: it reads the same last-day non-browser requests as Settings → Access and the `access` MCP tool, newest first, and filters them with `s` (since), `u` (user agent) and `p` (path), `c` to clear.
309
310 `/system` is the machine, for the owner (#1789), laid out like btop (#2721): a meter and a 30-reading block-character history per core, memory and swap meters, load, hwmon temperatures, disk use per mount, disk and network rates (`/proc/diskstats`, `/proc/net/dev`), and a table of every process grouped forge / check (`check:<run>`) / app (`app:<slug>/<env>`) / host with CPU, memory and age, sortable by CPU (`c`) or memory (`m`) or the buttons (shown only with a script; without one the order is by CPU). It reads `/proc` and `/sys` and updates in place over one event stream, `/system/stream` (`?pause` holds it still; no reload, no polling); the 2 s push timer runs only while someone is subscribed, so it costs nothing unwatched. Its history is the `machine` board source. A test file running in more than one check at once is counted at the top of `/system` and the `/boards` flow panel, with the checks. Command lines are never kept — only `*.test.ts` arguments.
311 In the terminal, `:` → System shows the same view (the owner's only): per-core CPU with a block-character history, memory, swap, disk and network rates, and each group's top two processes (no sort; the dialog is read-only), and `:` → Telemetry lists the agents and flips `telemetry:read` for the one you press Enter on.
312 An agent the owner grants `telemetry:read` (Settings → Telemetry, one switch per agent, revoked the same way) reads the same through `GET /api/v1/telemetry/{system,flow,sources,history}` and the MCP `flow` and `system` tools; private repositories' checks and apps are renamed `check:private-N` and lose their processes and test files, and `flow` (like `system`, an admin agent account needs the grant too: only a person who is an admin reads without it) names only public repositories' conflicted pulls, every read is listed on that page, and an agent without the grant is refused. `/contribute`, `GET /api/v1/contribute` and the MCP `contribute` tool are open to anyone: what the forge is, the bottleneck now, open issues labelled “help wanted” in public repositories, and how to propose (fork, pull, the same gate). The grant is a table, not a token scope, so no token has to be re-issued.
313
314 **Raw files and caches.** `GET /:owner/:name/raw/:ref/*path` at a full 40-hex commit id is `immutable` for a year: `public, max-age=31536000, immutable` for a public repository, `private, max-age=31536000, immutable` for any other, so a shared cache never keeps private bytes. At a branch or tag it is `private, max-age=0` and revalidates. Every raw answer has the blob's oid as its `ETag`, and a matching `If-None-Match` gets a 304 with the same `Cache-Control`, `nosniff` and sandbox CSP. The HTML blob page is not cached this way (it carries the viewer and a nonce). A public repository that later turns private keeps its year-long `public` copies in shared caches already holding them. See docs/CACHING.md.
315
316 `docs/PLANNING.md` is how issues depend on issues: the edge, the map, and
317 what "ready now" means to a person and to the crew. The map draws open
318 issues by default; `?done=1` ("Show done") draws the closed ones on an edge too. The map also shows each
319 pillar's ask-to-live: the median hours from an issue opening to its change live
320 in production, over the last 7 days (also in the map and milestones API and MCP).
321 It reads the `deploy_pull` record a production deploy writes when it finishes,
322 so a page view runs no git; an issue closed by several pulls counts once, at
323 its first time live. On boot the forge records what the last 7 days of earlier
324 production deploys carried, so work that went live before the record is counted.
325
326 ## The crew's shell
2327
3328 An agent's shell runs inside Claude Code's sandbox (`src/crew/sandbox.ts` is
4329 the policy, `src/crew/claude.ts` the settings): the network is the forge and
@@ -8,7 +333,7 @@ credentials are unreadable. No command runs outside it on request.
8333
9334 Bun's install cache is one per repository, `<data>/crew/.cache/<repo id>/bun` (`BUN_INSTALL_CACHE_DIR`): every run on a repository shares it, no two repositories do, and deleting the repository deletes it.
10335
11 ## Running this repository's tests inside it
336 ### Running this repository's tests inside it
12337
13338 On Linux the sandbox refuses the bind of a unix socket, and the forge's
14339 internal listener (the git hooks' door) is one. Claude Code can only allow
@@ -51,7 +376,8 @@ and names each fault with the evidence quoted (secret-shaped text scrubbed).
51376
52377 Each finding is exactly one open issue, keyed so it is never filed twice; a
53378 finding still there is a comment, and only when the evidence changed. The board
54 strip shows the last hunt and the count of open findings.
379 strip shows the last hunt and the count of open findings. The whole of it is
380 `docs/SENTINEL.md`.
55381
56382 Left to a person on purpose: ingress routes at the edge are not in the
57383 database, and the sentinel opens no pull itself — a finding with a mechanical
@@ -68,3 +394,24 @@ named instead, with its file count, on the feed post that says what waits
68394 (`too wide to count: #4840 (1,081 files)`), so a person can close or rebase
69395 it. A real sweeping change, such as a rename across the tree, goes uncounted
70396 too; it conflicts with everything anyway, and it is named.
397
398 ## Passports
399
400 A crew run's environment carries `FORGE_PASSPORT`, a 15-minute credential
401 scoped to `health:read` for the forge alone (`src/kernel/passport.ts`, no
402 dependency: Node crypto's HMAC and HKDF). It opens one route and nothing else:
403
404 curl -H "authorization: Passport $FORGE_PASSPORT" "$FORGE_URL/api/v1/passport/health"
405
406 Missing, expired, wrong-audience, wrong-scope or forged passports get a 401. A
407 passport has exactly one valid encoding (padded or altered variants are refused).
408
409 This route proves the mechanism and is not a privilege: it returns what the
410 anonymous `GET /api/v1/health` already returns. The next edge should guard
411 something only a passport may reach.
412
413 ## Seeing the direction a vision pull will write
414
415 An open vision pull's page shows, to an admin of the repository, "Direction after the merge": today's direction beside the one an admin's merge will write, computed by the same `nextDirection` the merge uses (`src/crew/vision.ts`), so the two cannot differ. Repository settings (Crew) marks the direction's fenced block `follows docs/VISION.md` and the words around it `the owner's`. Today and After the merge stack vertically at every width.
416
417 The same read is a second door: the MCP `vision` tool takes an optional `number`, and for an admin returns `preview` (`{ today, after }`, or `{ why }` the direction would stay) for that open vision pull, plus `directionParts` (the direction cut into `vision` and `owner` parts). The terminal (TUI) has no such view; it is not touched by this change.
docs/FOLLOWING.md +22 −1
@@ -51,7 +51,9 @@ each fork's standing as a badge. The JSON is
5151 The parent serves `/releases.json` as `{"releases":[{"name","url"}]}`, newest
5252 first; the child reads it with a 3 s timeout, no redirects and a 256 KB cap,
5353 caches it under `FORGE_APP_DATA` for 10 minutes, and says "N releases behind
54 <parent>" on a fork's following card and in the terminal's About card (`I`). A
54 <parent>" on a fork's following card, in the terminal's About card (`I`), and as `parentForge`
55 (`{host, behind, releases, fetchedAt, down}`, null when unset) in
56 `GET /api/v1/repos/:owner/:name/following` and the MCP `following` tool. A
5557 page never waits for the parent: it shows the cached copy and refreshes in the
5658 background, and a parent that is down or answering garbage leaves a "could not
5759 be reached" with the age of the last copy. Unset, nothing is shown. Read only:
@@ -193,3 +195,22 @@ has none. The step is `takeOrTell` in `src/actions/follow.ts`, shared by the
193195 page, `POST /api/v1/repos/:owner/:name/following/take` (409, naming the
194196 converge issue), and the MCP `take_updates` tool, so the same press does the
195197 same thing at every door.
198
199 ## Ranked forks
200
201 `/:owner/:name/forks` and the terminal's forks screen (`f` in a repository)
202 list a repository's direct forks the same way, so the best of a fork tree
203 rises. Order: commits ahead of the parent's default branch (most first),
204 then fewest behind, then the newest tip commit. Each row reads `N ahead · M
205 behind` and when the fork last moved; a fork that is ahead has a **Compare**
206 button (on the page) to its `/pulls/new`, which proposes it to the parent.
207 Each fork's own forks are listed indented beneath it (one more level, on the
208 page and in the terminal alike); the forks below those are a count, `and N more
209 forks below`, linking on the page to that fork's forks page. A private fork is
210 never named or counted to someone who cannot read it.
211
212 Forks with nothing ahead (or whose standing git cannot read) follow under
213 `N forks with nothing new`. At most 60 forks are ranked per view; past that
214 the page says `Ranking the first 60 of N forks`. A fork's standing is cached
215 by the pair of tips (`actions/forks.ts`), so a view of forks that have not
216 moved costs one `git log` per fork.
docs/HOME-EVAL.md +6
@@ -25,6 +25,12 @@ The five facts, and where the truth comes from:
2525
2626 Limits: the window is the last 24 hours, the one `/api/v1/telemetry/home` reads; landings are counted, not named. A fact the page may rightly leave out (nothing waiting, no quiet repository) scores `stated` when the page does not claim otherwise. The cold-read agent of step 2 stays the steward's.
2727
28 ## The two scripts
29
30 **Reading the home as the eye does (#5735, step 1).** `bun scripts/home-read.ts <url>` fetches a page and prints its visible text, then one line: `chars N · words N · fetched in N ms`. Set `FORGE_COOKIE` (for example `forge_session=…; forge_mode=studio`) to read it as the owner; the cookie is sent and never printed. It exits non-zero on any status but 200. The conversion is `pageText` in `src/web/page_text.ts`: scripts, styles, templates, the head and anything `hidden` or `aria-hidden="true"` are dropped, link text stays, blocks sit on their own lines, entities are decoded. The method then continues: (2) a fresh agent with no context reads only that text and answers in at most 150 words, (3) the steward scores it against ground truth on coverage of the 5 things that mattered, errors, and seconds, (4) repeat after every feed change. The steward runs steps 2–4 and logs them on #5735.
31
32 **Scoring the home (#5805).** `bun scripts/home-eval.ts <url>` is the deterministic half of that loop: it reads `/` with the same text conversion, builds ground truth from the forge's own API (`GET /api/v1/telemetry/home` for landings, the live version, builders and why idle, spend against the ceiling and what waits on the owner; the repository list and each newest commit for repositories quiet 3 days), and prints each of the five facts as `stated`, `missing` or `wrong` with the page's words, then coverage, errors, characters and fetch time. No dependency was added.
33
2834 ## Results
2935
3036 One row per feed pull, newest last. Coverage is out of 5; `agent s` is the cold read's seconds (steward), `script` is the fetch time of `home-eval.ts`.
docs/METRICS.md +5
@@ -60,3 +60,8 @@ scrape_configs:
6060 static_configs:
6161 - targets: ["forge.example.com"]
6262 ```
63
64 `GET /metrics` (site admin only; repository names are in the labels) serves
65 `forge_egress_connections`, `forge_egress_bytes_in` and `forge_egress_bytes_out`
66 per `repo` and `destination` as Prometheus text, so a leaked upstream socket
67 shows as a gauge that never falls (#3719).
docs/OPERATING.md added +234

Diff not shown — a line 4,879 characters wide (limit 2,000). +234

View file
docs/ORIGINS.md +155
@@ -68,6 +68,96 @@ origin) which both surfaces display in its place, with the origin's mark. The
6868 connecting user owns the mirror, so "author-or-writer" checks resolving to
6969 them is correct.
7070
71 ## Connecting and mirroring, as a person does it
72
73 Outbound git refuses a host that resolves to a private address. A site admin can vouch for exact host names (the owner's own Forgejo, reached by its in-cluster address): `FORGE_OUTBOUND_TRUSTED_HOSTS=git.example.test` (comma or space separated), plus the same list editable by an admin on **Settings → Origins**. A trusted host may resolve privately for origin sync, mirror, migrate and write-through; trust is by name, never by address range, and webhooks never read it. The origin row says "trusted internal host". In the terminal, the palette's **Trusted hosts** reads and saves the same list (admins only, same audit). An entry that is not a bare host name (an address, a wildcard, a URL) is refused with its name, on the page and in the terminal, never silently dropped.
74
75 A pull from an outsider's fork on the origin is someone else's code, so its
76 check and preview wait. A person with write access lets that exact head run:
77 on the pull page, **Approve to run here**; in the terminal, `A` on the pull
78 ("approve to run here"); over the API, `POST
79 /api/v1/repos/:o/:n/pulls/:n/approve-here`. It is a local record — a review
80 written through to, or imported from, the origin never counts — and a new push
81 waits for a new approval.
82
83 A pull a child forge proposed upstream waits the same way, and the writer who
84 minted the child's token may let it run: **Let it run here** on the pull page,
85 `A` in the terminal, or the same `POST …/approve-here`, which answers
86 `{granted: true, commitOid}`. That grant is recorded apart from reviews: it
87 never counts toward `require_review` or the merge gate, a new push needs a new
88 grant, and a proposed pull never lands on its own, even on an autonomous repo.
89
90 Connect one at **Settings → Origins** (or `A` then `o` in the terminal). Which
91 way in you get is a property of the forge, not a preference:
92
93 - **GitHub** — a device code. The forge shows a short code and a URL; you type
94 the code on whatever device has a browser, and the forge does the polling. Setting
95 it up is three steps, once, for everyone on the forge:
96 1. Create an OAuth App at <https://github.com/settings/developers>. Name it
97 anything, set the homepage to your forge's URL, set the authorization
98 callback URL to `https://your.forge/settings/origins/callback`, and tick
99 **Enable Device Flow**.
100 2. Start the forge with `FORGE_GITHUB_CLIENT_ID=<client id>`.
101 `FORGE_GITHUB_CLIENT_SECRET` is optional and buys one thing:
102 disconnecting revokes the grant on GitHub too.
103 3. The GitHub button appears. Until it does, both surfaces print these three
104 steps rather than naming an environment variable and leaving you to it.
105
106 - **Forgejo / Gitea** — authorization code with PKCE. They implement no device
107 grant, and every instance needs its own OAuth application, so you paste a
108 client id once per host — or the operator registers the host once for
109 everyone, `FORGE_ORIGIN_CLIENT_ID=git.example=<client id>` (comma-separated
110 for more), and Connect is one click. The page gives you the exact redirect
111 URI to enter (`https://your.forge/settings/origins/callback`); a public
112 client is fine.
113 This flow needs the forge to know its own public URL, since that URL is
114 registered with the other forge and a `Host` header is not evidence: set
115 **`FORGE_URL`** to the canonical base URL (`FORGE_HOST` and loopback are the
116 fallbacks it trusts). Until one of those is true the card says so, and the
117 token path still works.
118 - **A pasted token** — the fallback that needs no registration anywhere. It is
119 validated against the origin before it is stored, encrypted at rest, and
120 never displayed again.
121
122 Both connect screens hand the job to whatever device has a browser: a **QR
123 code** to point a camera at, a **short link** (`https://your.forge/go/<10
124 characters>`, live for ten minutes and gone once used) short enough to type by
125 hand, and — in the terminal — `c` to put the link on _your_ clipboard through
126 your own terminal (OSC 52, so it works over SSH; iTerm2, Ghostty, kitty and
127 WezTerm honour it, Apple Terminal does not). `o` opens a browser only when the
128 forge's terminal is the one in front of you; over SSH it says so rather than
129 appearing to work.
130
131 Then pick what you want: **Repositories** on an origin lists everything the
132 token can see, and you mirror them one at a time. There is no "mirror
133 everything" — a mirror is a repository you own, and a hundred you did not
134 choose is not a feature.
135
136 A mirrored repo wears its origin in the header (`mirrored from owner/name @
137 host`), shows its last fetch and any fetch error in the About rail, and syncs
138 on a worker tick or on demand. Imported issues and comments show the login on
139 the _origin_, with that forge's mark, and link back to the canonical thread.
140 Merging a pull happens on the origin: the merge box says so and links there,
141 and the next sync brings back the merge commit it actually made. Pushes go
142 through too — the pre-receive hook forwards the push upstream first, so a push
143 the origin refuses never lands here either.
144
145 Mirror fetches are vetted and pinned: the origin's host is resolved once, refused
146 if it is a loopback, LAN or other private address, and git is pinned to the
147 address that passed. Git follows no redirects for a mirror, so give the clone
148 URL in its final form (https, with the `.git` suffix the host expects); a URL
149 that redirects fails with git's redirect error.
150
151 Only the person who connected an origin can write through it. Another writer
152 gets `origin_owner_only` rather than a comment appearing upstream under
153 someone else's name with someone else's token.
154
155 **Pins** are the bridge to the next section: the pin toggle on a repository
156 header (`p` on a row in the terminal, `P` on a repo's home) says "keep this on
157 my machine", and the dashboard grows an **On your machine** card listing them.
158 The server remembers only the choice — nothing about the state of a laptop —
159 and `forge sync` (the README's "On your machine") is what acts on it.
160
71161 ## Flows
72162
73163 **Connect.** Three ways in, and which one you get is a property of the forge
@@ -534,6 +624,40 @@ The clone runs as a job in the mirror's registry (`origins/jobs.ts`, kind
534624 app and the crew's first run — and then lands on the repository, which is
535625 the app's page, with what happened as the flash.
536626
627 **After a GitHub migration** you choose what happens to the original: leave it
628 alone (the default), archive it, or keep it as a read-only mirror. The choice is
629 the "Afterwards" field on `/migrate`, in the terminal's migrate screen, and
630 `after` (`leave`, `archive`, `mirror`) on `POST /api/v1/migrate`. Archive marks
631 the GitHub repository archived using the account you connected. Mirror leaves
632 GitHub untouched at first, then copies later changes on the branches GitHub had
633 at migration back to it; branches made afterwards, such as the crew's work
634 branches and pull heads, stay on the forge. If someone pushes to GitHub
635 meanwhile, the forge stops rather than overwrite them, and the repository's
636 settings page (and the terminal's status line) says why it stopped and when it
637 last pushed. "Stop mirroring" there, or `U` in the terminal, ends it and leaves
638 GitHub as it is. Archiving needs admin
639 rights on GitHub: without them the migration still succeeds and says the
640 repository was not archived, so you can do it there yourself. Only repositories
641 read through a connected origin have an original to change; a pasted URL does not.
642
643 **Migrate from the terminal.** Press `M` on the dashboard (or pick "Migrate a
644 repository" in the palette, `ctrl+k`). The list is your connected origins'
645 repositories, private ones marked; `/` filters it and `u` pastes a URL instead.
646 Enter on a repository lists its branches with their last commit, your own
647 newest first and marked "(suggested)", so Enter again takes it; a pasted URL
648 skips this step. The form asks for name, owner, visibility, agent, autonomy and afterwards
649 (leave the original alone, archive it, or keep it as a mirror; `tab` moves, `space` changes); `ctrl+d` migrates. The job is a checklist of
650 stages: `esc` hides it and never cancels, and a toast says when it lands, on the
651 new repository. It is the same `migrateRepo` as `/migrate` and
652 `POST /api/v1/migrate`: the chosen `branch` becomes the default branch and the
653 crew's brief says the app lives on it. The web form takes it too (it lists the
654 picked repository's branches newest first), as does the API's `branch` field.
655
656 A GitHub migration also carries each milestone (title, description, due date,
657 open or closed) as an issue here, with the due date and GitHub's milestone
658 number kept in `milestone_origin`; carrying again makes no duplicates. Which
659 issues belong to a milestone is not carried yet.
660
537661 ## Follow an upstream
538662
539663 A repository can follow a git URL outside the forge (`repo_upstream`,
@@ -564,6 +688,37 @@ to admins only. API, all admin-only (anyone else gets 404):
564688 `GET|PUT /api/v1/repos/:owner/:name/upstream` (PUT takes
565689 `{url, branch, enabled}`) and `POST …/upstream/check`.
566690
691 A followed repository's home shows admins "N commits behind owner/name" (or "conflicts with owner/name: resolve") from the last stored check, with an **Update** button: a same-origin POST form to the settings page's check action that returns to the home, no JS. The terminal's About card (`I`) has the same words as a `behind` row, and `u` is the same update (`behindWords`, `src/actions/upstream_follow.ts`).
692
693 A repository can follow an outside git branch (`repo_upstream`, https or
694 http only, no credentials in the address). The clock checks each following
695 repository every 30 minutes (`checkDue`); the result is the follow row:
696 upstream tip, commits behind, the pull carrying the last proposal, and
697 `error`. In the terminal, a repository's About card (`I`) shows an
698 `upstream` row (address, branch, last check, standing; `off` when following
699 is off) to an admin of the repository and to nobody else. `u` checks now,
700 while following is on, through the settings page's own action. `f` turns
701 following on or off through `setFollowUpstream`; it keeps the saved address
702 and branch, and when none is saved it refuses with "set the upstream address
703 in the web settings first" (the address is set on the settings page). A check fetches only when the tip moved, into
704 `refs/upstream-follow/<branch>` (not a branch), then:
705
706 - **Nothing new:** nothing happens.
707 - **New commits that merge cleanly into the default branch:** the forge
708 writes `upstream/<tip7>` at the fetched head and opens one pull from it,
709 as the person who turned following on. A second check with the same tip
710 opens nothing, and a proposal someone closed is not reopened.
711 - **A newer tip while that pull is open:** the pull is moved to
712 `upstream/<newtip7>` and its title and body rewritten; the old
713 `upstream/<old7>` branch stays, as a person may have built on it.
714 - **A conflict:** the row's `error` reads `conflict`; no branch, no pull. With
715 autonomy on, the check also opens one issue for that head and queues one
716 converge run for the crew to resolve it; with autonomy off, the note on the
717 row is all there is.
718 - **A planted branch:** if `upstream/<tip7>` already exists anywhere but at
719 the fetched head, the check fails (`squatted`), says so on the row, opens
720 no pull and leaves the branch as it was.
721
567722 ## API
568723
569724 ```
docs/PULLS.md +4

Diff not shown — a line 2,363 characters wide (limit 2,000). +4

View file
docs/SENTINEL.md +17 −1
@@ -26,9 +26,25 @@ returned is filed again. It files in `FORGE_SENTINEL_REPO` (default
2626 `trevato/forge`) as the `sentinel` agent, else the first admin.
2727
2828 **What it shows.** To a site admin only: the board's forge strip and the
29 terminal palette's `Sentinel` command show the count of open findings and the
29 terminal palette's `Sentinel` command (as a toast) show the count of open findings and the
3030 last hunt's time. A hunt is synchronous, so there is no "hunting" state to show.
3131
32 A check that has never been green is filed only for the sentinel's own repository. In any other repository it stays on that repository's checks and the degraded list.
33
34 A capacity setting away from its default is not a finding: the sentinel files
35 nothing for it (#3269). On `/settings/capacity` the row of each such setting
36 carries one muted line, "changed from X N days ago by who", and its **Put back**
37 button (in the row's own form, beside Save) restores the default through
38 `changeCapacity` (same validation, record, event and reversal). The line stays
39 until the value is back at the default; nothing else reminds anyone. The
40 JSON API and MCP carry the same fact: a setting away from its default has a
41 `changed` field (`at`, `by`) in the capacity listing (`listCapacity`).
42
43 The `setting:<key>` findings filed before that are all closed; none raises an
44 ask, and the capacity page's Put back is the one answer. The `sentinel_kept`
45 table they once used is unread; a migration drops it once the additive rule
46 (`src/core/releases.json`) allows a drop.
47
3248 **What it never quotes.** Evidence passes through `scrub` (`redactSecrets` and
3349 any 32-character unbroken token) before it is filed, since the repository may
3450 be public.
docs/SNAPSHOT.md +54
@@ -121,6 +121,14 @@ as failed. A put's deadline follows its size: its bytes at a floor of
121121 5 Mbit/s, plus 60 s. A fixed 180 s once failed every attempt at a 334 MB
122122 database behind a 15 Mbit/s uplink (#2536).
123123
124 ## Progress
125
126 A snapshot uploads eight objects at a time. Run logs older than a day go up
127 bundled per day (cut at 4 MB, so a long history is several objects, never one
128 huge one); restore unpacks them. While one runs, progress shows on
129 **Settings → Backups** and as a line in the log every 500 objects or minute.
130 The terminal has no backups view, so it has no readout.
131
124132 ## When a snapshot fails
125133
126134 Its outcome is kept in `snapshot/state.json` under the data directory, so a
@@ -162,6 +170,52 @@ restore from running a restore on every boot. Kept for the
162170 week, a drill red at 09:11 stood as a card long after the fix deployed at
163171 10:18 (#2541).
164172
173 **Run a drill** on the same page (admins only) restores the newest snapshot
174 into scratch, opens it, counts what is there and unseals a value; the page
175 waits for it, up to 10 minutes, and the last drill shows there as one line,
176 ending with the drill's time against its budget and each part's time, longest first ("6 min
177 12 s of its 14 min budget: clone 4 min, download 58 s, …"), or only the time against the
178 budget when no part was timed; drills from before this show none. The budget is 10 min
179 plus 1 s per MB of snapshot, at most 45 min (`docs/RECOVERY.md`).
180 The parts are download, decompress, clone, open, and start and migrate (the
181 whole side-boot of the restored forge, process start included). The same words,
182 slowest named, are in the drill's log line and in a red drill's card
183 ("… (download about 8 min, decompress 40 s, …; download was the slowest)").
184
185 Each snapshot's manifest keeps
186 the five counts of its database; a restore that counts differently is red at
187 `count` and, like any red drill, one board line and one push. Snapshots from
188 before the counts were kept are not compared.
189
190 ## Failure domains, and what the owner is told
191
192 **Failure domains** (a card at the foot of `/settings/backups`; the old
193 `/settings/resilience` sends there, and `/resilience` is the THREATS.md page): one row per failure domain from
194 `docs/DOMAINS.md` — what breaks, the words the forge says, and the domain's
195 last kept drill (when, green or red with its reason, seconds until back) or
196 the sentence on why it is not drilled. In the terminal it is the **Failure domains**
197 command (`ctrl+k`; the **Resilience** command is the THREATS.md one), shown to an admin only and read through the same
198 `listResilience` action. Only the latest drill per domain is shown.
199 The **Owner's needs** command (admin only, `ownerView`) is the owner's one screen: the forge's needs as the board strip says them, where snapshots go (and any mirror) with the newest snapshot, and every domain with what the owner does.
200
201 A red drill, or none in eight days, is one line for the forge's admin on the
202 board's forge strip, in the terminal's dashboard (same `forgeNeeds`), and in
203 MCP `health` and `asks`; it names the failed step and the next green drill
204 clears it. An admin with a phone URL gets one push per cause (a new red
205 drill, or the silence), also when eight days pass with no event: the notifier
206 wakes every 10 minutes for it. The time the forge first owed a drill is kept
207 at boot in `drill/first-seen`; if the disk will not take it, a warning is
208 logged and a forge with no drill ever is not flagged.
209
210 Snapshots written to a directory on the forge's own volume, with no off-box
211 mirror set, are lost with that volume. That is one line for the forge's admin,
212 "Snapshots are kept on the forge's own volume and mirrored nowhere: losing the
213 volume loses them too.", on the board's forge strip (linking Settings →
214 Backups), in the terminal's dashboard (same `forgeNeeds`), in the asks, in
215 `GET /api/v1/health` and MCP `health`. Anyone else sees nothing. It clears when
216 the destination moves to another volume or a bucket, or a mirror is set. Every
217 one of those reads the vitals through `forgeVitals` in `src/actions/health.ts`.
218
165219 ## Restore
166220
167221 The printable card, seven lines, is [RESTORE.md](RESTORE.md); a test runs it.
docs/SPAWN.md +34
@@ -73,6 +73,8 @@ Children report in; the parent never reaches in. Every ten minutes a spawned chi
7373
7474 `POST /api/v1/forges {name, apps[], crew[], direction?, keepAccess?, modelKey?, budgetUsd?, size?, dryRun}` (rw token, admin only) and the MCP `spawn` tool share `src/spawn/door.ts`. `size` is `small` (default), `medium` or `large`; anything else is a 422 on `size`. It is recorded in the plan, the ledger, `parent.json` and `GET /api/v1/forges`, but not enforced yet: it limits nothing. `dryRun` is true unless it is `false`: the answer is the plan, what is left behind, and `firstAdmin` (the caller's username, or `null` for an anonymous plan, where whoever claims the child becomes its admin), and nothing is made. `GET /api/v1/forges` lists the ledger's children. The child's org is its name; `crew` names the agents whose briefs travel. Errors: 401 unauthorized, 403 forbidden / cross_origin, 422 invalid, 409 refused / exists, 500 failed — each with one sentence. The MCP lets an `ro` token plan, never make.
7575
76 A spawn may also name a `size`: `small`, `medium` or `large` (`src/spawn/sizes.ts` maps each to CPU, memory and disk). It is recorded in the plan only when given, so a plan without one hashes as it always did, and an unknown size is a 422 on `size`. The New forge page and the terminal form both offer it, small preselected and not sent; the page shows a refusal beside the select, linked with `aria-describedby`. Nothing enforces the limits yet, and the table's numbers are provisional. `GET /api/v1/forges/sizes` (no sign-in, ETag, cached for 5 minutes) and the MCP tool `sizes` give the same answer — each size's CPU, memory and disk, read from that table, and one sentence on what it is for — so a page or an agent offering a size reads it there.
77
7678 ## The forge's own code, and the chain (#1085)
7779
7880 Every child is born with `<org>/forge`: a fork, with full history, of the
@@ -130,6 +132,29 @@ fork, its history and the record of where it came from stay, and nothing
130132 leaves. The page and the parent's tree then say it follows nothing since
131133 the date.
132134
135 A spawned forge's own code is a fork of its parent's (`<org>/forge`, chosen
136 `followMode` `tell-me` or `take-updates`; in `take-updates` the hourly check puts what the parent gained in a pull.
137 Once an hour, and
138 on demand, a forge that follows and has not severed fetches its parent's code into
139 `refs/upstream/*` of its own `<org>/forge` (no branch of the fork moves) and keeps
140 `{ behind, parentTip, checkedAt, error }` in `upstream.json` under `FORGE_APP_DATA`;
141 the count shows on the chain page, in the terminal's **Chain** (`c` checks now), and as
142 `upstream` in the API and MCP payloads. `POST /:owner/forge/following/check` (a writer,
143 same-origin) checks on demand; a fetch is cut at 60 s and a failure is recorded, not
144 thrown. In `take-updates` the check then puts `refs/upstream/HEAD` (a fast-forward, or a clean merge commit) on the
145 fork's `forge/take-updates` branch and opens one pull, "Take updates from <parent>", into main, or updates
146 that pull when it is open (`spawn/take.ts`); the fork's check runs on it like on any pull and a person merges it.
147 Main is never moved, so nothing deploys by itself. It is recorded as `taken` in `upstream.json` (the chain line
148 reads "N commits from your parent are in pull #P") and as a `forge.updates_taken` event; a pull closed unmerged
149 is not reopened until the parent moves again. `forge/take-updates` belongs to the check: a commit a person pushes there is overwritten by the next check that finds something different to put on it (open a branch of your own to build on it). On a conflict nothing changes and one open issue,
150 "Take updates from <parent>", names the files. In `tell-me`, a new parent tip makes one inbox card for the owner (an issue on the fork assigned to them, "Your parent forge has N new commits", linking the chain page); `toldTip` in `upstream.json` keeps it to one per tip, and `take-updates` or a severed link makes none. Its chain
151 to the root is at `/:owner/forge/following`, in `chain` of `GET
152 /api/v1/repos/:owner/forge/following` and the MCP `following` tool, and in the
153 terminal's **Chain** command; the owner severs it with the page's Sever,
154 `POST …/following/sever` or the MCP `sever` tool (nothing is deleted). Taking
155 updates into a conflict opens the converge issue at every door. See
156 `docs/FOLLOWING.md`.
157
133158 ## One click from a stranger to their own forge (#2914)
134159
135160 The program: a stranger makes an account with a username and a password (email
@@ -153,3 +178,12 @@ apps as forks, and the MCP `spawn`. What is missing is built as linked issues:
153178 | #2931 | nightly drill of the same path on the vxrail | #2930 |
154179 | #2932 | /boards: forges made, time to live, failures | |
155180 | #2933 | /forges: an owner's forges side by side, use against size | #2925, #2926 |
181
182 ## Child quota
183
184 `FORGE_CHILD_CPUS`, `FORGE_CHILD_MEMORY_MIB` and `FORGE_CHILD_DISK_MIB` declare
185 what all children together may hold; each is the sum of the sizes in the spawn
186 ledger (`spawn/sizes.ts`). **Unset means no quota.** A spawn that would pass a
187 declared limit is a 409 `refused` with one sentence naming it, and the New
188 forge page shows that sentence above the form when even a small forge would
189 not fit (`spawn/quota.ts`).
docs/TOUR.md added +438

Diff not shown — a line 2,062 characters wide (limit 2,000). +438

View file
src/tui/screens/dashboard.ts +1 −1
@@ -1161,7 +1161,7 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen {
11611161 note(s, inner, c, [
11621162 "Nothing pinned yet.",
11631163 "p pins the repository under the cursor;",
1164 'see "Origins" in the README for forge sync.',
1164 'for forge sync, see README "On your machine".',
11651165 ]);
11661166 return;
11671167 }
src/web/pages/site.ts +1 −1
@@ -155,7 +155,7 @@ export function downloadsCard(ctx: Ctx, user: User): Html {
155155 ${copyField("forge-sync", "forge sync", "Sync command", "Copy the sync command")}
156156 <span class="row-meta"
157157 >Clones what is missing, fetches the rest, and reports
158 ahead/behind/dirty. See "Origins" in the README.</span
158 ahead/behind/dirty. See "On your machine" in the README.</span
159159 >
160160 </span>
161161 </div>
test/readme-front-door.test.ts added +33
@@ -0,0 +1,33 @@
1 import { expect, test } from "bun:test";
2 import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3 import { join } from "node:path";
4
5 // README.md is the front door (#6226): what the forge is, how to run it, and
6 // an index into docs/. Feature prose goes in the doc that owns the topic.
7 const root = join(import.meta.dir, "..");
8 const readme = readFileSync(join(root, "README.md"), "utf8");
9 const INDEX = "## Where the rest is";
10
11 test("README.md stays under 60 KB", () => {
12 const size = statSync(join(root, "README.md")).size;
13 if (size >= 60 * 1024)
14 throw new Error(
15 `README.md is ${size} bytes, over 60 KB. Put feature prose in the docs/ file that owns the topic (README's "${INDEX}" lists them), not in README.md.`,
16 );
17 });
18
19 test("every docs/ link in README.md resolves", () => {
20 const links = [...readme.matchAll(/\]\((docs\/[^)#\s]*)/g)].map((m) => m[1]!);
21 expect(links.length).toBeGreaterThan(0);
22 const broken = links.filter((l) => !existsSync(join(root, l)));
23 expect(broken).toEqual([]);
24 });
25
26 test(`every top-level docs/*.md is linked from "${INDEX}"`, () => {
27 const at = readme.indexOf(`\n${INDEX}\n`);
28 expect(at).toBeGreaterThan(-1);
29 const index = readme.slice(at).split(/\n## /)[1]!;
30 const docs = readdirSync(join(root, "docs")).filter((f) => f.endsWith(".md"));
31 const unlisted = docs.filter((f) => !index.includes(`](docs/${f})`));
32 expect(unlisted).toEqual([]);
33 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs