Skip to content

feat(crews): answer a Crew thread's provider approvals and questions from the Inbox - #309

Merged
bryantderosier merged 10 commits into
j5/crew-requests-serverfrom
j5/crew-requests-web
Sep 28, 2026
Merged

bryantderosier merged 10 commits into
j5/crew-requests-serverfrom
j5/crew-requests-web

Conversation

@bryantderosier

@bryantderosier bryantderosier commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Important

⚠️ Merge order: merge these in this exact order

#309 is based on #308, so #308 merges first. Neither has migrations or ordering against the other Crews PRs.

  1. feat(crews): a Crew thread's provider approvals and questions reach the Inbox API #308: A Crew thread's provider approvals and questions reach the Inbox API
  2. feat(crews): answer a Crew thread's provider approvals and questions from the Inbox #309: Answer a Crew thread's provider approvals and questions from the Inbox ⬅️ this PR
  3. feat(crews): seats ask their Captain instead of the person #348: seats ask their Captain instead of the person (ship with feat(crews): answer a Crew thread's provider approvals and questions from the Inbox #309)

Scope now: seat approvals only (Jackson, 2026-09-26)

The sections below describe the original scope and have been narrowed; this summary is current.

  • Nothing is hidden in the thread. The ChatView.tsx change, its FORK.md case, useJ5CrewRoutedRequests/withoutInboxRequests, and the "waiting in your Inbox" note are gone. A seat approval shows both inline and in the Inbox, and a stale Inbox answer gets a 409.
  • Approvals only, from seats only. The question UI is removed.
  • The Inbox renders each approval with upstream's ComposerPendingApprovalPanel and ComposerPendingApprovalActions, so provider warnings and the "permission" label show.
  • Polling is gated. An environment is read only while one of its threads has hasPendingApprovals, so there are zero requests when nothing is pending.
  • No upstream file changes remain in this PR.

Problem

A Captain's or seat's provider approvals and questions render only inline in that thread's composer (ChatView feeds every pending request to ChatComposer), so I only see them if I open that thread, and the Crew stalls (#264). The Inbox and bell showed agent asks and Crew seat requests only.

What I changed

  • packages/client-runtime/src/j5/http.ts and state.ts: listCrewRuntimeRequests and respondCrewRuntimeRequest, plus a query atom family and a command, polled every 7.5 seconds like Crew gates.
  • apps/web/src/j5/state.ts: crewRuntimeRequestSourcesAtom, read on every connected environment.
  • apps/web/src/j5/crew/crewRuntimeRequests.logic.ts (new): mergeCrewRuntimeRequestSources, inboxRequestIdsForThread, withoutInboxRequests, and inboxBadgeCount.
  • apps/web/src/j5/crew/crewRuntimeRequestsClient.ts (new): the answer call, the forced refresh, the shared poll, and useJ5CrewRoutedRequests.
  • apps/web/src/j5/crew/CrewRuntimeRequestsSection.tsx (new): the Inbox's "Crew agent requests" section. Each item names its Crew and seat (or Captain) and links to the thread. Approvals show the provider's choices, or Approve and Decline by default; questions reuse upstream's pendingUserInput.ts helpers so answers have the composer's shape.
  • HumanInboxPage.tsx mounts the section and counts it; HumanInboxBell.tsx counts it through inboxBadgeCount.
  • apps/web/src/components/ChatView.tsx (upstream, FORK.md case 46): the derived memo is renamed providerPendingRequests and passed through useJ5CrewRoutedRequests, whose result keeps the name pendingRequests.
  • CrewRosterGate.tsx: shows "A request from this agent is waiting in your Inbox" with a link when the Inbox holds requests for the thread; the roster card is unchanged.
  • Docs: crews.md (the Definition's gate-placement rule, AC9, History) and the personas guide's Crews section, including that mobile still shows these prompts in the conversation.

Screenshots are from an isolated dev server on a copy of my data. The fixture Crew "Release Crew" has the Captain's thread holding a pending question and a builder seat holding a pending command approval. Every thread in the copy points at a provider instance that doesn't exist, so nothing could start a turn.

Seat thread, before (the approval inline) → after (the note):

Captain thread, before (the question inline) → after:

Inbox, before → after (bell: "2 open"):

Answering with no live provider session is refused, and the request stays listed:

The pass turned up two fixes, both in bcef3054c1: with no provider options, the Inbox offered only Approve and Decline while the composer offers four choices, so it now uses the composer's defaults; and the thread note sat on the composer's edge.

Why this shape

The thread hides exactly the request ids the Inbox currently lists for that thread on that environment. So a request the Inbox hasn't read yet, or can't read on a server without #263's route, stays inline. A prompt is never hidden from both places, and the thread doesn't need its own "is this a Crew thread" check. Putting the note in CrewRosterGate, which ChatView already mounts, keeps the upstream touch to one hook. The upstream approval and question panels are untouched.

Invariants

  • Non-Crew threads pass through unchanged (withoutInboxRequests returns the same object when no ids match).
  • The initial roster gate still renders inline; rosterGatesForThread is unchanged.
  • Ids are scoped by environment, so the same local thread id on two environments never hides another's prompt.
  • The bell counts asks, Crew seat requests, and Crew agent requests; an unread ask count still shows the Crew items.

Surfaces

Surface Decision
Entry points (chat, Settings, command palette, keybinding) Inbox page and bell; the thread composer's note. No new route or nav entry.
Clients (web, desktop, mobile) Web and desktop changed. Mobile keeps its inline cards (it has no J5 Inbox), which the personas guide states.
Providers Any provider whose requests reach the orchestrator; unaffected otherwise.
Contracts (packages/contracts) From #263; no further change.
Reverse states An answered or resolved request leaves the Inbox and the thread on the next read; the note disappears with it.
Connection modes (local, remote, tunnel) Read per connected environment; a server without the route fails its source alone and keeps its threads' inline panels.
Upstream files / FORK.md ChatView.tsx, recorded as FORK.md case 44.
Docs crews.md and docs/user/personas.md.

Out of scope

Upgrade and data

None.

Verification

  • crewRuntimeRequests.logic.test.ts covers:
    • a Crew thread's requests leaving the composer while unread ones stay inline;
    • a non-Crew thread keeping its panels;
    • environment scoping;
    • the bell count going up and back down.
  • vp test run apps/web/src/j5/crew apps/web/src/j5/a2a: 245 passed. ChatView.logic.test.ts: 125 passed.
  • Web typecheck clean. Lint clean apart from one existing set-state-in-effect warning in HumanInboxPage.tsx.
  • Seeded dev-server pass (screenshots above): Inbox items, bell count, notes on both threads, and inline panels gone on web; answering reached the orchestrator and was refused with 409 with no live session.
  • Not verified: a real provider turn resuming after an Inbox answer. The fixture had no live provider session by design.

Review focus

  • useJ5CrewRoutedRequests in ChatView: activeThreadId is passed even for drafts, which have no Inbox requests. Confirm nothing downstream depends on pendingRequests identity changing only with the projection.
  • CrewRuntimeRequestsSection handles multi-question requests all at once; check it against the composer panel's behavior.

Closes #264

Claude Opus 5.5 via Claude Code

🤖 Generated with Claude Code

@bryantderosier bryantderosier self-assigned this Sep 25, 2026
@bryantderosier
bryantderosier added this pull request to stack #310 September 25, 2026 01:00
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository: Jacksondr5/j5code/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: dcbd54d6-6a43-4500-9233-21b5c8492277


Comment @coderabbitai help to get the list of available commands.

@bryantderosier
bryantderosier marked this pull request as draft September 25, 2026 01:00
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 effective changed lines (test files excluded in mixed PRs). labels Sep 25, 2026
@bryantderosier
bryantderosier marked this pull request as ready for review September 25, 2026 01:15
bryantderosier and others added 2 commits September 24, 2026 21:25
…from the Inbox

A Captain's or seat's tool approvals and questions rendered inline in that
thread's composer, where the person only saw them by opening the thread, so
a Crew stalled unseen.

The Inbox gets a Crew agent requests section, read from #263's route on
every connected environment: each item names its Crew and seat (or the
Captain), links to the thread, and is answered in place with the provider's
approval choices or the question form. The bell counts them. On a live Crew
thread, one appended J5 hook in ChatView (FORK.md case 44) hands the
composer only the requests the Inbox does not hold, and CrewRosterGate shows
a note pointing to the Inbox; a request the Inbox has not read yet stays
inline, so nothing is hidden from both places. The roster gate still
renders inline, non-Crew threads are unchanged, and mobile keeps its inline
cards until it has an Inbox.

Closes #264

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

Found in the seeded dev-server pass: with no provider options the Inbox
offered only Approve and Decline while the composer offers Cancel, Decline,
Always allow this session, and Approve; the note sat on the composer's edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bryantderosier and others added 5 commits September 25, 2026 15:07
Conflicts in crews.md and personas.md: took the sync's handoff artifact wording and kept this branch's Inbox rule. FORK.md: this branch's case is renumbered 44 → 49, since the sync took 44 and 45 and the Captains stack takes 46–48; the inventory count line is left to the Captains stack so the two land without conflicting.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The upstream sync's Badge lint (shadcn/no-restyle) refuses spacing and typography classes on <Badge>; the Crew agent request row's seat badge now uses size="sm" like the other Crew badges.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rest inline

With #308's selector matching the composer, a request can be listed that the
Inbox cannot answer: a question answered by sending a message, or one that is
no longer resumable. The Inbox now offers answers only for live requests and
points to the thread for the rest, and the composer keeps those requests
inline instead of handing them to an Inbox that cannot answer them.

FORK.md case 46 records the shared selector and the client-runtime test that
holds it to derivePendingThreadRequests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread apps/web/src/j5/crew/crewRuntimeRequestsClient.ts Outdated
Comment thread apps/web/src/j5/crew/CrewRuntimeRequestsSection.tsx Outdated
@Jacksondr5

Copy link
Copy Markdown
Owner

[Review panel: Opus 5.5 + Astra] Live test evidence for #308 and #309, run by Astra against real provider turns on an isolated dev server. This fills the PR's "Not verified: a real provider turn resuming after an Inbox answer".

Scenario Result
A real Claude Captain's AskUserQuestion and a real seat's Bash approval pending at the same time Pass. The Inbox listed both and the bell showed 2.
Both answered from the Inbox UI Pass. Both POSTs returned 200. The Captain then printed CAPTAIN_RESUMED_BLUE and the seat printed SEAT_RESUMED_AFTER_APPROVAL, and the seat's harmless /tmp proof file had the expected content.
Answered inline in a second browser, then answered again from the stale Inbox card Pass. The second answer got a 409 ("already resolved") and the card left on the next poll. The event log shows one pending and one resolved runtime-request event, with only the inline command's id, so no second response was sent to the provider.
Approval on a thread that isn't in a Crew Pass. It stayed inline, the bell stayed at 0, the Inbox didn't show it, and answering it inline resumed the provider.
Crew retired while a Captain question was pending Pass. The next poll removed it from the Inbox, the "waiting in your Inbox" note went away, the question showed inline again, and answering it inline worked.
Poll traffic, 60s, no live Crews, Inbox closed 8 POSTs, 120 response-body bytes in total (15 each), request bodies 2 bytes each (headers excluded). With two pending requests, one response is 2,154 bytes, about 17.2 KB/min. The data had 171 threads and one small test Crew, so these numbers say nothing about server cost with large Crews.

Not tested: the "waiting in your Inbox" note on a seat thread (only the Captain's note was checked), the native desktop shell, mobile, and a disconnect in the middle of a flow. Bryant's before-images in the PR description cover the baseline.

Inbox with both requests:

Note on the Captain's thread:

Captain resumed after the Inbox answer:

Seat resumed after the Inbox approval:

Stale Inbox answer refused with 409:

Approval on a non-Crew thread stays inline:

After the Crew retired, the question is inline again:

@Jacksondr5

Copy link
Copy Markdown
Owner

Decision (Jackson, 2026-09-26): slim #308/#309 to seat approvals only. See the matching comment on #308 for why.

For #309 that means:

  • Don't hide anything in the thread. A seat thread isn't the one the person watches, so showing a prompt both inline and in the Inbox is harmless: answering in either place resolves it, and a stale Inbox answer gets a clean 409. Remove:
    • the upstream ChatView.tsx change and its FORK.md case;
    • useJ5CrewRoutedRequests / withoutInboxRequests;
    • the "waiting in your Inbox" note in CrewRosterGate.
  • Approvals only, from seats only. Drop the question UI in CrewRuntimeRequestsSection.
  • The panel's fixes that still apply:
    • reuse upstream's ComposerPendingApprovalPanel/Actions, so provider warnings and the "permission" label show;
    • poll only while a seat thread has a pending approval, instead of an always-on 7.5s poll.
  • The docs (crews.md, personas.md) describe the slimmed behavior.

bryantderosier and others added 3 commits September 28, 2026 08:56
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…wn approval UI

The Inbox now lists only Crew seat approvals, and nothing is hidden from
a thread: ChatView.tsx is back to upstream's, its FORK.md case and file
row are gone, and the roster gate no longer points to the Inbox.

CrewRuntimeRequestsSection renders each approval with upstream's
ComposerPendingApprovalPanel and ComposerPendingApprovalActions, so
provider option warnings and every request kind label match the
composer. The question UI and the copied approval choices are removed.

An environment is read only while one of its thread shells has
hasPendingApprovals: the source atom answers empty for the rest, the
7.5s poll runs only while some approval is pending, and a flag turning on
or off triggers an immediate re-read. crewApprovalPollPlan holds that
decision and is unit tested.

Docs describe seat approvals in the Inbox and the Captain's requests
staying in its thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bryantderosier

Copy link
Copy Markdown
Collaborator Author

@Jacksondr5 #309 is slimmed to match (746a720):

  • Nothing is hidden in the thread: the upstream ChatView.tsx change, its FORK.md case, the routed-requests hook, and the "waiting in your Inbox" note are all gone, so this PR no longer changes any upstream file.
  • The Inbox shows seat approvals only, through upstream's approval panel and actions, so provider warnings and the permission label show.
  • The poll runs only while some thread has a pending approval.

The docs describe the slimmed behavior, and the PR description has a current-scope summary. CI is running.

@github-actions github-actions Bot added size:L 100-499 effective changed lines (test files excluded in mixed PRs). and removed size:XL 500-999 effective changed lines (test files excluded in mixed PRs). labels Sep 28, 2026
Comment thread apps/web/src/j5/crew/crewRuntimeRequestsClient.ts

@Jacksondr5 Jacksondr5 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on Jackson's behalf.

Approved. Verified by the review panel (Opus 5.5 + Astra), with a live pass on 3c3bee9:

  • a real seat approval shows in the Inbox and inline, and answering from the Inbox resumes the seat;
  • a stale answer gets 409 with nothing dispatched;
  • the Captain's approvals and seat questions stay inline only;
  • the poll makes zero calls when nothing is pending;
  • upstream's approval panel shows warnings and the permission label.

No upstream files change.

  • Known limitation, not a blocker: a thread reports only its newest pending request, so a newer question on a seat hides an older approval from the Inbox until the question resolves. The approval can still be answered inline, and this gets rarer once seats lose their question tools (#325).
  • The polling simplification (r4124053858) is optional.
  • Merge order: #308 first, then this PR.

@Jacksondr5

Copy link
Copy Markdown
Owner

[Review panel: Opus 5.5 + Astra] Round-two live verification of the seat-approvals-only scope.

Scenario Result
Seat command approval PASS: one Inbox item and bell count; the same request remains inline in the seat thread.
Answer from Inbox PASS: HTTP 200 resumes the real Claude turn, runs the harmless temporary-file command, and produces SEAT_INBOX_RESUMED.
Answer inline, then submit a stale Inbox answer PASS: inline answer resumes the turn; stale Inbox click returns 409; no additional orchestration events are dispatched; the item disappears on the next real poll.
Captain approval PASS: remains inline, absent from Inbox, bell 0.
Seat question PASS: AskUserQuestion remains inline, absent from Inbox, and can be answered there.
Poll gating PASS: zero Crew-request calls in an initial 60-second idle window; calls begin when an approval appears; zero calls in the 60 seconds after the seat approval resolves.

The real Crew used a Claude Haiku Captain and seat, both Supervised, against a fresh snapshot of real data. The base and head web clients used the same isolated backend, 1440×1000 viewport and light theme. All 10 focused tests pass.

Live permission-kind requests and native provider warnings were NOT RUN: Claude emitted command approvals without these fields. A separately labelled response fixture verified the upstream permission label, warning icon, tooltip and accessible description. Native desktop and mobile were not run.

Pending seat approval: base → head.

Empty Inbox: base → head.

Read held pending; there is no dedicated Crew-request loading indicator: base → head.

Stale-answer error: base → head.

The seat approval remains inline.

The real seat turn resumes after the Inbox approval.

Captain approval stays inline.

Seat question stays inline.

Supplemental UI fixture: permission label and provider warning.

The turn resumes after the Inbox approval.

@bryantderosier
bryantderosier merged commit ff73442 into j5/main Sep 28, 2026
31 checks passed
@bryantderosier
bryantderosier deleted the j5/crew-requests-web branch September 28, 2026 17:18
@bryantderosier

Copy link
Copy Markdown
Collaborator Author

@Jacksondr5 Thanks. #308 and #309 are merged. About the known limitation, where a newer question on a seat hides an older approval: #348, one PR up this stack, already removes seats' native question tools (Claude seats lose AskUserQuestion, and Codex seats get their question tool turned off in the thread config, confirmed on a live app-server). So a seat can no longer raise the question that would mask its approval. I'm taking the optional polling simplification in a follow-up, since #309 has merged.

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

Labels

size:L 100-499 effective changed lines (test files excluded in mixed PRs). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A Crew thread's provider approvals and questions are answered from the Inbox, not inline (web)

2 participants