Skip to content

feat(app): needs you can split into columns of your own, which turn check sorts agents into - #214

Draft
Stener1 wants to merge 4 commits into
cosscom:mainfrom
Stener1:needs-you-sub-states
Draft

Stener1 wants to merge 4 commits into
cosscom:mainfrom
Stener1:needs-you-sub-states

Conversation

@Stener1

@Stener1 Stener1 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Closes #213.

Summary

Needs you mixes asks that want an answer with asks that send you off to do something. This adds an optional set of columns under it, which turn check sorts agents into. Off by default: with no set, the board, the API and turn check's requests are unchanged.

agent_state doesn't change. A session in a sub-state is still waiting, so sends, waits, notifications and plugins behave as they did. An optional agent_sub_state says which one, and older clients ignore it.

 end of turn
   ask(status)                     # today's request, byte for byte, set or no set
   if needs_input or blocked, sure enough
+    if a set exists: ask(needs)   # a second request, on the same words, with the descriptions
     publish agent.waiting
+      sub_state, or sub_state_why

 question form (AskUserQuestion, request_user_input)
   publish agent.waiting
+  if a set exists and turn check is on: ask(needs) on the form's text
+    publish agent.substate        # never a second agent.waiting

 permission prompt
   publish agent.waiting
+    sub_state: the default, by rule, with no request
internal/box/substates.go         # the set, its routes, placing a form, the client
internal/box/turns.go             # the ledger's mark on a wait: survives a restart, clears on the next state
internal/boxcmd/substatescmd.go   # berthd / berth substates
internal/mcpserver/mcp.go         # berth_substates; berth_sessions says sub_state
app/src/lib/substates*.ts         # placing a card, newest wins, spreading to boxes
app/src/views/dashboard/          # one amber column each; a "Not sorted" tag
app/src/views/settings/substates-section.tsx   # preset, editor, warnings, who changed what
  • Set: id, name (40), description (300), parent (waiting only for now), at most 6. The first is the default. Stored per box in substates.json with a revision and a stamp.
  • Sync: the app spreads the newest edit to every box it can reach, gated on the substates capability in GET /v1/info. A box keeps only what is newer than it holds, so two laptops can't flip it. An older box shows as outdated and keeps today's behaviour.
  • Not sorted: anything that can't be placed sits in the first column with a small tag and never drops off the board: turn check off, no provider, an agent it doesn't read, a timeout, under 65%, a refusal, a leftover secret, an older box, or an ID the set no longer has.
  • Agents may edit the set over /v1/substates, berthd substates and MCP, with limits, a before:substates.change hook and a substates.changed event. /v1/turncheck stays closed to local callers, and only a paired laptop can spread a stamped set.
  • New data flow, to say plainly: with a set and turn check on, a question form's text and the descriptions go to the turn check provider, through the same redaction as a turn. The docs say so.

One decision that differs from #213: I proposed sharing turn check's request. It's two requests instead. An agent can write a description, and with a shared request the only thing between that text and the verdict is a sentence asking the model to ignore it. Now a description can't reach the verdict's request at all, and a provider that can't take the second question costs only the sort. A hostile description didn't move the verdict in the shared request when I tried it on 31 real turns, so this is a matter of guarantee, not of a bug seen. Accuracy was the same either way.

Evidence

Unchanged with no set. Before and after removing the set, the same four columns:

four columns

The preset (mock). Two amber columns, with counts. The card on an older box is tagged:

two columns
settings

Real boxes, a real Claude Code, the real provider (verify-berthd, scenario dashboard-sub-states-live):

- An agent on devq turns on the preset over MCP
- It spreads to devr through the app, with the same stamp
- A real turn that ends asking for a choice lands in Needs a decision
- A real Claude asks a form: it read as: decision → act
- 1 agent.waiting, 1 agent.substate
- The field reads the same through the API, the CLI and MCP
- Removing the set brings the four columns back, on both boxes

real form
settings after an agent's edit

Claude Code's terminal driver was also run by hand: a form reports as a permission wait with tool AskUserQuestion, then again about 6 seconds later as a notification, and it stays one wait.

Waits already open, and every card style (a second commit, after trying it on real waits). Agents that were already waiting when the set was turned on stayed Not sorted until their next wait, so a board turned on mid-day had every card in the first column. Now the box sorts them when a set is turned on or changed, or turn check is: one request at a time, a form by its questions and any other wait by the agent's last message. A wait too young to tell whether its own form placing is under way is looked at again once its window has passed. At start, the box sorts the waits it never tried, so a restart asks about each once. A wait recorded before the box kept sub-states is placed by the event that began it. The board also hands Not sorted to the card as a line of its own (children), so a card component that takes children shows it too.

Real box (dashboard-sub-states-open-waits):

- With no sub-states, a real Claude asks for a key and waits
- Turn on the preset in Settings
- The wait that was already open is sorted into Needs you to act
- 1 agent.waiting before it, 1 agent.substate (act)

open wait sorted

Tests: TestWaitsOpenBeforeASetAreSortedWhenItIsTurnedOn, TestTheStartPassAsksAboutAWaitOnce, TestAWaitTooYoungForThePassIsSortedLater, TestAnUnmarkedFormIsSortedByItsQuestions, TestAWaitFromBeforeSubStatesCanBePlaced. The new data flow: for a wait that isn't a form, the agent's last message goes to the provider, as it does at the end of every turn with turn check on; the docs say so.

Tests

  • TestTurnCheckReplays passes on every existing fixture without re-recording, and TestNoSubStatesLeavesTheClassifierRequestByteIdentical and TestDescriptionsNeverReachTheVerdictsRequest hold the request to today's bytes.
  • TestSubStateReplays: synthetic turns placed correctly on recorded gateway answers.
  • Late answers, one notification per form, restart, limits, the hook, a local caller refused on /v1/turncheck, older clients, no file: internal/box/substates_test.go.
  • app/src/lib/substates.test.ts, app/e2e/dashboard-sub-states.spec.ts, and the existing dashboard specs unchanged.
  • go vet, go test -race ./..., the app's checks and build, and the docs build pass, except TestTheCatalogIsCapped, which fails the same way on main on my machine because it counts the machine's real commands. cargo check wasn't run: no Rust changed.

Accuracy: on 51 real past asks from two machines, labelled by hand before any classifier call, the prompt was right on 47 (92%). The asks themselves aren't in this PR.

Not tested:

  • OpenAI Decisions and Laya, live. I had no key or server for them. The request's shape is unit tested for OpenAI, and if either rejects it the card goes to Not sorted, with the verdict untouched. Please say if you can try one.
  • The Mac app's window. The drives use the web build.

Merge Danger

Door: two-way

Removing every sub-state restores today's behaviour on the spot. substates.json and the sub mark in turns.json are additive, and an older berthd ignores both.

Blast Radius: small

With no set, the added work is a mutex-guarded read of an empty set on a waiting session, plus one read of a box's info. With a set, each form costs one classifier call, and each end-of-turn wait one more. Turning a set on, changing it, or turning turn check on costs one call per agent already waiting and not yet placed. The turn check ledger and check() are touched, so a bug there would show on turn check for everyone: that is what the byte-identical and replay tests guard.

🤖 Generated with Claude Code

…heck sorts agents into

An optional set of sub-states under Needs you. agent_state doesn't change: a
session in one is still waiting, and an optional agent_sub_state says which.
With no set, the board, the API and turn check's requests are as they were.

At the end of a turn, a waiting verdict is followed by a second request that
picks the sub-state, so the descriptions never reach the verdict's request. A
question form gets one request on its text, and its place arrives as
agent.substate, never a second agent.waiting. A permission prompt goes to the
default by rule. What can't be placed sits in the default, marked not sorted.

Each box keeps a stamped copy, and the app spreads the newest. The set can be
edited over /v1/substates, berthd substates and MCP, including by an agent on
the box, with limits, a before: hook and an event. /v1/turncheck stays closed
to local callers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 2, 2026

Copy link
Copy Markdown

@Stener1 is attempting to deploy a commit to the seanbrydonio's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Stener1

Stener1 commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

I can't add the run-ci label from this account, so CI needs a maintainer to add it. I ran AGENTS.md's checks locally; the PR says what passed and what didn't.

Stener1 and others added 3 commits November 2, 2026 10:17
… and every card style shows not sorted

Waits open before a set was turned on or changed, or before turn check
was, used to stay unsorted until their next wait. The box now sorts them
then, one request at a time: a form by its words, any other wait by the
agent's last message. At its start it sorts the waits it never tried, so
a restart asks about each once. A wait recorded before the box kept
sub-states is placed by the event that began it.

The board now hands the Not sorted tag to the card as a line of its own
(children), so a card style that takes children shows it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reset

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ough, and an unmarked form by its questions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: optional sub-states under Needs you, placed by turn check

1 participant