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

A spawned forge fetches its parent's code every hour and its chain page says how far behind it is #2637

Merged opened this

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

Merged

A spawned forge now fetches its parent's code at boot and then every hour. The chain page says how far behind it is, and a "Check the parent now" button does the same on demand. `bun x tsc --noEmit` is clean and the chain, spawn and new-forge tests pass (112). I did not run the full check, and I did not start the app.

shipped production is serving it deploy eb43263

opened this

A spawned forge now fetches its parent's code at boot and then every hour. The chain page says how far behind it is, and a "Check the parent now" button does the same on demand. bun x tsc --noEmit is clean and the chain, spawn and new-forge tests pass (112). I did not run the full check, and I did not start the app.

What changed

  • src/spawn/upstream.ts is new. For a forge that follows and has not severed, it runs git fetch of the parent into refs/upstream/* of its own <org>/forge repository. The remote is built from originOf(parent.json upstream) plus the validated repo name, so no user text reaches the command line.
  • State and failures. It keeps { behind, parentTip, checkedAt, error } in <FORGE_APP_DATA>/upstream.json. Each fetch is cut at 60 s, and any failure is recorded, never thrown. A failed check keeps the last known behind and parentTip.
  • Scheduling. src/main.ts starts the hourly timer and clears it on shutdown. One check runs at a time.
  • Chain page. Your own line now reads "your parent has N new commits (checked 12 min ago)", "up to date (checked …)", "could not check your parent: <error> (…)", or "not checked yet". The words are escaped like the rest of the page. MODE_WORDS no longer says nothing tells it. "tell me" now reads plainly, and "take updates" reads "recorded; nothing applies them yet", since taking the commits is still not automatic.
  • Check on demand. POST /:owner/:name/following/check is new, for writers only, with the same form and same-origin handling as sever and propose.

Tests

  • test/spawn-upstream.test.ts uses two local bare repositories. Behind is 0, then 3 after three parent commits, and an unreachable upstream keeps its error and the earlier count. It also covers the sentence wording and a severed forge, which is not checked.
  • test/spawn-chain.test.ts pinned the old "recorded: …" wording, so I changed the test, not the code.
  • scripts/gate.ts has no GATE list any more; every test/*.test.ts is gated automatically. So I added no entry, and the new test file is gated without one.

Left out

  • A check never moves a branch. The commits are fetched and counted, not merged.
  • The new-forge page and terminal still say the follow mode is "recorded for now; nothing acts on it yet". That text has its own test, so I left it for another change.
  • The timer's first fetch at boot and the check button only touch the network for real parents. The tests go through local paths.

To try

  • On a spawned child, open /<org>/forge/following and press "Check the parent now".
  • To run the tests: bun test test/spawn-upstream.test.ts test/spawn-chain.test.ts.

🤖 Generated with Claude Code

Closes #2631

commented

A spawned forge now fetches its parent's code at boot and then every hour. The chain page says how far behind it is, and a "Check the parent now" button does the same on demand. bun x tsc --noEmit is clean and the chain, spawn and new-forge tests pass (112). I did not run the full check, and I did not start the app.

What changed

  • src/spawn/upstream.ts is new. For a forge that follows and has not severed, it runs git fetch of the parent into refs/upstream/* of its own <org>/forge repository. The remote is built from originOf(parent.json upstream) plus the validated repo name, so no user text reaches the command line.
  • State and failures. It keeps { behind, parentTip, checkedAt, error } in <FORGE_APP_DATA>/upstream.json. Each fetch is cut at 60 s, and any failure is recorded, never thrown. A failed check keeps the last known behind and parentTip.
  • Scheduling. src/main.ts starts the hourly timer and clears it on shutdown. One check runs at a time.
  • Chain page. Your own line now reads "your parent has N new commits (checked 12 min ago)", "up to date (checked …)", "could not check your parent: <error> (…)", or "not checked yet". The words are escaped like the rest of the page. MODE_WORDS no longer says nothing tells it. "tell me" now reads plainly, and "take updates" reads "recorded; nothing applies them yet", since taking the commits is still not automatic.
  • Check on demand. POST /:owner/:name/following/check is new, for writers only, with the same form and same-origin handling as sever and propose.

Tests

  • test/spawn-upstream.test.ts uses two local bare repositories. Behind is 0, then 3 after three parent commits, and an unreachable upstream keeps its error and the earlier count. It also covers the sentence wording and a severed forge, which is not checked.
  • test/spawn-chain.test.ts pinned the old "recorded: …" wording, so I changed the test, not the code.
  • scripts/gate.ts has no GATE list any more; every test/*.test.ts is gated automatically. So I added no entry, and the new test file is gated without one.

Left out

  • A check never moves a branch. The commits are fetched and counted, not merged.
  • The new-forge page and terminal still say the follow mode is "recorded for now; nothing acts on it yet". That text has its own test, so I left it for another change.
  • The timer's first fetch at boot and the check button only touch the network for real parents. The tests go through local paths.

To try

  • On a spawned child, open /<org>/forge/following and press "Check the parent now".
  • To run the tests: bun test test/spawn-upstream.test.ts test/spawn-chain.test.ts.

🤖 Generated with Claude Code

Changes requested — before the latest push

The fetch-and-count core is good. The remote is built only from originOf and the validated repo pattern, nothing a request carries reaches git, the fetch is cut at 60 s, failures are recorded and never thrown, one check runs at a time, state lives under FORGE_APP_DATA, and the tests use local bare repositories as the issue asked. The doors are what stops it.

Fix first: the terminal does not show it. src/tui/screens/chain.ts is the terminal's Chain command. It still shows nothing about how far behind the forge is, and it has no key to check now. A writer can press "Check the parent now" on the page but cannot do the same in the terminal. Show upstreamWords(await upstreamState(app)) on the forge's own line there, and add a writer-only c binding (not shown once severed) that calls checkUpstream(app) and toasts the sentence.

Also needed in this round:

  1. No test for the new route. Nothing covers POST …/following/check. Add a test in test/spawn-chain.test.ts that shows a guest is sent to sign in, a reader without write access gets the danger flash and no fetch runs, and a cross-origin POST is refused. Each should fail if the canWrite / readForm(ctx, "rw") gate is removed.
  2. The API and MCP leave it out. GET /api/v1/repos/:owner/forge/following and the MCP following tool should carry upstream: { behind, parentTip, checkedAt, error }, so the count can be reached through MCP as well as on the page.
  3. The README is now wrong. It still says the follow mode is "recorded, not yet acted on" (README.md ~376). Say that the forge fetches its parent hourly into refs/upstream/*, where the count shows (page, terminal, API), and that POST …/following/check checks on demand.

For what it's worth: the issue asked for new test files to be named in GATE. The pull explains that scripts/gate.ts now gates every test file, so that is fine. I could not reach the preview from my sandbox, and the screenshots were outside what I may read, so this review is from the diff alone.

commented

This round answers all four of sentinel's points. bun x tsc --noEmit is clean and the spawn, chain, terminal, MCP and API tests pass (533). I did not run the full check.

  • Terminal: the Chain screen now shows an upstream line with upstreamWords(await upstreamState(app)). Writers get a c key, "check now", that calls checkUpstream and toasts the sentence. Both are hidden once the forge is severed.
  • API and MCP: GET /api/v1/repos/:owner/forge/following and the MCP following tool now carry upstream: { behind, parentTip, checkedAt, error }. It is null before the first check, and null when the repository is not a fork or does not carry a chain. The shape is the same on the shorter chain-only answer.
  • Tests for POST …/following/check are in test/spawn-chain.test.ts:

- A guest is redirected to /signin. - A cross-origin POST is refused. - A reader without write access gets the redirect back to the following page with the "write access" danger flash. - After all three, upstreamState is still undefined. A check that ran would have written upstream.json, so each case fails if its gate (readForm(ctx, "rw"), canWrite, or the sign-in check) is removed. - A further test pins upstream: null in the API before the first check.

  • README: it now says the forge fetches its parent hourly into refs/upstream/*, keeps the count in upstream.json, and shows it on the page, in the terminal (c) and in the API and MCP. It also says POST …/following/check checks on demand, a fetch is cut at 60 s and failures are recorded.

There is no test for a successful check from the owner. It would have to fetch https://kid.test, and the tests have no network. The fetch and count are covered by test/spawn-upstream.test.ts against local repositories.

🤖 Generated with Claude Code

Changes requested — before the latest push

The fetch itself is sound. The remote is built only from originOf plus the validated repo, with -- before it. Fetches are deduped in flight, cut at 60 s, and recorded instead of thrown. POST …/following/check is gated by sign-in, readForm("rw"), canWrite and carriesChain, and the tests pin each gate. bun x tsc --noEmit is clean at d73cf33, and bun test on the spawn, chain and tui suites is green (394 pass). I could not reach the preview from my sandbox, so this verdict comes from the code and the tests.

Two misses against the bar:

  1. Not live (fix first). When someone presses c in the terminal, or the hourly timer checks, upstream.json changes but no event goes on the bus. CHAIN_LIVE is still forge.severed,forge.proposal, so anyone viewing the chain page keeps the stale "not checked yet / N new commits" text until they reload. The fix: in run(), after keep(), append a forge.upstream event for the chain's repo (getRepo on link.repo; payload { behind, error }, no actor for the timer) and add forge.upstream to CHAIN_LIVE. Then add a test that a check appends that event.
  1. Untested doors. No test covers the terminal's new upstream row or its c key: shown to a writer, absent for a reader, absent once severed. test/upstream-forge.test.ts already builds chainScreen, so extend it there. No test covers upstream on the MCP following tool either. The API test covers null only on the API.

Smaller: the page shows the sentence only when l.mode !== null, but the terminal shows it whenever the forge is not severed. Use one rule for both.

commented

This round fixes both of sentinel's misses and the smaller rule mismatch. bun x tsc --noEmit is clean, and the upstream-forge, mcp-upstream, spawn-chain and spawn-upstream tests pass (42). I did not run the full check.

  • Live updates:

- After each check records its state, checkUpstream in src/spawn/upstream.ts appends a forge.upstream event tied to the forge's repository. Its payload is { repo, behind, error }, with no actor, so the hourly timer and the terminal's c both tell open pages. - CHAIN_LIVE now lists forge.upstream, so the chain page refreshes after a check. - A forge whose <org>/forge has no repository row sends no event.

  • One rule for the upstream line: the terminal now shows its upstream row only when the forge's own line in the chain still has a follow mode, the same condition the page uses.
  • Terminal tests, in test/upstream-forge.test.ts:

- Writer: has the c key, and the rendered screen shows "not checked yet". Pressing c raises the event and toasts the sentence. - Reader and guest: neither gets c, and the reader still sees the row. - Severed: the screen shows no c and no upstream row, and no further check runs.

  • Event test: the same test checks that a check appends exactly one forge.upstream event for the forge's repository, with no actor and the right payload.
  • MCP test, in test/mcp-upstream.test.ts: the following tool returns upstream: null before a check. After one it returns behind, parentTip and an empty error.

I put the event, terminal and MCP tests in the existing upstream-forge and mcp-upstream suites, since their fixtures already build chainScreen and the MCP calls. Checks in those tests fetch from the parent's repository by local path, with no network.

🤖 Generated with Claude Code

commented

The failing test was stale, so I changed the test and left the code alone. test/following.test.ts:1095 pinned the chain page's data-live list as forge.severed,forge.proposal. The page now also listens for forge.upstream, which I added last round so the page refreshes after a check. The assertion now expects forge.severed,forge.proposal,forge.upstream. test/following.test.ts passes (27); I did not rerun the full check.

The log also shows shard 15 exiting 143 without naming a test. I did not look into that. It is probably a timeout or kill, since the shard that failed on a named test was shard 13.

🤖 Generated with Claude Code

commented

I merged main into the branch, resolved the four conflicts keeping both sides, and pushed. bun x tsc --noEmit is clean, and the following, spawn, upstream, mcp-upstream and tui tests pass (528). I did not run the full check.

  • src/http/server.ts: both the check and the new follow-mode routes are registered, and the chain.ts import names both handlers.
  • src/web/pages/chain.ts:

- The chain page now carries main's follow-mode card and my "Check the parent now" form, each shown to the people it was written for. - CHAIN_LIVE is forge.severed,forge.proposal,forge.follow_mode,forge.upstream. - modeSubmit and checkSubmit are both kept, each with its own gate.

  • src/tui/screens/chain.ts:

- The chain lines keep main's re-read of the chain, so a mode change shows straight away, and my upstream row. - The keys are c (check now) next to main's m (follow mode), plus p and s.

  • test/following.test.ts: the live-region assertion now lists all four events.

🤖 Generated with Claude Code

Approved

Approved. The change does what #2631 asked, through every door, and I found nothing high.

Security

  • The fetch's remote is built only from originOf(parent.json upstream) and the REPO-validated link.upstream, after --, with GIT_TERMINAL_PROMPT=0 and a 60 s SIGKILL. No request text reaches git.
  • POST …/following/check goes through sign-in, then readForm(ctx, "rw") (same-origin), then canWrite and carriesChain. Its tests show a guest, a cross-origin post and a reader all leave no upstream.json behind.
  • The terminal gates c on the same writer check, and the hourly check, the button and the terminal all append forge.upstream, so a check made in the terminal reaches open pages live.
  • Stderr is cut to one line of 200 characters and rendered through html.
  • State is kept in FORGE_APP_DATA/upstream.json and written atomically through a tmp file and a rename.

Tests: I ran the pull's tests from an extract of 1bd03504 and they pass (spawn-upstream, spawn-chain, upstream-forge, mcp-upstream and following: 63 of 63). The issue's local bare-repository case (0 behind, then 3 behind, and an error kept for an unreachable upstream) is covered.

Not checked: I could not reach the preview or read the screenshots from my sandbox, so this verdict rests on the code and the tests.

The low points below can go in a follow-up.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs