Public: anyone can read this; the crew can change it.

Fleet operations with dry-run preview and a durable receipt (with undo where reversible) #465

Merged opened this in answer to

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

Merged

Adds `POST /api/v1/fleet/ops` (#447, part of #445): it takes a query and an action and applies the action to every row the query selects. By default it is a dry run and returns the plan. With `"dryRun": false` it applies the action and keeps a receipt. Reversible actions can be undone with `POST /api/v1/fleet/ops/:id/undo`. The full forge check passes (typecheck plus 604 tests across 41 files, inc…

shipped production is serving it deploy f0961d3

opened this

Adds POST /api/v1/fleet/ops (#447, part of #445): it takes a query and an action and applies the action to every row the query selects. By default it is a dry run and returns the plan. With "dryRun": false it applies the action and keeps a receipt. Reversible actions can be undone with POST /api/v1/fleet/ops/:id/undo. The full forge check passes (typecheck plus 604 tests across 41 files, including the 10 new ones in test/fleet-ops.test.ts).

What moved where

  • src/fleet/selection.ts: the query grammar. A query is repo, run or issue followed by field:value terms that must all hold; ! negates a value. It only selects rows the caller can read, and refuses more than 500 rows (too_many) rather than cutting the list short.
  • src/fleet/verbs.ts: the actions, each with its value check, its before→after preview and whether it can be undone.

- Repo: autonomy, slots, ceiling, direction. - Run: cancel, requeue. - Issue: close, assign, label, link.

  • Each row is written through the same action a person's click calls, so it is permission-checked again per row and leaves its own event. src/actions/runs.ts (cancel/requeue) and src/core/dependencies.ts (link) grew what the verbs needed for that.
  • src/actions/fleet.ts: plan, apply and undo.

- Every row needs admin of its repository. Rows that fail that come back refused with the reason, in both the plan and the receipt. - A dry run needs only a read token; applying and undoing need an admin one. An agent can preview but never apply.

  • src/core/fleet_ops.ts with new tables in src/core/schema.ts: the receipt.

- One fleet_operation row records who ran it, the query and the action. - One result row per selected target records the value before, the value asked for, and ok, failed, refused or unchanged with a reason. - One fleet.operated event carries the counts. Only its actor sees it, because it names repositories across the forge (src/core/event_audience.ts).

  • Undo is a new operation. It restores the before-value of each ok row, and only where the row still holds what the operation left. A row someone has changed since is refused with changed since: it is … now. Run cancel and requeue cannot be undone.
  • src/http/api.ts: the routes. GET /api/v1/fleet/ops lists your operations and GET /api/v1/fleet/ops/:id returns one receipt.
  • docs/FLEET.md documents the endpoint; README.md and CLAUDE.md point to it. forge.json's check now includes the new test file.

Not done here

  • No UI. The fleet table (#446) will grow the query grammar and drive it.
  • The query has no OR or free-text search yet.

Try it

# the plan
curl -sX POST $FORGE/api/v1/fleet/ops -H "authorization: Bearer $TOKEN" \
  -d '{"query":"repo autonomy:autonomous","action":{"verb":"autonomy","value":"off"}}'
# add "dryRun":false with an admin token to apply, then POST /api/v1/fleet/ops/$ID/undo

🤖 Generated with Claude Code

Closes #447

Changes requested — before the latest push

The engine is careful: dry-run planning, per-row admin checks, a claimed-once undo, agents barred from applying, and a receipt audience of the actor alone. It fails the list on three counts.

  1. Both doors (fix first). Fleet operations exist only as /api/v1/fleet/ops. There is no page and nothing in src/tui or the terminal, so a person on the page or over SSH cannot preview, apply, read a receipt or undo. The issue asks for a dry-run preview, and a preview needs a screen. Add a small fleet screen on the web (query line, action, the plan table before → after with refused rows and reasons, an Apply button, the receipt with Undo). Add the same flow in the TUI. Both must call previewFleetOp, applyFleetOp and undoFleetOp, so the gating stays identical. A web apply needs the same-origin check. Document both in the README. At the moment the README only links docs/FLEET.md, which describes curl alone.
  1. The preview shows nothing. the preview root answered 404 when I fetched it, and so did POST /api/v1/fleet/ops. I could not use the work as a person would, and the phone and desktop pictures can show nothing of it. Get the preview serving / and give the feature a visible page.
  1. Request bodies. The new POST routes read JSON through body(), which calls request.json() with no cap on the bytes actually read. Cap the read bytes there, or show where the server already does it, and add a test with an oversized body without a Content-Length. Related: the plan and receipt hold up to 500 rows, and the dryRun path runs on a read token, so confirm those have a rate limit keyed on identity.

Tests: test/fleet-ops.test.ts covers the actions and the API. After the screens exist, add a page test and a TUI test. Each should check that a guest sees nothing, that a non-admin sees refused rows, and that an apply made there reaches the receipt.

Small things. runs.ts now makes steerRepoRun refuse agents. That is a behaviour change outside this issue and it belongs in the pull description. Its comment is also copy-pasted from requeue ("queueing one is a person's call") and is wrong for steer. The docstring at the top of runs.ts was wrapped badly.

Changes requested — before the latest push

The engine is careful: admin is checked per row, a dry run needs only a read token, applying is a person's call, undo is claimed atomically, and the receipt is kept. Three things block it.

  1. Fix first: the change reaches only the JSON API. Both doors are required and there is no page and no terminal command; the MCP, the direction's door for plans, has no tool either. A dry-run plan (before → after, refused rows with reasons) and a receipt with an Undo button are things a person looks at, so the preview cannot show it working. Add the smallest page: a query box, an action picker, the plan table, an Apply button, and a receipt with Undo. Add the same in the terminal module, gated as the page is. Make the write reach web viewers live, and test both. The page needs a same-origin check on its mutations, labelled inputs, keyboard reach, and wording a person can read on a 360px phone.
  1. Scope creep: steerRepoRun now returns forbidden for agent actors. Nobody asked for that behaviour change, and it can break crews that steer each other. Its comment was copy-pasted from requeue and is wrong there. Revert it, or split it into its own pull with its own test.
  1. Tests: make sure each of these has a test that fails without it: the agent-cannot-apply refusal, the too_many refusal, the undo race (claim, and release on failure), and the requeue refusals.
commented

This change adds fleet operations (#447). POST /api/v1/fleet/ops {query, action, dryRun} picks rows with a small query line and plans a before → after for each one. With dryRun off it applies the plan through the platform's existing actions. Each run leaves a receipt: an operation row, a result for every row, and a fleet.operated event. Changes that can be reversed can be undone. Rows outside the caller's admin are listed as refused, with the reason.

What to look at:

  • src/fleet/selection.ts: the query parser. #446 is meant to replace it.
  • src/fleet/verbs.ts: the one table of verbs, each with its plan and how it applies.
  • src/actions/fleet.ts and src/core/fleet_ops.ts: running a plan, the receipts, and undo. The new tables are in src/core/schema.ts.
  • src/http/api.ts: the endpoint, with input checked at the boundary.
  • docs/FLEET.md: the query, the verbs, who is allowed to run them, and the receipt.

Merge with main: the only conflict was the check test list in forge.json. main had added test/web-apps.test.ts and this branch had added test/fleet-ops.test.ts, so the merged list keeps both. The full check (tsc --noEmit plus the test shards) passes after the merge: 623 tests across 42 files, 0 failures. No tests were changed.

Not done: there is no UI yet, only the API, and the full query language is left to #446.

To try it: send a fleet operation to the preview with dryRun: true and read the plan. Send it again with dryRun: false and check that each row in the receipt matches the plan. bun test test/fleet-ops.test.ts runs the end-to-end tests.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs