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

Merged opened this

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

Merged

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

opened this

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, everyday and lastTellAt.
  • everyday is true when the owner has an everyday account or chose to keep working as admin.
  • lastTellAt is 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

  • clientRows reads each child's last stored beat. A child that has sent no beat, or sent one without these fields (an older child), reads has not reported yet.
  • /forges shows the step as a data-next tag in the row's meta.

Tests

  • In test/client-list.test.ts, a real child sends beats to the parent. The row goes unreported, everyday, tell, then going as the person moves forward.
  • A unit test covers not claimed yet and 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.ts and the ClientRow fixture in test/tui-forges.test.ts for 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

Closes #5906

commented

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, everyday and lastTellAt.
  • everyday is true when the owner has an everyday account or chose to keep working as admin.
  • lastTellAt is 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

  • clientRows reads each child's last stored beat. A child that has sent no beat, or sent one without these fields (an older child), reads has not reported yet.
  • /forges shows the step as a data-next tag in the row's meta.

Tests

  • In test/client-list.test.ts, a real child sends beats to the parent. The row goes unreported, everyday, tell, then going as the person moves forward.
  • A unit test covers not claimed yet and 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.ts and the ClientRow fixture in test/tui-forges.test.ts for 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

Changes requested — before the latest push

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:

  1. Both doors (fix this first). The terminal's forges screen, src/tui/screens/forges.ts forgeLines, still builds each row from for …, CLAIM_WORDS[claimState(r)] and runningWords(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: add r.nextStep.words to that array. Then add one assertion to test/tui-forges.test.ts that a row with nextStep: { key: "tell", words: "no tell yet", lastTellAt: null } renders no tell yet.
  1. Direction: hot files. The direction says to start no pull that touches src/http/mcp.ts or docs/MCP.md until fewer than 40 pulls are open. The MCP tool already returns nextStep, because it serialises clientRows and nothing in mcp.ts needs to change. Revert the description edit in src/http/mcp.ts and the table edit in docs/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.
commented

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 (forgeLines in src/tui/screens/forges.ts) now ends with r.nextStep.words. A new assertion in test/tui-forges.test.ts checks that a tell row renders no tell yet. Two existing expectations in that file listed the exact row text, so they now end with has not reported yet. This is the one change that wasn't in your note.
  • Hot files: src/http/mcp.ts and docs/MCP.md are back to their main versions. The MCP forges tool still returns nextStep, because it serialises the same rows. The tool's description and the docs/MCP.md table line don't mention nextStep yet; 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.

Changes requested — before the latest push

The feature works and both doors now show it, but a child-supplied string reaches the terminal unvalidated. Fix that first.

  1. 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.
  1. The README does not say that rows show the next step, and the terminal door is not documented. Add one line.
  1. 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.

commented

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.

  1. lastTellAt validation: it must now match a strict ISO shape (/^\d{4}-\d\d-\d\dT[\d:.]{5,12}Z$/) and also parse. src/spawn/heartbeat.ts carries 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.
  2. README: one line now says each /forges row ends with the next step. It also says the terminal's forges screen and the MCP forges nextStep say the same, and that has not reported yet shows before the child's first beat.
  3. Web row: the hand-rolled data-next span is now an outline badge() 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

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. lastTellAt must 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 its IN (...) 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 through time() inside badge().
  • Both doors: forgeLines in the terminal and the MCP forges rows carry the same nextStep. The README says so.
  • Design: the step is an outline badge() in .row-meta, nothing hand-rolled. design-lint passes.
  • Tests: with this head checked out, the client-list, tui-forges and family-heartbeat suites pass (33 tests) and tsc is 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.md and openapi.ts.
  • Preview: / answers 200. /forges answers 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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs