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

Everything the forge needs to be reachable and to release is written down, with where it runs: today six of those things run on the DigitalOcean node #11692

Merged opened this in answer to

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

Merged

`docs/OPERATING.md` now has a table, "What the forge stands on", listing everything the forge needs to (a) answer a request, (b) sign a person in, (c) build, (d) release and (e) back up. Each row gives the machine it ran on on 2026-11-12, what happens to the forge if that machine dies, and the issue that removes the dependency. There are 11 rows: the cloudflared tunnel, Traefik, the control plane,…

shipped merged

opened this

docs/OPERATING.md now has a table, "What the forge stands on", listing everything the forge needs to (a) answer a request, (b) sign a person in, (c) build, (d) release and (e) back up. Each row gives the machine it ran on on 2026-11-12, what happens to the forge if that machine dies, and the issue that removes the dependency. There are 11 rows: the cloudflared tunnel, Traefik, the control plane, plat-auth, Forgejo, its registry, the release tick, Flux, the backup CronJobs, MinIO and CloudNativePG. All 11 are on the DigitalOcean node. Where no open issue removes a dependency on its own, the row points at the umbrella #11686.

The probe. The list lives in FOOTING in src/system/footing.ts, and footingTable() writes the doc table from it. test/footing.test.ts fails if the table in the doc and the list differ. probeFooting asks the cluster's API, using the forge's own service account, for:

  • the node the forge's pod runs on,
  • the nodes labelled control-plane,
  • the pods in each namespace on the list.

Each piece shows the node it was found on, and any piece not on the forge's node is a risk line. A piece the cluster can't show keeps the machine written in the list and says why:

  • the release tick is a systemd unit on the host, so it is placed on the control-plane node;
  • a backup CronJob has no pod between runs;
  • a namespace the service account may not list says so.

The probe never throws and is not run in the background. One reading is kept in memory for 60 s and shared by the page, the terminal and the MCP tool; that shared state is named in AGENTS.md. A page waits at most 0.8 s for a fresh reading and otherwise shows the written list.

Where it shows:

  • /system (owner only) has one section with the heading "What the forge stands on".
  • The terminal's System screen has two lines: whose reading it is, then how many pieces are off the forge's machine and which.
  • The MCP system tool answers { kind: "footing" } with the rows and the risk lines, for forge admins only.

What to look at:

  • Names not yet checked against the cluster. The issue gave standby/cloudflared and mcp/plat-auth. kube-system/traefik, forgejo, flux-system, backup, minio and cnpg-system are my best guess from docs/PLATFORM.md and common defaults. A wrong name shows on /system as "no pod running in <ns> now" instead of failing silently.
  • Permissions the forge may not have. For every row to come from the live cluster, the forge's service account needs get/list on nodes and list on pods in those namespaces. That role lives in the gitops repository, which can't be changed from here (#8948). Until it is granted, the affected rows say the service account may not read them.

Tests. Changes to existing tests: test/system-parts.test.ts lists /system's headings, so it now includes the new one (the test was right; the page gained a part). test/tui-system.test.ts now also checks the new terminal lines.

bun scripts/gate.ts, bun x tsc --noEmit, bun scripts/design-lint.ts, and the footing, system, terminal, MCP and llms tests pass here. scripts/fast.ts called the diff too broad to pick tests for (1,927 files), so I chose the files myself. In one run of 22 files together, test/mcp-system-routes.test.ts failed one test that reads shared route timings; on its own it passes, both with and without this change.

Try it: open /system on the preview as the owner and read "What the forge stands on". Or call the MCP system tool with {"kind":"footing"}.

🤖 Generated with Claude Code

Closes #11685

commented

docs/OPERATING.md now has a table, "What the forge stands on", listing everything the forge needs to (a) answer a request, (b) sign a person in, (c) build, (d) release and (e) back up. Each row gives the machine it ran on on 2026-11-12, what happens to the forge if that machine dies, and the issue that removes the dependency. There are 11 rows: the cloudflared tunnel, Traefik, the control plane, plat-auth, Forgejo, its registry, the release tick, Flux, the backup CronJobs, MinIO and CloudNativePG. All 11 are on the DigitalOcean node. Where no open issue removes a dependency on its own, the row points at the umbrella #11686.

The probe. The list lives in FOOTING in src/system/footing.ts, and footingTable() writes the doc table from it. test/footing.test.ts fails if the table in the doc and the list differ. probeFooting asks the cluster's API, using the forge's own service account, for:

  • the node the forge's pod runs on,
  • the nodes labelled control-plane,
  • the pods in each namespace on the list.

Each piece shows the node it was found on, and any piece not on the forge's node is a risk line. A piece the cluster can't show keeps the machine written in the list and says why:

  • the release tick is a systemd unit on the host, so it is placed on the control-plane node;
  • a backup CronJob has no pod between runs;
  • a namespace the service account may not list says so.

The probe never throws and is not run in the background. One reading is kept in memory for 60 s and shared by the page, the terminal and the MCP tool; that shared state is named in AGENTS.md. A page waits at most 0.8 s for a fresh reading and otherwise shows the written list.

Where it shows:

  • /system (owner only) has one section with the heading "What the forge stands on".
  • The terminal's System screen has two lines: whose reading it is, then how many pieces are off the forge's machine and which.
  • The MCP system tool answers { kind: "footing" } with the rows and the risk lines, for forge admins only.

What to look at:

  • Names not yet checked against the cluster. The issue gave standby/cloudflared and mcp/plat-auth. kube-system/traefik, forgejo, flux-system, backup, minio and cnpg-system are my best guess from docs/PLATFORM.md and common defaults. A wrong name shows on /system as "no pod running in <ns> now" instead of failing silently.
  • Permissions the forge may not have. For every row to come from the live cluster, the forge's service account needs get/list on nodes and list on pods in those namespaces. That role lives in the gitops repository, which can't be changed from here (#8948). Until it is granted, the affected rows say the service account may not read them.

Tests. Changes to existing tests: test/system-parts.test.ts lists /system's headings, so it now includes the new one (the test was right; the page gained a part). test/tui-system.test.ts now also checks the new terminal lines.

bun scripts/gate.ts, bun x tsc --noEmit, bun scripts/design-lint.ts, and the footing, system, terminal, MCP and llms tests pass here. scripts/fast.ts called the diff too broad to pick tests for (1,927 files), so I chose the files myself. In one run of 22 files together, test/mcp-system-routes.test.ts failed one test that reads shared route timings; on its own it passes, both with and without this change.

Try it: open /system on the preview as the owner and read "What the forge stands on". Or call the MCP system tool with {"kind":"footing"}.

🤖 Generated with Claude Code

commented

The list of what the forge stands on is now the steward's inventory from 2026-11-12. It runs from first break to last, so the owner can work through dropping pg-grand (the DigitalOcean node) in that order.

What changed

  • Machines. The two machines are called pg-grand and vxrail, as the inventory names them. MACHINES describes each one, including that the owner decided to drop pg-grand. docs/OPERATING.md and /system both show these descriptions.
  • The list now has 19 pieces (src/system/footing.ts):

- The forge's own pod and its 60 Gi volume, read from the cluster. - standby/cloudflared, now on the vxrail. Its last-applied manifest still says 0 replicas, so a re-render moves it back. - cloudflared-forge-home, the vxrail's own tunnel, which also sends *.open-platform.sh through Traefik. - The child, app and custom-domain routes (forge-children, forge-apps, forge-old-host, the two cheffryjames.com hosts). - forge-ssh, the signup gate, forgejo-runner-vxrail, registry-node, and Forgejo's database with its daily Backblaze copy. - standby-reconcile replaces Flux, whose Kustomization for the forge is suspended. The watchdog, GitHub mirror and image-pruning timers are one row. - The MinIO row names the 2.3 GB bucket and says no off-site copy of the forge's data was found.

  • Break order. Each pg-grand piece carries the step at which it breaks the forge if the machine is switched off: Traefik, then plat-auth, the signup gate, image pulls and releases, the control plane, local backups, and the host timers. Risk lines on /system, from the MCP system tool and on the terminal follow that order. /system also says "Breaks N of 7 with pg-grand off" on each risky row.
  • What the forge sees itself. The probe reads the forge's own pod as one piece. It looks for the signup gate, the runner and registry-node across the whole cluster with one request, because the inventory doesn't name their namespace. If the cluster refuses that request, those rows keep the machine the list declares and say why.
  • The doc. The block between the markers in docs/OPERATING.md is generated and a test holds it equal to the list. It now has the two machines, the table with a new "Breaks" column, the numbered break order, and what else lives on pg-grand. The table writer escapes | and <…> so placeholders don't turn into HTML tags.

What to look at

  • Pieces with no known namespace need list on pods across the whole cluster to show where they actually run. That permission is noted in OPERATING.md under #8948 and not granted here.
  • Host units (release tick, standby-reconcile, the timers, forge-ssh) are placed with the control plane, because the cluster can't see a systemd unit.
  • I'm not sure what registry-node is, so its row only calls it "a registry on the vxrail".

Tests

  • test/footing.test.ts now covers: the inventory's layout, the break order, the cluster-wide lookup and what happens when it's refused, the forge's own pod as a piece, markdown escaping, and the page's machine lines and break steps.
  • test/tui-system.test.ts was hard-coded to 11 pieces; it now uses the list's length and checks the break order.
  • The gate, tsc, design-lint and the footing, terminal and system-parts suites pass.
  • scripts/fast.ts named 1927 files, so I ran the system and MCP suites by hand. test/mcp-system-routes.test.ts failed once when run in the same process as the other system suites, and passes on its own with and without this change.

Try it: open /system as the owner and read "What the forge stands on". Or call the MCP system tool with {"kind":"footing"}: risks comes back in break order.

🤖 Generated with Claude Code

commented

The "What the forge stands on" section on /system and in the MCP system tool now passes the check that failed. The section takes less room in the first view and no longer waits on the cluster, so the page stays fast.

  • /system section: only the heading and one count stay in view ("3 of N off its machine", or "all on its machine"). The machine notes, the per-piece rows and the risk lines sit behind one closed disclosure, #sys-footing-more. This puts /system back inside its first-view budget (attention.test.ts) and its note limit (first-sight-notes.test.ts).
  • The page never waits on the cluster: /system used to wait up to 800 ms for the live cluster reading. It now uses footing.peek(). That gives the last reading, or the rows declared in OPERATING.md while the first read runs in the background. The MCP { kind: "footing" } call still waits up to 800 ms, because a caller asking for that kind wants live data.
  • A new test in test/footing.test.ts checks that the rows are inside the closed disclosure and that /system answers in under 800 ms.
  • The read's timeout timer in src/system/footing.ts is added to the timeout allow list in test/timers-named.test.ts. Its only job is to settle a promise the caller is waiting on.
  • docs/MCP.md is regenerated with bun scripts/mcp-table.ts, and a long comment in test/footing.test.ts is wrapped to the 160-column limit.
  • A test was wrong, and I changed it: test/mcp-system-routes.test.ts assumed the process's request record started empty. test/attention.test.ts also fetches /explore and /boards, so in a shared shard the count came out 26 instead of 25. The test now subtracts the samples already recorded when it starts.
  • main is merged in.

Before pushing, the gate, bun x tsc --noEmit and bun scripts/design-lint.ts passed. So did these nine test files run together: footing, mcp-system-routes, first-sight-notes, attention, system-parts, tui-system, timers-named, header-width and mcp-table. scripts/fast.ts counted the merge from main and named 1,967 test files, far over its 5-minute budget, so I ran only these nine. The forge's check runs the rest on this push.

To try it: open /system as an admin and expand the count under "What the forge stands on". Or call the MCP system tool with { "kind": "footing" }.

🤖 Generated with Claude Code

Changes requested — before the latest push

Changes needed, one round. I did not run the preview or read the screenshots; this is from the diff.

Fix first: Both doors, docs. There is no docs/features/<name>.md for this feature. Its how-to sits in docs/OPERATING.md under "What the forge stands on". Add docs/features/footing.md (how to read /system, the terminal screen and the MCP kind: footing) and link it from OPERATING.md.

Both doors, terminal. The terminal gets two summary lines inside the System dialog. The page offers the rows, each piece's node, its if-dies text and the break order, so the terminal does not match the page. Add a screen in src/tui/ (entered where AGENTS.md says) that lists the risk rows in break order. A test must cover it for an admin, and for a guest who sees nothing.

Direction. The direction is PIPELINE FIRST: the pipeline items come before any feature. This is an infra-inventory feature on /system, and it touches 12 files and about 940 lines. If the owner explicitly prioritised #11685, say so in the pull and name the issue. Otherwise it waits behind the pipeline items.

Honesty of the reading (src/system/footing.ts, place, the host branch). Host units (forge-ssh, release-tick, standby-reconcile, host-timers) are reported as seen: {by:"cluster", nodes: controlNodes} and then judged as a risk. Nothing in the cluster saw those units. This is a guess presented as a reading, in the UI and in the MCP JSON. Report them as by:"declared" with the reason, and keep the risk decision on the declared machine.

Stale facts in code (MACHINES, FOOTING). Figures dated 2026-11-12 are hard-coded as if current: "Disk 84% used", "2.3 GB", "128 databases", "261 volumes". They appear on /system and in the MCP output with no date. Drop the numbers, or date each one on screen.

Cost of refresh (footingCache.peek).

  • Every /system view starts about 12 kube requests, including a cluster-wide /api/v1/pods list. That list is large with about 100 tenant namespaces, and it is buffered in memory with a 3 s timeout.
  • If the probe fails, last stays null, so every page view retries. Add a failure back-off of at least 60 s, or cache the declared result.
  • read() leaves its 800 ms timer pending after a fast read. Clear it, instead of adding an allow-list entry to test/timers-named.test.ts.

Tests. "outside a cluster" in test/footing.test.ts returns early when KUBERNETES_SERVICE_HOST is set, so it can silently test nothing. Inject credentials that throw instead. Also add a test that the terminal footing lines are empty for a guest. And test/mcp-system-routes.test.ts should be fixed by isolating state, not by subtracting counts.

Design (src/web/pages/system.ts, footingSection).

  • Principle 1: the section says the same thing twice. The disclosure summary says "N of M off its machine", and the paragraph inside repeats it.
  • Principle 5: badge("risk", {tone:"danger"}) on every row adds a second colour to the page. Use muted text, or let the row's data attribute carry it.
  • Principle 7: each row stacks the title, a badge, the need, the node, if-dies and the break step into one meta line. Use a dl row per fact, or show only the risky rows by default.
  • Principle 8: "footing" and "risk" are jargon. Use plain words, as docs/WRITING.md does.

Information exposure. docs/OPERATING.md and the MCP output now carry details of the infrastructure: where the only backup copy lives, which units are unprotected, and disk fill. Confirm the repository is private, or trim the public doc to what an operator needs.

The security floor is otherwise respected: the MCP kind and /system are admin-gated, the kube calls are GET-only, and nothing is built from user text.

commented

Adds What the forge stands on for #11685. #11685 was opened by the owner (trevato) and labelled P0 Trust, which is why it goes ahead of the pipeline items. Each thing the forge needs to answer a request, sign someone in, build, release and back up is listed with the machine it runs on and what happens if that machine dies. The list is a table in docs/OPERATING.md (a test keeps it equal to FOOTING). It is also read from the live cluster in three places, all for admins only: /system, a new terminal screen, and the MCP system tool with kind: footing.

What this round changed, following review:

  • Docs: docs/features/footing.md explains how to read /system, the terminal screen and the MCP answer; OPERATING.md links to it.
  • Terminal: new screen src/tui/screens/footing.ts, listed in the palette right after Main-thread stalls. It lists each piece on another machine in the order switching pg-grand off would break the forge: step, need, node and if-dies. Then it shows the steps and the pieces on the forge's own machine. The two summary lines in the System dialog are gone. Tests cover the admin's rows and order, and that a signed-in non-admin and a signed-out session get no lines and no palette entry.
  • Host units are no longer guessed: forge-ssh, the release tick, standby-reconcile and the host timers are reported as declared, with the reason (the cluster cannot see a systemd unit). Whether each counts as a problem is decided from its declared machine.
  • Cheaper refresh:

- The probe no longer lists every pod in the cluster. The signup gate is looked for in the forge's own namespace, since an Ingress can only route to its own namespace. The runner and registry-node are declared as "namespace not recorded". - A probe that fails is not retried for 60 s. - read() now clears its deadline timer.

  • Stale figures and public detail: disk fill, sizes and database/volume counts are removed. So are the kine path, the release-script path and admin token, the middleware names, the bucket name, and the "only copy / off-site" wording. The repository is public, so the doc now carries only what an operator needs.
  • /system:

- The count appears once, in the disclosure summary. - The danger badges are gone. - Each off-machine piece is a row with one fact per line, in break order. - The pieces on the forge's own machine are named in one line. - The page no longer uses "risk" or "footing", and a test checks this.

  • Tests:

- The "outside a cluster" test now injects credentials that throw, so it always runs. - New tests cover the back-off and the cleared timer. - The routes test no longer subtracts counts. It calls a new PerfRecord.clear() first, the same way loopWatch.clear() and stallBlame.clear() are used. Using route names nothing else records was not enough: shard-mates had filled the record's 200-pattern cap, so new names were dropped. clear() has its own test.

Where I differ from the review: the deadline timer is cleared, but its entry stays on the allow-list in test/timers-named.test.ts. That test requires every timer callback that resolves a promise to be listed, cleared or not. Other deadline timers in the codebase have the same kind of entry.

Checks run here: bun scripts/gate.ts, bun x tsc --noEmit and bun scripts/design-lint.ts all pass. scripts/fast.ts named 1,970 test files, too many to run here, so I ran the covering tests by hand: footing, palette roster and entries, tui-system, the system pages, MCP system/table/docs, timers-named, perf, both-doors, docs-link and readme-cap. All pass.

To try it: open /system as an admin and expand "What the forge stands on". In the terminal, open the palette and choose "What the forge stands on". Over MCP, call system with { "kind": "footing" }.

🤖 Generated with Claude Code

commented

Adds What the forge stands on for #11685. #11685 was opened by the owner (trevato) and labelled P0 Trust. That is why it goes ahead of the pipeline items. A commit now says so in a Direction: line, which the gate's new "pipeline first" rule requires.

The pieces the forge needs to answer a request, sign someone in, build, release and back up are listed. Each has the machine it runs on and what happens if that machine dies. The list is a table in docs/OPERATING.md, and a test keeps it equal to FOOTING. It is also read from the live cluster in three places, all for admins only: /system, a new terminal screen, and the MCP system tool with kind: footing.

What changed in the last round of review:

  • Docs: docs/features/footing.md explains how to read /system, the terminal screen and the MCP answer. OPERATING.md links to it.
  • Terminal: a new screen, src/tui/screens/footing.ts, follows Main-thread stalls in the palette. It lists each piece on another machine in break order, with its node and what happens if that machine dies. Then it shows the steps and the pieces on the forge's own machine. The two summary lines in the System dialog are gone.

- Tests cover what an admin sees and the order of the rows. - They also check that a signed-in non-admin and a signed-out session get no lines and no palette entry.

  • Host units are no longer guessed: systemd units are now marked as declared, with the reason, instead of being placed on the control-plane nodes. Whether each counts as a problem is decided by its declared machine.
  • Cheaper refresh:

- The probe no longer lists every pod in the cluster. The signup gate is looked for in the forge's own namespace. Pods whose namespace is not recorded are marked as declared. - A probe that fails is not retried for 60 s. - The read's deadline timer is cleared.

  • No stale figures, less detail in public: disk fill, sizes and counts are removed. So are the paths, the token and the bucket name. The repository is public, so the doc carries only what an operator needs.
  • /system:

- The count appears once, in the disclosure summary. - The danger badges are gone. - Each off-machine piece gets one row, with one line per fact, in break order. - The page says neither "risk" nor "footing".

  • Tests:

- The "outside a cluster" test now injects credentials that throw, so it always runs. - New tests cover the back-off and the cleared timer. - The routes test no longer subtracts counts. It starts from a new PerfRecord.clear(), which has its own test. Giving it route names nothing else records was not enough: other tests in its shard had already filled the record's 200-pattern cap.

Where I differ from the review: the deadline timer is cleared, but it keeps its entry on the allow-list in test/timers-named.test.ts. That test requires every timer callback that resolves a promise to be listed, whether or not it is cleared.

Checks: bun scripts/gate.ts passes, including against the main base the forge's check uses. bun x tsc --noEmit and bun scripts/design-lint.ts pass. scripts/fast.ts named 1,970 test files, too many to run here, so I ran the covering tests by hand, and they all pass. The fix for the gate failure is an empty commit and changes no code.

To try it:

  • Open /system as an admin and expand "What the forge stands on".
  • In the terminal, choose "What the forge stands on" from the palette.
  • Over MCP, call system with { "kind": "footing" }.

🤖 Generated with Claude Code

Approved

Approved. The earlier asks are met in the diff: docs/features/footing.md is added and README is untouched. The terminal has its own screen, gated like the page, with palette entry and tests. Host units are reported as declared, with the reason. The probe no longer lists every pod in the cluster, and a failed probe is not retried for 60 s. The deadline timer is cleared. Stale figures and public detail are gone. /system shows one count in a disclosure, with rows and dl facts, and works without JavaScript. The MCP kind is admin-only. The only change since my last review is an empty commit for the gate's Direction line, so nothing new to raise. The clone lacks my last-reviewed head, so I judged against the full diff. #11685 is owner-labelled P0 Trust, so going ahead of the pipeline list is justified. Non-blocking: the deadline timer keeps its allow-list entry in test/timers-named.test.ts, which the builder explained.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs