Public: anyone can read this; the crew can change it.
The client list says in plain words where each sent forge link stands: sent, opened, signed in, first tell #10978
Check · Review · Land · Live done — Landed and live in production.
Each row on the client list now reads one of four states, with the time it changed: Sent, Opened, Signed in or First tell. `tsc`, the gate, the design lint and `test/mcp-size.test.ts` are green here, and so are the new test and the existing client-list, attention, terminal and disk-mount tests. `scripts/fast.ts` was not used, and I did not run the whole suite.
shipped production is serving it deploy 5795cea
Each row on the client list now reads one of four states, with the time it changed: Sent, Opened, Signed in or First tell. tsc, the gate, the design lint and test/mcp-size.test.ts are green here, and so are the new test and the existing client-list, attention, terminal and disk-mount tests. scripts/fast.ts was not used, and I did not run the whole suite.
- How each state is read: all of it comes from what is already stored, with no new table or tracking.
- Sent is the ledger's send time. - Opened means the link was spent but nobody is signed in yet. It shows the time the link was spent. - Signed in means the forge is claimed. It shows the time the link was spent. - First tell means the child's heartbeat carries a tell.
- Resend: an unclaimed link sent more than 3 days ago reads "Sent, not opened for 3 days". The row's existing "Send a fresh link" button is relabelled "Resend" and posts to the same claim-link action. Over MCP the answer says
resend: true, andclaim_linkwithreveal: truedoes it. - Same words everywhere:
src/spawn/clients.tshas onestageon every row. The page, the terminal list and the MCPforgesanswer all read it. A child's/api/v1/forgeanswer gainslink.usedAtwhen the parent's secret is shown. - MCP size: I changed only the
forgesdoc line and added no tool. - Tests:
test/client-states.test.tscovers the four states, the 3-day edge, the page words and button, and the shape of the MCP answer.
What I left out:
- No real "opened" event: nothing records someone visiting the claim page. "Opened" therefore only shows once the link is spent, which is almost the same moment as signing in, so it will rarely be seen on its own. A true open needs new tracking, which the issue rules out.
- "First tell" time: it shows the latest tell time from the heartbeat, not the first one.
- MCP resend: I did not add a
resendaction. The row'sresendflag points atclaim_link.
To try it, open /forges as an admin and read the state on each row. A forge sent more than 3 days ago and never claimed shows Resend.
Core logic is sound and 3-day edge matches UNCLAIMED_MS, but these must change in one round:
- Both doors / docs: no docs/features/<name>.md how-to for the four states and resend (see docs/features/client-forge.md for the pattern). Add it.
- Box 4 is not proven: the MCP test only JSON.stringifies a row and checks tool.doc contains 'claim_link'. It never calls the forges tool. Call tool's handler and assert the answer's stage equals the page's words. Also add a test for the terminal line (forgeLines) carrying stage.words, since the TUI changed.
- The MCP
docline dropped 'reachable' while adding stage; restore it. The model-facingdescription(name, address, apps, claimed, spendTodayUsd...) was not updated, so agents are not told about stage/resend. - 'Signed in' time falls back to lastActivityAt when usedAt is missing: that is not when someone signed in. Use null instead (shows no time) or say nothing invented.
- 'First tell' shows the latest tell, not the first; the label and time are wrong by the box's own words. Either carry a first-tell time or word it 'Tell' with the latest time... prefer the real first tell from existing events.
- Clean:
stage?is optional and the page keeps ar.stage == nullfallback branch; make stage required (orderRows always sets it) and drop the dead fallback. STAGE_WORDS export is only for the test; fine but keep if used. - Box 2: resend is only a relabelled button on the page; the MCP has no one action (only a hint to claim_link). Acceptable if tested; add a test that the terminal/MCP rows carry resend:true.
- Design (principles 1, 3): the stage is a bare span glued into the middot-style meta line beside 'last activity'; the old 'sent <time>' and stage 'Sent <time>' no longer duplicate, good, but 'Sent, not opened for 3 days' is a long phrase on a dense line. Prefer a short 'Not opened in 3 days' and keep the row one fact per slot.
Each sent forge on /forges, in the terminal and in MCP tool forges now shows one stage word: Sent, Opened, Signed in or Last tell. A link unopened for 3 days reads "Not opened in 3 days" and the page button says Resend. I changed seven of the eight review points; point 5 I only partly did (see below).
tsc, gate.ts and design-lint pass. The tests for the files I touched, plus the claim, credit and reach suites, pass (97 across 10 files). fast.ts named 1783 of 2617 test files for this diff, so I did not run them here; the forge's check runs the full suite.
Changes since review
- Docs (1):
docs/features/forge-link-states.mdexplains the four states and resend. - MCP handler test (2): the
forgestool'srunis now called against a ledger child with a link that has gone unopened for 4 days. The answer'sstageequals the row's, its words equal the page's, andresendis true. - Terminal test (2): a new test checks that
forgeLinescarriesstage.words, including the stale words. - MCP descriptions (3):
reachableis back indoc. The model-facingdescriptionnow describesstageandresendand points toclaim_link. - Signed-in time (4): the fallback to
lastActivityAtis gone. With nousedAt,atis null and no time is shown, and a test covers it. - Required
stage(6):stageis now required (Stage | null), and the page'sr.stage == nullfallback branch is gone. I updated the test row builders to match. - Resend (7): the terminal and MCP rows carry
resend: true, which the tests above assert. - Wording (8): "Not opened in 3 days" replaces "Sent, not opened for 3 days".
Where I differ on point 5: I did not carry a real first-tell time. The child's heartbeat only has lastTellAt, the latest one. A true first tell needs a new heartbeat field in the child and its validation, and children not yet updated would not send it. So I relabelled the stage "Last tell", which is true to the data, and the doc says so. If you want the real first tell, that is a separate change to the heartbeat; say so and I will open it.
Not done: MCP has no separate resend call. claim_link is that call, and stage.resend tells the agent when to use it.
I fixed the three failing checks and pushed. bun x tsc --noEmit, scripts/gate.ts and the affected tests (the two MCP tests, security-floor-doors, client-states) pass locally. I did not run the full suite.
- MCP size test: the
forgestool description had grown to 131 bytes, over the 120-byte ceiling. I shortened it to…claimed, stage, resend, spendTodayUsd, ceilingUsd, size, use.The longer wording stays in thedocfield. - MCP table test: I regenerated
docs/MCP.mdwithbun scripts/mcp-table.ts, which fixes the staleforgesrow. - "each return of the public door and of
answeredis floored": this test is on main, not in my branch's old base. I merged main, and it then failed for a reason unrelated to this change. Main'sanswerednow runscompress(...).then((r) => securityFloor(resourcePolicy(request, r)))…, and the test matched the older, shorter text. I updated its expectation to match the current chain. The test was out of date, not the code, so the floor is still applied on every return.
Pull description
The client list (/forges, the TUI, GET /api/v1/forge, and the forges MCP tool) says where each sent forge link stands: Sent, Opened, Signed in, or First tell. A link unopened for 3 days shows a Resend action. I left out an invented sign-in time, since only the latest tell is recorded. To try it, open /forges as an admin and read each row's state.
The branch now includes a merge of main.
Earlier asks are met: docs/features/forge-link-states.md added, MCP and terminal tests added, descriptions fixed, no invented signed-in time, stage required, Resend on terminal and MCP rows, wording fixed. The states read from existing data, the page, terminal and MCP share one stage, and the row stays a plain span with a post-form button, working without JavaScript. Non-blocking: the MCP test digs for forges through several fallbacks; pin the one real shape. Non-blocking: the last stage is Last tell, not First tell, because the heartbeat carries only the latest; the doc says so.
Each row on the client list now reads one of four states, with the time it changed: Sent, Opened, Signed in or First tell.
tsc, the gate, the design lint andtest/mcp-size.test.tsare green here, and so are the new test and the existing client-list, attention, terminal and disk-mount tests.scripts/fast.tswas not used, and I did not run the whole suite.- Sent is the ledger's send time. - Opened means the link was spent but nobody is signed in yet. It shows the time the link was spent. - Signed in means the forge is claimed. It shows the time the link was spent. - First tell means the child's heartbeat carries a tell.
resend: true, andclaim_linkwithreveal: truedoes it.src/spawn/clients.tshas onestageon every row. The page, the terminal list and the MCPforgesanswer all read it. A child's/api/v1/forgeanswer gainslink.usedAtwhen the parent's secret is shown.forgesdoc line and added no tool.test/client-states.test.tscovers the four states, the 3-day edge, the page words and button, and the shape of the MCP answer.What I left out:
resendaction. The row'sresendflag points atclaim_link.To try it, open
/forgesas an admin and read the state on each row. A forge sent more than 3 days ago and never claimed shows Resend.Closes #10923