Skip to content

feat(agents): Crews, a roster the Captain proposes and the person approves - #148

Merged
bryantderosier merged 14 commits into
crews/03-saved-agents-as-peersfrom
crews/04-crews-roster-gate
Sep 21, 2026
Merged

bryantderosier merged 14 commits into
crews/03-saved-agents-as-peersfrom
crews/04-crews-roster-gate

Conversation

@bryantderosier

@bryantderosier bryantderosier commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

This is the heart of Crews. Nobody writes a Crew file in advance, and there is no dedicated Captain agent either: I send /crew <brief> in any web or desktop thread, fresh or mid-conversation, and the brief goes to whatever agent that thread already runs as, wrapped in a short platform block that says to compose a Crew rather than do the work. That agent reads the saved-agent library with list_agents and files a roster with propose_crew; the agent that proposes is the Captain. My call after the first cut of this branch was to drop the bundled crew-captain persona rather than keep a second copy of the same guidance that would drift.

The roster is answered inline above the Captain's composer, like a planning question; I can drop seats or add agents from the library, then approve or decline. Approval spawns every seat as a saved-agent Peer Agent placed under the Captain, records the roster snapshot, and tells the Captain the decision in its thread. A Captain grows a Crew only through request_crew_member, which lands in the Inbox and counts on the bell; members are refused both verbs and spawn_agent with escalation to their Captain. One Captain may command several Crews at once when the work is concurrent, each proposed through its own gate and named for what it is for. Seats are checked where they are structured (missing or disabled agent, duplicate seat, twelve-seat cap), a failed spawn hands the gate back so the retry converges on its reserved seats, and declining releases seats a failed spawn reserved. Human approval is the authority: approved seats run with their own agent's permissions even when the Captain is read-only.

Crew members show a seat chip in the sidebar and the Captain a Captain chip. In the thread, my /crew turn renders as the brief I wrote with the guidance block one click away, and the gate's decision arrives as a card naming the Crew and listing the roster seat by seat, each seat opening its thread; anything the card parser does not recognize stays raw. Mobile has no delivery seam and shows these as text, as it does A2A deliveries. Open proposals reach the card through a 7.5 second foreground poll on the proposals client, a second poll beside the inbox count; that stays until the J5 change stream in #182 replaces the J5 read polls together. Migration 14 records instances, members, and proposals; id 13 is the machine-sender migration from #144, and this migration drops the earlier-shaped crews tables an early cut left under that id on my dev database. The Crews definition doc ships here, docs-first, with the acceptance criteria the later PRs in this stack fill in.

Built with Claude Fable 5.1 in Claude Code.

Evidence

New UI, so after-only, from my dev server with the stack checked out. The gate card sits inline above the Captain's composer; the two seats here are a Critic seat and a custom seat (#183).

Roster gate inline in the Captain's thread

🤖 Generated with Claude Code

Review-fix browser evidence

Verified in an isolated local app copied from dev data. Focusing roster inputs leaves the composer collapsed; identical seat/persona labels appear once. Issues #189 and #190 remain open for maintainer review.

Change Before After
Roster focus Composer expands on roster focus Composer stays collapsed
Approved seat labels Repeated seat labels Seat labels appear once

Review follow-ups implemented and verified with Codex in the Codex app.

Closes #204.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). labels Sep 15, 2026
@bryantderosier
bryantderosier added this pull request to stack #153 September 15, 2026 21:36
@bryantderosier
bryantderosier force-pushed the crews/04-crews-roster-gate branch from 1d6b4da to 53d721f Compare September 15, 2026 21:44
@bryantderosier bryantderosier self-assigned this Sep 15, 2026
@bryantderosier
bryantderosier force-pushed the crews/04-crews-roster-gate branch 2 times, most recently from 340490a to edaca70 Compare September 16, 2026 11:32

@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.

Reviewed with the whole stack in view and re-checked after the /crew decoupling and concurrent-Crews rework. The rework resolves the Captain-as-role ruling cleanly: no persona, /crew in any thread, one Captain with several named Crews, and the server already keyed Captain-ness by participant and thread. The gate design (MCP proposes, the operate-scoped HTTP route approves, each seat runs its own saved agent's policy) is right.

Requesting changes on two groups.

Lifecycle recovery, three gaps that leave state nothing cleans up (inline on CrewProposalService, CrewLaunchService, AgentCrewInstanceService).

The notice format is injectable and the card parser trusts it (inline on tools.ts, crewNotices.logic.ts, ChatView.tsx). The three problems share one fix at the writer: single-line names, escape the opening seat tag as well as the closing body tag, and wrap after the effort prefix.

Minor, no inline:

  • The body says id 13 is a deliberate gap; the migration list is contiguous and 13 is the machine-sender migration from #144.
  • Human-edited seat names from the card bypass the length bounds, so an empty or over-long name throws while building the UPDATE and surfaces as a 500 rather than the mapped 409.
  • The /crew send substitutes the wrapped brief for the full outgoing text, so appended terminal, element, and review contexts never reach the Captain, and the launch card renders no attachments.
  • The proposals client adds a second 7.5s foreground poll beside the inbox count; not mentioned in the body. With 149's Fleet poll that is three, which is the case for a J5 change stream (separate issue, not this PR).
  • FORK.md calls the ChatView edit one appended case; it is three hunks now. Accurate enough after the persona spread went away, but worth a recount.

Reviewed by Claude Fable 5.1 in Claude Code, with an independent pass by GPT-6-Astra in Codex.

Comment thread apps/server/src/j5/a2a/CrewProposalService.ts Outdated
Comment thread apps/server/src/j5/a2a/AgentCrewInstanceService.ts
Comment thread apps/server/src/j5/a2a/CrewLaunchService.ts Outdated
Comment thread apps/server/src/j5/a2a/AgentCrewReadsHttp.ts
Comment thread apps/server/src/j5/a2a/mcp/tools.ts Outdated
Comment thread apps/server/src/j5/a2a/mcp/tools.ts
Comment thread apps/web/src/j5/crew/crewNotices.logic.ts
Comment thread apps/web/src/components/ChatView.tsx Outdated
@bryantderosier

Copy link
Copy Markdown
Collaborator Author

All of it is in 800b48f8f. The Crew record is now written before any seat spawns, so a failed launch is handed back with my edits kept and a decline retires what exists; additions refuse a same-name seat under a different identity and any seat into a retired Crew; names are one line (seat names lowercase-hyphen) at both doors; the cap is reported as a cap; the /crew turn keeps its contexts and survives the effort prefix; and the body no longer calls id 13 a gap. FORK.md recounts the ChatView hunks.

Comment thread apps/server/src/j5/a2a/mcp/tools.ts Outdated
Comment thread apps/server/src/j5/a2a/mcp/tools.ts Outdated
Comment thread apps/server/src/j5/a2a/mcp/tools.ts
@Jacksondr5

Copy link
Copy Markdown
Owner

Proposal from Jackson for the orchestration instructions, to land with this PR since propose_crew and the /crew guidance live here.

apps/server/src/provider/T3OrchestrationInstructions.ts currently tells every agent about two shapes of help, Subagent and Peer Agent, and never mentions a Crew. An agent asked to "spawn a crew" with no /crew prefix maps the request to the nearest thing it was told about and makes subagents (screenshot on #152). Replace only the first two bullets with a three-option list; the list_participants, schedule_task, and write_artifact bullets and the footer stay as they are.

The `t3-code` MCP server provides app-owned orchestration. When you need other agents to help with work, you have three options. Treat them distinctly:

1. A provider-native Subagent is child work created and owned inside one provider session. Use your provider's native Subagent mechanism when the user asks for a subagent, or when you want help the user does not need to see or message. T3 observes what providers expose, but it does not create or organize Subagents.
2. A Peer Agent is a full participant with its own top-level thread that the user can open and message. Use platform `spawn_agent` when the user wants or would benefit from talking to the agent directly, or when the work needs a model outside your provider. Its brief states the task and whether a reply is expected; when you need a reply, include what should come back in that brief instead of sending a follow-up ask.
3. A Crew is a group of Peer Agents you command for one bounded task; the user can message any seat but mostly speaks through you. Use `propose_crew` when the user asks for a crew or the work splits into distinct responsibilities that should run at once: pick seats from `list_agents`, and the user approves, edits, or declines the roster in the app. Approved seats spawn under you, and each seat's finish reaches you as a message.

Notes:

  • T3OrchestrationInstructions.test.ts pins "provider-native Subagent", "your provider's native Subagent mechanism", "Use platform spawn_agent", and "what should come back in that brief", and forbids delegate_task and "In your brief, tell the new agent". The text above satisfies all of that, so the test needs no change; the adapter tests assert the constant, not the prose.
  • FORK.md case 8 records this file as the fork's seam and says wording changes are authorized ones; add a sentence there that the three-option form landed with Crews.
  • Knock-on: once the standing instructions carry the Crew concept, the <j5_crew_launch> block prepended to every /crew brief can shrink to "compose a Crew for this brief rather than doing the work yourself, then end your turn"; the rest is now said once, here.

@bryantderosier
bryantderosier force-pushed the crews/04-crews-roster-gate branch from 800b48f to cac20d2 Compare September 17, 2026 12:27
@bryantderosier

Copy link
Copy Markdown
Collaborator Author

Round two is in cac20d224. Your orchestration-instructions proposal landed as written, with the mention bullet kept ahead of the three options and the test now pinning the Crew bullet; FORK.md case 8 records it and the /crew launch block shrank to the one sentence the standing text does not carry. The descriptions are shorter and say "user". Seat threads are titled by seat alone. The card's third field is the new seat's instructions on its own line, which is what that box was meant to be. Custom seats (no saved agent) are in #183 on top of the stack.

@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.

Round three, re-checked against the fix commits. Fixed and verified: reservation identity, archived-target additions, single-line names at every door, the ultrathink prefix, contexts preserved on the /crew send with attachments listed on the card, the cap error, 409 for edited seats, the former-seat Captain chip, the id-13 sentence, the three-option orchestration instructions (test pins the Crew bullet, case 8 updated, launch block down to one sentence), the shorter descriptions, and seat threads titled by seat.

Requesting changes on the recovery path, four related items (inline on CrewProposalService.ts, CrewLaunchService.ts, CrewProposalCard.tsx). The record-before-spawn order and the roster retirement on decline are the right shape; these are the gaps left in it.

Comments:

  • Steering text, two accuracy notes (inline on T3OrchestrationInstructions.ts).
  • propose_crew description versus schema (inline on tools.ts).
  • crews.md L42 still lists settle among the machinery the platform ships; #150 removed it.
  • The proposals poll stays until #182, as the body says. Fine.

Reviewed by Claude Fable 5.1 in Claude Code, with an independent pass by GPT-6-Astra in Codex.

Comment thread apps/server/src/j5/a2a/CrewProposalService.ts Outdated
Comment thread apps/server/src/j5/a2a/CrewProposalService.ts Outdated
Comment thread apps/server/src/j5/a2a/CrewLaunchService.ts Outdated
Comment thread apps/web/src/j5/crew/CrewProposalCard.tsx Outdated
Comment thread apps/server/src/provider/T3OrchestrationInstructions.ts Outdated
Comment thread apps/server/src/j5/a2a/mcp/tools.ts Outdated
@bryantderosier

Copy link
Copy Markdown
Collaborator Author

Round three is in d7ed4e8f1: cleanup before the decline is recorded, release by identity for every name a proposal reserved, renamed seats retired before the retry records its roster, and the card seeded from the approved seats. The two orchestration clauses and the seat-name hint are tracked in #187 with the rest of the non-blocking notes, so this could go back to you without them.

@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.

Round four. Fixed and verified: release by identity for every row a proposal minted with its thread archived first, the retry retiring dropped or renamed seats before recording the roster through one unconditional removal, and the card seeding from approvedSeats. The two new tests cover the decline and the renamed-seat retry.

Requesting changes on one thing the reorder opened (inline on CrewProposalService.ts L548): resolving a reopened gate from two devices at once. AGENTS.md: "Multi-device and multi-environment cases are real." The not-found handling (inline on L468) folds into the same change.

For #187, not blocking:

  • A seat dropped in one retry and reintroduced in the next reuses its deterministic spawn ids against a thread the earlier retry archived, and nothing unarchives it. Narrow; refuse or explicitly recover a reintroduced seat.

Reviewed by Claude Fable 5.1 in Claude Code, with an independent pass by GPT-6-Astra in Codex.

Comment thread apps/server/src/j5/a2a/CrewProposalService.ts Outdated
Comment thread apps/server/src/j5/a2a/CrewProposalService.ts Outdated
@Jacksondr5

Jacksondr5 commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

Not blocking, filed as #189: clicking anywhere inside the roster gate card flips the composer from its resting layout to its expanded one. Jackson's two screenshots of the same card, before and after one click:

roster gate, resting composer

roster gate after one click, expanded composer

Cause: the gate is mounted inside the composer's root form, whose onFocusCapture sets the focused flag for any focus inside it except the two exempt scopes in composerEventScope.ts; the gate's root carries neither marker. Preferred fix is to mount the gate above the form, since it is a planning-style question and not a composer control, and the mount line is already the fork's appended case. Details in the issue.

@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.

Round six. Fixed and verified by two independent passes: provider diagnostics encoded at the writer and decoded after field extraction with the named tests; the decline notice sent while the proposal is still claimed; FORK.md naming the reporter, gate-notice, and alert modules and migration 016; thread ids bounded to 78 characters, deterministic per request key, JSON-framed against delimiter collisions, with nothing anywhere parsing the old format; and the failure-injection fixture replaced with an explicit flag. CI green.

Requesting changes on three small things, inline: the escape set is short by two code points; the web badge still says "Failed to start"; and the decline reopen has one continuation that lets a declined proposal launch.

Comments on the id change, none blocking:

  • Pre-upgrade spawn threads on a dev database no longer match by derivation, so a retried addition across the upgrade is refused and its reservations are not released. Acceptable under the pre-dogfood rule; the body's "replay stability unchanged" is true for post-upgrade requests only, and one sentence saying so avoids a surprise.
  • The id itself is 78 bytes, but the terminal history name is id plus terminal id; with a 128-character terminal id, which the contract allows, the file name reaches 289 bytes. The UI generates uuids, so this is a boundary, not a path anyone hits today.
  • mcp/orchestratorVerbs.live.test.ts still builds the old readable id and asserts equality against it; it fails when enabled.

Reviewed by Claude Fable 5.1 in Claude Code, with an independent pass by GPT-6-Astra in Codex.

Comment thread apps/server/src/j5/a2a/runFailures.ts Outdated
Comment thread apps/web/src/j5/crew/CrewNoticeRenderer.tsx Outdated
Comment thread apps/server/src/j5/a2a/CrewProposalService.ts
@bryantderosier
bryantderosier force-pushed the crews/04-crews-roster-gate branch from 776d8da to 2e2ba08 Compare September 21, 2026 14:16
@bryantderosier

Copy link
Copy Markdown
Collaborator Author

Round six fixes are in 2e2ba0894: the escape class covers U+2028/U+2029 with the decoder restoring them, the badge says "Failed", and a decline whose notice committed stays declining for the boot sweep instead of reopening. On the id notes: orchestratorVerbs.live.test.ts now derives the expected id with spawnThreadId; the replay-stability claim in my earlier comment holds for requests made after the upgrade, since pre-upgrade spawn threads on a dev database no longer match by derivation (acceptable under the pre-dogfood rule); and the terminal history name with a 128-character terminal id is a boundary I am leaving alone, since the UI generates uuids and the clamp would live in upstream terminal/Manager.ts.

bryantderosier and others added 14 commits September 21, 2026 11:07
…roves

/crew <brief> on a fresh web or desktop thread launches the bundled Crew Captain, which reads the
saved-agent library with list_agents and files a roster with propose_crew. The person answers the
roster inline above the Captain's composer, removing or adding seats, and approval spawns every seat
as a saved-agent Peer Agent placed under the Captain with the brief and roster in its first turn.
A Captain grows a Crew only through request_crew_member, which lands in the Inbox and counts on the
bell; members are refused both verbs and spawn_agent with escalation to their Captain. Seats are
validated where they are structured, a Crew holds at most twelve seats, and a failed spawn hands
the gate back so the retry converges on its reserved seats. Crew members show a seat chip in the
sidebar and the Captain a Captain chip. Migration 14 records instances, members, and proposals.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n persona

/crew <brief> no longer turns a fresh draft into the crew-captain saved agent. It sends the brief,
wrapped in the platform's crew-composition guidance, as the current thread's next turn, so the
thread keeps whatever agent, model, and policy it already runs as and any thread, fresh or running,
can become a Captain. The bundled crew-captain example, its built-in id, and the library check that
refused the command without it are gone; the guidance the persona carried now rides with the
command, and the tools describe the rest. A Captain may command several Crews at once for
concurrent work, each proposed through its own gate and named for what it is for; the propose_crew
description and the Crews definition say so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The person's /crew turn showed its guidance block in the bubble, and the platform's
<j5_crew_gate> notice arrived in the Captain's thread as raw tagged text. Both now render as
cards through the existing J5 user-row delivery seam: the brief as the brief, with the guidance one
click away, and the decision as its title, the Crew's name, and the roster seat by seat, each seat
opening its thread. The gate notice carries the crew name so the card can say which Crew was
decided. Recognition is strict and anything unparsed stays raw.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two assertions still expected twelve bundled agents after the crew-captain example was removed;
CI caught what the targeted local runs did not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ces cannot be forged

A roster launch now records its Crew before any seat thread exists, so a spawn that fails
partway leaves nothing a Crew record does not know: the gate is handed back with the human's
edited seats kept, a retry converges on the same seats (a renamed seat's stale reservation
is dropped), and a decline retires the record and archives whatever seat threads were
created. Additions release by the approved set, refuse a same-name seat under a different
identity inside the reservation transaction, and refuse to seat anyone into a retired Crew.

Crew and seat names are one line each (seat names lowercase-hyphen, as the card already
enforces), checked by the MCP schemas and again in the service for the human's edited
seats, so a name can no longer forge roster lines in the notices the Captain's card parses.
The seat cap is reported as a cap error with a matching next step, the request_crew_member
description no longer reads the literal `${CREW_SEAT_CAP}`, a retired seat's old
membership no longer hides the Crews that agent now commands, and the `/crew` turn keeps
its attached contexts, survives the Claude effort prefix, and lists its attachments on the
launch card.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…m takes instructions

Jackson's review of the stack, round two. The standing orchestration instructions name three shapes of help (Subagent, Peer Agent, Crew) so an agent asked for a crew without the /crew prefix reaches propose_crew instead of making subagents; the /crew launch block shrinks to what those instructions do not say. The propose_crew and request_crew_member descriptions are shorter, say user rather than human, and ask for short seat names. Seat threads are titled by seat name alone, since the sidebar group and the Crew chip already name the Crew. The roster card's third field is the new seat's instructions on its own line; a user-added seat's reason is fixed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A decline recorded before its cleanup could not be retried once a seat
archive failed, an addition decline released only the last approved
names, a seat renamed between attempts kept its thread as a member with
no brief, and the gate card reseeded from the requested seats after a
failed launch. Cleanup now runs before the decline is recorded, every
row a proposal minted is released by identity, the launcher retires a
renamed seat's thread before recording the roster it launches, and the
card seeds from the approved seats when the gate was handed back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… missing seat thread reads as absent

Two devices resolving the same reopened gate at once could have a decline retire the Crew the
other device's approval had just launched, because approve claimed first and launched while
decline cleaned up first and claimed last. Both now claim the proposal first, compare-and-set from
open into approving or declining, do their work, then write the final status; the second resolver
finds the row claimed and is refused. A claim the server lost mid-way is settled by a boot sweep:
a declining proposal has its cleanup finished and the decline recorded, an approving one is handed
back to the gate, since its launch converges on a retry.

The seat-thread reads in the decline cleanup and the launcher's retry path treated every failure
as "no thread", so a storage error dropped the member row from under a live thread. Only a genuine
not-found passes now; anything else fails the operation and the gate is handed back.

The new seat's name field on the roster card now says "name" rather than "seat".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… started or failed

The approved gate notice was posted the moment the briefs were dispatched, which commits intent
only. Two Claude seats that died on a signed-out subscription reached the Captain later as
"finished" with no reason, after it had already been told the crew was running (Jackson's dogfood,
2026-09-17).

A launch reporter now watches each seat the approval launched: it takes a verdict from what the
seat thread already shows, then follows run updates on the stored-event stream until every first
turn is running or ended without running, or a 60 second window closes. One report then reaches
the Captain: what the person changed against the proposal (added, removed, renamed seats), and
per seat started, failed with the run's recorded error, or not started yet. Ids derive from the
proposal as before, so a replay posts nothing twice; the proposal records when it was reported and
a boot sweep reports any approval the server lost. The decline notice is unchanged.

A seat whose provider is signed out, disabled, or missing was already refused at spawn by the
persona route; the refusal now names the reason ("codex is signed out") instead of "unavailable".
The card shows the change line, a badge on seats that failed or had not started, and each failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gration, not by editing 014

Migration 14 had been edited in place to widen the status check and add reported_at, which a
development database that already ran it would never pick up. Migration 16 rebuilds the proposal
table instead, carrying every row and marking approvals from before the launch report as reported
so the boot sweep does not re-announce them. It is 16 because 15 is the custom-seats rebuild above
this change in the stack, and the migrator skips every id at or below the latest applied, so
renumbering would strand any database that already ran 15.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ 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

2 participants