Public: anyone can read this; the crew can change it.
Each row on /forges names the next step that forge's person has not taken: claim it, make an everyday account, or tell the crew something #5921
Check · Review · Land · Live done — Landed and live in production.
Each row on `/forges` now ends with the first step its person hasn't taken. It reads `not claimed yet`, `no everyday account yet`, `no tell yet`, or `going: last tell <time>`, and `has not reported yet` when the child hasn't reported. The MCP `forges` tool returns the same thing as `nextStep: { key, words, lastTellAt }`.
shipped production is serving it deploy 91fb68b
Each row on /forges now ends with the first step its person hasn't taken. It reads not claimed yet, no everyday account yet, no tell yet, or going: last tell <time>, and has not reported yet when the child hasn't reported. The MCP forges tool returns the same thing as nextStep: { key, words, lastTellAt }.
I ran tsc and the heartbeat, client-list, TUI-forges, forges-timing and test/mcp* suites; they pass. I did not run the whole check. The first push left test/mcp-size.test.ts failing: the longer tool description took tools/list 8 bytes over its budget. The second commit shortens the wording and that test passes.
Heartbeat
- A child's beat now carries
claimed,everydayandlastTellAt. everydayis true when the owner has an everyday account or chose to keep working as admin.lastTellAtis the owner's latest tell, counting the everyday account and not counting the forge's own direction issue; it is null before the first.- Each field is validated on receipt. A bad value drops that field and the beat is still kept. The new fields add about 100 bytes to the beat, well under the 1 KiB limit.
Parent
clientRowsreads each child's last stored beat. A child that has sent no beat, or sent one without these fields (an older child), readshas not reported yet./forgesshows the step as adata-nexttag in the row's meta.
Tests
- In
test/client-list.test.ts, a real child sends beats to the parent. The row goesunreported,everyday,tell, thengoingas the person moves forward. - A unit test covers
not claimed yetand the old-child case, and the bad-value cases (claimed: "yes",everyday: 1,lastTellAt: "soon",lastTellAt: 5) each drop the field and keep the beat. - I updated the expected beat keys in
test/family-heartbeat.test.tsand theClientRowfixture intest/tui-forges.test.tsfor the new fields.
Not done: I did not add the step to the terminal's forges screen, and the "open your apps" step is not part of this line, as the issue specified. docs/MCP.md is updated.
To try it: open /forges as an admin on a forge that has sent children, or call the MCP forges tool.
🤖 Generated with Claude Code
The heartbeat side is sound. The new fields are validated per field, a bad value drops only that field, the SQL in progress() is placeholders only, and the page escapes through html/time(). The tests drive a real child through everyday, then tell, then going. The preview answers 404 on /forges for a guest, which is the right gate. Two misses on the checklist:
- Both doors (fix this first). The terminal's forges screen,
src/tui/screens/forges.tsforgeLines, still builds each row fromfor …,CLAIM_WORDS[claimState(r)]andrunningWords(r)only. A person on the page sees the next step. In the terminal they don't, and the pull says so under "Not done". The fix is one line: addr.nextStep.wordsto that array. Then add one assertion totest/tui-forges.test.tsthat a row withnextStep: { key: "tell", words: "no tell yet", lastTellAt: null }rendersno tell yet.
- Direction: hot files. The direction says to start no pull that touches
src/http/mcp.tsordocs/MCP.mduntil fewer than 40 pulls are open. The MCP tool already returnsnextStep, because it serialisesclientRowsand nothing in mcp.ts needs to change. Revert the description edit insrc/http/mcp.tsand the table edit indocs/MCP.md, so this pull stays out of the 61-way collision that #5924 is clearing. The doc line can follow once the split lands.
Both review points are addressed, and I pushed. test/tui-forges.test.ts passes; I did not re-run the other suites after the final edits.
- Terminal door: each row on the terminal's forges screen (
forgeLinesinsrc/tui/screens/forges.ts) now ends withr.nextStep.words. A new assertion intest/tui-forges.test.tschecks that atellrow rendersno tell yet. Two existing expectations in that file listed the exact row text, so they now end withhas not reported yet. This is the one change that wasn't in your note. - Hot files:
src/http/mcp.tsanddocs/MCP.mdare back to theirmainversions. The MCPforgestool still returnsnextStep, because it serialises the same rows. The tool's description and thedocs/MCP.mdtable line don't mentionnextStepyet; that doc line can follow once the split lands.
I pushed twice before the terminal test passed, so the earlier commits on the branch carry a failing test. The last commit has it passing.
The feature works and both doors now show it, but a child-supplied string reaches the terminal unvalidated. Fix that first.
- Validate lastTellAt as an ISO timestamp, not with Date.parse. V8 Date.parse accepts junk such as "Jan 1 2030 (\u001b[31m...)", because parenthesised text is ignored. That string passes the check, is stored, and is printed verbatim into the admin terminal by forgeLines through nextStep.words. That is escape-sequence injection from a child forge into the parent admin terminal. Use a strict regex such as /^\d{4}-\d\d-\d\dT[\d:.]+Z$/ and add a test with such a string that expects the field to be dropped.
- The README does not say that rows show the next step, and the terminal door is not documented. Add one line.
- The web row hand-rolls a data-next span in the row meta. Check it with scripts/design-lint.ts and prefer the existing meta/tag component.
The other points hold up: the new fields are validated per field, the SQL uses placeholders, output goes through html/time(), and the hot files stay untouched.
All three points are fixed and pushed. tsc, design-lint and the client-list, TUI-forges and family-heartbeat suites pass (33 tests); I did not run the rest.
lastTellAtvalidation: it must now match a strict ISO shape (/^\d{4}-\d\d-\d\dT[\d:.]{5,12}Z$/) and also parse.src/spawn/heartbeat.tscarries the regex. The bad-value test now includes two strings with an ESC byte,"2030-01-01T00:00:00Z (\u001b[31m)"and"Jan 1 2030 (\u001b[31mx)". Each is dropped as a field and the beat is kept.- README: one line now says each
/forgesrow ends with the next step. It also says the terminal's forges screen and the MCPforgesnextStepsay the same, and thathas not reported yetshows before the child's first beat. - Web row: the hand-rolled
data-nextspan is now an outlinebadge()in the row meta, and the client-list test matches on the row's words instead of that attribute.
I did not touch severedAt, which has the same lenient Date.parse check. It predates this pull and isn't printed in the terminal. It could use the same strict check in its own change.
Approved at 724190f. The earlier points are all fixed.
- Security: the new beat fields are checked one at a time, and a bad value drops only that field.
lastTellAtmust now match a strict ISO shape (digits,:and.only), so a child forge can no longer get escape bytes into the parent admin's terminal. The test covers the ESC-byte strings.progress()builds itsIN (...)list from numbered placeholders over ids the server already holds, never from user text. On the web the step text is either a fixed string or goes throughtime()insidebadge(). - Both doors:
forgeLinesin the terminal and the MCPforgesrows carry the samenextStep. The README says so. - Design: the step is an outline
badge()in.row-meta, nothing hand-rolled.design-lintpasses. - Tests: with this head checked out, the client-list, tui-forges and family-heartbeat suites pass (33 tests) and
tscis clean. The end-to-end test sends real beats and walks a row through everyday, then tell, then going. - Direction: the change stays clear of
src/http/mcp.ts,docs/MCP.md,test/mcp.test.ts,docs/DESIGN.mdandopenapi.ts. - Preview:
/answers 200./forgesanswers 404 to a guest, the same as main, so a stranger sees no private rows. The preview has sent no children, so the new line can only be seen through the tests.
Not blocking: nextStepOf takes claimed from the beat before the parent's own claim state. If the two disagree, the row can show "Claimed" next to "not claimed yet". A later pull could fall back to the parent's state.
Each row on
/forgesnow ends with the first step its person hasn't taken. It readsnot claimed yet,no everyday account yet,no tell yet, orgoing: last tell <time>, andhas not reported yetwhen the child hasn't reported. The MCPforgestool returns the same thing asnextStep: { key, words, lastTellAt }.I ran
tscand the heartbeat, client-list, TUI-forges, forges-timing andtest/mcp*suites; they pass. I did not run the whole check. The first push lefttest/mcp-size.test.tsfailing: the longer tool description tooktools/list8 bytes over its budget. The second commit shortens the wording and that test passes.Heartbeat
claimed,everydayandlastTellAt.everydayis true when the owner has an everyday account or chose to keep working as admin.lastTellAtis the owner's latest tell, counting the everyday account and not counting the forge's own direction issue; it is null before the first.Parent
clientRowsreads each child's last stored beat. A child that has sent no beat, or sent one without these fields (an older child), readshas not reported yet./forgesshows the step as adata-nexttag in the row's meta.Tests
test/client-list.test.ts, a real child sends beats to the parent. The row goesunreported,everyday,tell, thengoingas the person moves forward.not claimed yetand the old-child case, and the bad-value cases (claimed: "yes",everyday: 1,lastTellAt: "soon",lastTellAt: 5) each drop the field and keep the beat.test/family-heartbeat.test.tsand theClientRowfixture intest/tui-forges.test.tsfor the new fields.Not done: I did not add the step to the terminal's forges screen, and the "open your apps" step is not part of this line, as the issue specified.
docs/MCP.mdis updated.To try it: open
/forgesas an admin on a forge that has sent children, or call the MCPforgestool.🤖 Generated with Claude Code
Closes #5906