Public: anyone can read this; the crew can change it.
`/fleet` applies one verb to the rows it selects, with a preview first and Undo after #10147
Check · Review · Land · Live done — Landed and live in production.
`/fleet` now applies one verb to the rows it selects, with a preview first and Undo afterwards. No JavaScript is needed.
shipped production is serving it deploy 9e8c29c
/fleet now applies one verb to the rows it selects, with a preview first and Undo afterwards. No JavaScript is needed.
What changed
- Form: under the table, a signed-in person gets a verb picker (the verbs for the kind) and a Value field. A signed-out visitor and an agent are not offered it, and a POST from either is refused (signed-out gets the sign-in page, an agent gets not-found).
- Preview: a same-origin
POST /fleetwithstep=previewcallspreviewFleetOp. It shows each row's before → after, and whether it will change, is unchanged or is refused. Refusals and verb errors appear as muted lines. Nothing is written. - Apply:
step=applycallsapplyFleetOpand redirects toGET /fleet/ops/:id, a result page with the same table. - Undo:
POST /fleet/ops/:id/undocallsundoFleetOp. The Undo button shows only while the operation is reversible, not yet undone, and withinUNDO_WINDOW_MS. - Same-origin check: every POST goes through
readForm, so a cross-origin one gets 403. - Code: the handlers are in
src/web/pages/fleet-ops.tsand the HTML is insrc/web/fleet-act.ts.fleetPageinsrc/web/pages/fleet.tsnow delegates torenderFleet, which draws the preview or receipt under the form. The routes are insrc/http/server.ts. No new dependency and no new component class. - Tests:
test/fleet-act.test.tscovers the reader seeing no form, the signed-out and cross-origin refusals, a label preview over two issues that writes nothing, apply, undo, and a cross-origin undo. - Docs:
docs/features/fleet.mddescribes the new flow.
Checks
bun x tsc --noEmit,bun scripts/gate.tsandbun scripts/design-lint.tspass.- I ran 634 tests (the fleet, route, origin, docs and no-JS suites) and all pass.
scripts/fast.tsnamed 1549 files because the change touchesserver.ts, so I did not run them all; the forge's check runs the full suite. - One test run printed a denied network call to
parent.test:443; all tests still passed. - I could not call
keep_plan, so there is no plan on the issue.
Left out
- The Value field is free text and is read as a flag, a number, or text, whichever the verb accepts. There is no per-verb typed input.
- The Undo button disappears after a day for every verb. The API still undoes non-asks verbs later.
- There is no operations history list on the web.
- No terminal change.
Try it: sign in, open /fleet?q=issue repo:you/repo state:open, choose label, enter a label name, then Preview, Apply and Undo.
🤖 Generated with Claude Code
The check failed on two gate rules, and I've pushed a fix for both. The rules need Why now: and Try: lines in a commit message, and my first commit had neither. The new commit is empty and adds only those two lines. No code changed, so the tests are unaffected.
bun scripts/gate.ts now passes locally. I haven't seen the forge's check run on the new push yet.
Try on the preview: sign in, open /fleet?q=issue+state:open, pick label, enter a label name, then Preview, Apply and Undo.
🤖 Generated with Claude Code
The failing test was test/security-floor-routes.test.ts. It wants every route the seed can't fill to be named, and the new GET /fleet/ops/:id receipt route wasn't, because it needs an operation id the seed never creates. I added it to the UNSEEDED list in that test, reusing the existing FLEET reason, and pushed.
That test file now passes locally (3 pass, 0 fail). I haven't seen the forge's full check run on the new push yet.
Try on the preview: sign in, open /fleet?q=issue+state:open, apply a verb, and open the result page.
🤖 Generated with Claude Code
Web flow works and is tested, but it misses the 'both doors' rule and has two correctness gaps.
Fix first: add the terminal screen. The PR says 'No terminal change'; what a person can do on /fleet (preview, apply, undo a verb) must also be doable in src/tui/screens/fleet.ts (verb + value prompt, preview, apply, undo), gated like the page (guest/agent get no write), with its entry where AGENTS.md says.
Also:
- Apply is not bound to what was previewed. The apply form only carries q/verb/value, and applyFleetOp re-selects rows at apply time, so rows that changed between preview and Apply are written without having been shown. Carry a digest of the previewed row ids (or the id list, validated server-side) and refuse with a muted line if the selection differs.
- Add tests: an agent/ non-acting user POST is refused (canAct branch), a refused row in apply, and the terminal screen's write path.
- Design: the preview/receipt tables put three columns where principle 7 wants rows; the result line 'N changed, N unchanged, ...' repeats what the table says (principle 1). Use a single muted sentence and drop the duplicate count in the receipt.
- docs/features/fleet.md lists 'Tested in ... bun test pass' as a claim; keep the how-to and add the terminal section once the screen exists.
/fleet can now preview, apply and undo a verb from the terminal as well as the web, and Apply is tied to the rows that were previewed. bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts pass, and the fleet tests pass, 40 in all. I did not run the full suite: scripts/fast.ts named 1579 of 2350 files, so I ran the fleet tests plus test/security-floor-routes.test.ts instead.
What changed
- Apply is bound to the preview.
- The preview carries a digest of each row's id, outcome and new value (src/fleet/digest.ts).
- The new applyPreviewed in src/actions/fleet.ts plans again and writes nothing if the digest differs. The page then shows a muted line asking for a new preview.
- A missing digest is refused the same way. A preview with nothing to change has no Apply button.
- Terminal screen.
- In src/tui/screens/fleet.ts, v asks for a verb and value (label urgent) and shows each row's change in a dialog. Apply then writes through applyPreviewed with the digest of what the dialog showed.
- u undoes the last result. The keys exist only for a signed-in person who is not an agent, so a guest or agent gets no write path.
- The palette entry "Fleet rows" already sits where AGENTS.md says, so it needed no change. Both doors now read the typed value through src/fleet/reading.ts, which I moved out of the web page.
- Design.
- The preview and receipt are ul.list rows (label, change, outcome) instead of a three-column table.
- The preview says one muted sentence, "Nothing is written until you apply." The count appears only on the Apply button.
- The receipt no longer repeats the counts.
- Docs.
docs/features/fleet.mddrops the "Tested in … pass" claim, describes the digest, and gains the terminal how-to.
Tests added
test/fleet-act.test.ts:
- An agent's POST is refused with a 404, and the form is not shown to it. - A row the actor does not administer shows as refused in the receipt and stays unlabelled, while their own row is labelled. - Apply after a new issue appears is refused and nothing is written, as is Apply with no digest. A fresh preview then applies.
test/tui-fleet.test.ts:
- Preview, Apply and u undo work, and the labels change and revert.
- A guest is not offered v.
Left out
- The terminal has no key to refresh the preview after a stale refusal. You press
vagain. - The terminal dialog shows at most 12 rows, then "and N more".
All four earlier asks are met. Apply is bound to the preview by a digest and refuses if the rows moved, with a test. The preview says its one sentence once. The receipt no longer repeats counts. The terminal has v and u, and docs/features/fleet.md has the how-to. Writes check who, POSTs are same-origin, and agents and guests get no form or write key. Non-blocking: the terminal has no key to refresh a stale preview.
/fleetnow applies one verb to the rows it selects, with a preview first and Undo afterwards. No JavaScript is needed.What changed
POST /fleetwithstep=previewcallspreviewFleetOp. It shows each row's before → after, and whether it will change, is unchanged or is refused. Refusals and verb errors appear as muted lines. Nothing is written.step=applycallsapplyFleetOpand redirects toGET /fleet/ops/:id, a result page with the same table.POST /fleet/ops/:id/undocallsundoFleetOp. The Undo button shows only while the operation is reversible, not yet undone, and withinUNDO_WINDOW_MS.readForm, so a cross-origin one gets 403.src/web/pages/fleet-ops.tsand the HTML is insrc/web/fleet-act.ts.fleetPageinsrc/web/pages/fleet.tsnow delegates torenderFleet, which draws the preview or receipt under the form. The routes are insrc/http/server.ts. No new dependency and no new component class.test/fleet-act.test.tscovers the reader seeing no form, the signed-out and cross-origin refusals, a label preview over two issues that writes nothing, apply, undo, and a cross-origin undo.docs/features/fleet.mddescribes the new flow.Checks
bun x tsc --noEmit,bun scripts/gate.tsandbun scripts/design-lint.tspass.scripts/fast.tsnamed 1549 files because the change touchesserver.ts, so I did not run them all; the forge's check runs the full suite.parent.test:443; all tests still passed.keep_plan, so there is no plan on the issue.Left out
Try it: sign in, open
/fleet?q=issue repo:you/repo state:open, chooselabel, enter a label name, then Preview, Apply and Undo.🤖 Generated with Claude Code
Closes #10133