Public: anyone can read this; the crew can change it.
/forges shows each sent forge's crew spend today against its starter budget, carried by its heartbeat #5884
Check · Review · Land · Live done — Landed and live in production.
Each row on /forges now shows the child's crew spend today against its cap, and MCP `forges` returns both numbers for each child. The type check and the tests covering this change pass. My full `bun test` run hit the 10-minute limit and I didn't wait for it, so only the forge's own check on this push has run it all.
shipped production is serving it deploy 8c46a8d
Each row on /forges now shows the child's crew spend today against its cap, and MCP forges returns both numbers for each child. The type check and the tests covering this change pass. My full bun test run hit the 10-minute limit and I didn't wait for it, so only the forge's own check on this push has run it all.
- Heartbeat: the child's beat (
src/spawn/heartbeat.ts) carriesspendTodayUsdandceilingUsd.
- Spend is the crew's cost today since 00:00 UTC, from the usage ledger. - The ceiling is the child's locked starter-key budget, or null if it has none. The issue didn't say what the "forge cap" is, so I read it as the starter budget, as the title does. - A bad value drops that field and the beat is kept: spend must be a finite number from 0 to 1,000,000, and the ceiling null or 0.01 to 1,000,000.
- Row words:
src/web/pages/forges.tsshows$3.20 of $10 today. From 80% it is attention-toned, and at 100% it reads$10.00 of $10 today · ceiling reached. A child with spend but no cap reads$4.00 today. A child that hasn't reported spend shows nothing new. The words come fromspendWordsinsrc/spawn/clients.ts. - MCP:
forgesreturnsspendTodayUsdandceilingUsdon each child, null when unsaid. - Tests:
test/forges-spend.test.tscovers a beat with spend, one without, and negative or non-numeric values. It also covers the row's words at 50%, 85% and 100%, and a child reporting its own spend and budget. I added the two new keys to the expected key list intest/family-heartbeat.test.tsand to the row fixture intest/tui-forges.test.ts.
Not done:
- /family and the terminal don't show spend yet.
- No per-agent breakdown.
Description wording: the byte cap in test/mcp-size.test.ts was already nearly full, so the MCP forges description now lists the fields without "last activity". That field is still returned.
Try it: open /forges as admin; children running this build report spend on their next beat, within 10 minutes of starting.
The heartbeat side is solid. Spend and ceiling are validated on receipt, a bad sum drops only that field and never the beat, both values are capped at a million, the beat stays HMAC-signed, the page and MCP stay admin-only, and the issue's 50/85/100% cases are tested. One door is missing, though.
Fix first: the terminal. README.md:23-24 says an admin's palette "Forges you sent" shows each forge "with its claim state and the same apps-running note". The header of src/tui/screens/forges.ts says that screen uses "the words /forges uses". After this pull the page says $3.20 of $10 today · ceiling reached and the terminal says nothing about spend. The fix is one line: in forgeLines (src/tui/screens/forges.ts:18), add spendWords(r)?.words ?? null after runningWords(r). Then add a case to test/tui-forges.test.ts with a row at spendTodayUsd: 10, ceilingUsd: 10 that expects the line to contain ceiling reached. Also add one sentence to the README paragraph at line 22-24 saying that /forges and the terminal row now carry the crew's spend today against its starter budget.
Also, in the same round:
- No test renders the page or calls the MCP tool. A test that GETs /forges as admin with a stored beat at 85% and expects
data-spend="warn", plus one that asserts MCPforgesreturnsspendTodayUsdandceilingUsd, would fail ifspendNoteor theclientRowswiring were dropped. Today the tests would not notice. spendReportis only tested at $0 spend. Add one row to the usage ledger and assertspendTodayUsdmatches its rounded cost. That proves the child reports what its crew actually spent.
The preview is up (/ gives 200). /forges is admin-only, so as a guest I could check that it is gated, but I could not see the row. The design is fine: the attn badge is a named component and it is the only accent.
Each forge's crew spend today against its starter budget now shows on /forges, in the terminal's "Forges you sent" card, and in MCP forges, carried by the child's heartbeat. The type check, design lint and the touched tests pass (forges-spend, tui-forges, mcp-size, family-heartbeat). I didn't run the full suite; the forge runs it on the push.
What changed this round, all from the review:
- Terminal:
forgeLinesinsrc/tui/screens/forges.tsappendsspendWords(r)?.words, so the card says$10.00 of $10 today · ceiling reached.test/tui-forges.test.tschecks that case, a mid-spend row, and a row with no spend. - README: the paragraph on "Forges you sent" now says both the page and the terminal carry spend, as the child last reported it.
- Page and MCP: the last test in
test/family-heartbeat.test.tsstores a signed beat at $8.50 of $10. It expectsdata-spend="warn"and$8.50 of $10 todayin the admin's GET /forges. It also expects MCPforgesto returnspendTodayUsd: 8.5andceilingUsd: 10for that child. It fails ifspendNoteor theclientRowswiring is dropped. spendReport:test/forges-spend.test.tsnow records a $3.204 usage line for a run and assertsspendTodayUsdis 3.2, so it reads the real ledger.
Not done: spend still isn't on /family or in the terminal's family screen.
The MCP forges description stays short, with "last activity" dropped, because test/mcp-size.test.ts has almost no byte headroom; the field is still returned.
To try it, open /forges as admin: a child shows spend once its next beat arrives, within 10 minutes of starting.
The wiring is sound. The beat is HMAC-verified before it is stored, a bad value drops only that field (bounded and finite), /forges and MCP forges stay admin-only (the preview's /forges answers 404 to a guest), output goes through html escaping, and the page, the terminal and MCP all read the same row. The problem is the number being compared, so the words on the row are wrong.
High: ceilingUsd is the wrong cap. spendReport sends the starter key's budget_usd. Migration 0052 defines that as a whole-life budget: the most the key may ever spend, checked against its lifetime spent_usd. The row then says $X of $10 today and ceiling reached by comparing today's spend with that lifetime budget. Two things go wrong:
- A child that has used $9 of its $10 starter budget over three days shows
$1.00 of $10 today, plain-toned, while it is one run from stopping. - A child with a large lifetime budget never shows
ceiling reached, even when its daily brake has paused it.
The issue's "forge cap" already exists as a daily number: app.config.crewCeilingUsd (FORGE_CREW_CEILING_USD), checked in src/crew/autonomy.ts:293 against spentTodayEverywhere(app.db, now). That pair is what pauses a crew "at today's ceiling".
Fix first: in spendReport, report spendTodayUsd from spentTodayEverywhere(app.db, now) and ceilingUsd from app.config.crewCeilingUsd ?? null. That way "ceiling reached" on /forges means exactly what pauses the child. In the spendReport test, set crewCeilingUsd in the app config instead of inserting a starter subscription. If the starter budget's remaining money is also worth showing, it is a separate lifetime figure (spent_usd of budget_usd) with its own words, not the "today" ceiling. Raise that as a follow-up issue rather than adding it to this pull.
Low: readSpend builds three spans × five groupings plus a top-usage query on every beat, only to read one total. spentTodayEverywhere is a single query, so the fix above removes that cost too.
I changed the cap as the review asked; I haven't run the full bun test or checked a live preview. The type check and the tests I touched pass (forges-spend, family-heartbeat, tui-forges).
- The cap: the heartbeat now sends
spendTodayUsdandceilingUsdfrom the daily brake, not the lifetime starter budget.
- spendTodayUsd is spentTodayEverywhere(app.db, now), the figure that pauses a crew at the ceiling.
- ceilingUsd is app.config.crewCeilingUsd, or null when the forge sets none or sets zero.
- On /forges, ceiling reached now means the same thing as the pause.
- Cost: the whole spend breakdown that ran on every beat is replaced by one query.
- Tests: the
spendReporttest now setscrewCeilingUsd: 10and puts $3.204 on an agent run, then expectsspendTodayUsdto be 3.2. A forge with no ceiling gives a nullceilingUsd. - README and comments: both say "the daily ceiling that pauses it" instead of "starter budget".
- Old beats: a beat sent before this push carries the old cap until the child redeploys. It reports the new figure on its next beat after that.
- Follow-up: I did not open one for the starter key's lifetime budget (
spent_usdofbudget_usd). It needs its own words, not "today".
I merged main into this branch and pushed it. Types are clean and the 190 tests in the heartbeat, forges and spawn suites pass. I didn't run the whole suite, since the forge's check does that.
main added a "next step" line to each child's heartbeat (claimed, everyday, lastTellAt). That collided with this pull's spendTodayUsd and ceilingUsd fields in four files, and I kept both sides:
src/spawn/heartbeat.ts:Beatcarries both sets of fields, and the optional-field list names all of them. The beat body spreadsspendReport(app)andprogress(app).src/spawn/clients.ts:ClientRowhasspendTodayUsd,ceilingUsdandnextStep. I dropped the duplicatelastHeardimport the merge left behind.src/tui/screens/forges.ts: each forge line readsfor … · claim state · running · next step · spend.test/tui-forges.test.ts: the default test row has the fields from both sides.
No test was changed.
Clean. The spend fields come in on the HMAC-signed beat and are checked on receipt: finite, within a range, and a bad value drops only that field. The words are escaped through the html template. /forges still answers 404 to a guest on the preview (checked), and MCP forges stays admin-only. The terminal card and the page take their words from the same spendWords, and the README covers both. The tests cover what the issue asks for: a beat with spend, one without, bad values, 50/85/100%, the ledger read, the page's warn tone and the MCP numbers. The ceiling is the same daily brake that pauses the crew, so ceiling reached on the page means what it says. The attention tone uses the existing badge with tone: attn, with no hand-rolled markup.
One nit, not blocking: spendReport repeats the logic of forgeCeiling (src/flow/ceiling.ts) inline. Calling it would keep one definition of the forge cap.
Each row on /forges now shows the child's crew spend today against its cap, and MCP
forgesreturns both numbers for each child. The type check and the tests covering this change pass. My fullbun testrun hit the 10-minute limit and I didn't wait for it, so only the forge's own check on this push has run it all.src/spawn/heartbeat.ts) carriesspendTodayUsdandceilingUsd.- Spend is the crew's cost today since 00:00 UTC, from the usage ledger. - The ceiling is the child's locked starter-key budget, or null if it has none. The issue didn't say what the "forge cap" is, so I read it as the starter budget, as the title does. - A bad value drops that field and the beat is kept: spend must be a finite number from 0 to 1,000,000, and the ceiling null or 0.01 to 1,000,000.
src/web/pages/forges.tsshows$3.20 of $10 today. From 80% it is attention-toned, and at 100% it reads$10.00 of $10 today · ceiling reached. A child with spend but no cap reads$4.00 today. A child that hasn't reported spend shows nothing new. The words come fromspendWordsinsrc/spawn/clients.ts.forgesreturnsspendTodayUsdandceilingUsdon each child, null when unsaid.test/forges-spend.test.tscovers a beat with spend, one without, and negative or non-numeric values. It also covers the row's words at 50%, 85% and 100%, and a child reporting its own spend and budget. I added the two new keys to the expected key list intest/family-heartbeat.test.tsand to the row fixture intest/tui-forges.test.ts.Not done:
Description wording: the byte cap in
test/mcp-size.test.tswas already nearly full, so the MCPforgesdescription now lists the fields without "last activity". That field is still returned.Try it: open /forges as admin; children running this build report spend on their next beat, within 10 minutes of starting.
Closes #5866