diff --git a/FORK.md b/FORK.md index 371f36a94f273..1c98f107a0dc3 100644 --- a/FORK.md +++ b/FORK.md @@ -48,7 +48,7 @@ Against upstream pin `b9fa1399cfbacf23f35ba9201af8aebe3f41e807`, the inventory h 5. Shared J5 server composition: `apps/server/src/server.ts` appends only `j5AuthenticatedRoutesLayer` after `websocketRpcRouteLayer`, provides `J5SquadronCreationLayer` once to the V2 runtime and `J5A2AAuxiliaryLayer` once around the combined HTTP/MCP graph. Both transport registrations consume that shared graph and must never create nested J5 runtimes. The aggregate in case 9 owns all J5 authenticated route additions, including the human inbox; upstream `http.ts` changes are limited to the separate branding boundary. 6. A4's visible human-inbox navigation append, relocated by B1: `apps/web/src/components/sidebar/SidebarChrome.tsx`, where `SidebarUtilityMenu` recognizes `/inbox` (and, since 2026-09-09, `/fleet`) for Back behavior and `SidebarChromeHeader` mounts the J5-owned `HumanInboxBell` and `FleetRailEntry` in a trailing control group, after the brand and optional environment pill. These current symbols supersede the original footer/grid anchors. The route files `routes/inbox.tsx` and `routes/fleet.tsx`, the pages, the bell, the rail entry, and their clients remain under J5-owned files (`j5/a2a`, `j5/fleet`); Settings, Pull Requests, Usage, and Back behavior are unchanged. 7. B3's thread A2A rendering seam: `apps/web/src/components/chat/MessagesTimeline.tsx:47` imports J5-owned `renderThreadA2ADelivery`; `TimelineRowContent` at `:1038-1046,:1073-1076` calls it only for `row.kind === "message" && row.message.role === "user"`, passes the complete `ChatMessage` plus one `formatDayAwareTimestamp` label from `TimelineRowCtx`, and renders the generic `UserTimelineRow` only when `a2aDelivery === null`. Classification, envelope parsing, raw fallback, and focused proof remain in `apps/web/src/j5/a2a/ThreadA2ARenderer.tsx` and `.test.tsx`; TA4 stays absent. -8. A2's provider steering seam: `apps/server/src/provider/T3OrchestrationInstructions.ts:7-12` replaces upstream delegated-task steering with current-head truth: provider-native Subagents remain provider-owned, platform `spawn_agent` creates a Peer Agent, and the spawn brief itself states the task plus whether and what reply is expected. A follow-up `send_message(..., expect_reply=true, intent="...")` opens an Exchange only for later work owed by an existing participant; it is never the second half of a spawn sequence. The schedule paragraph stays unchanged. `apps/server/src/provider/T3OrchestrationInstructions.test.ts`, test `steers to provider-native Subagents and platform Peer Agents`, is the narrow terminology/tool-availability proof and guards against restoring spawn-then-ask prose. The composed-prompt assertions in `apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts` and `ClaudeAdapterV2.test.ts` import and assert `T3_CODE_ORCHESTRATION_INSTRUCTIONS` rather than pinning product wording; future authorized wording changes therefore require neither adapter-test edits nor another seam request. A rebase restoring an upstream literal is a protected-seam regression, not an authorized wording update. The ratified SP4 brief sentence lives exactly once in the actual J5 `spawn_agent` Tool description; global steering restates the contract without duplicating that sentence. Since 2026-09-17 (Jackson's proposal on PR #148) the steering names three shapes of help, Subagent, Peer Agent, and Crew, so an agent asked for a crew without the `/crew` prefix reaches `propose_crew` instead of the nearest thing it was told about; the same test pins the Crew bullet, and the `/crew` launch block shrank to what the standing text does not say. +8. A2's provider steering seam: `apps/server/src/provider/T3OrchestrationInstructions.ts:7-12` replaces upstream delegated-task steering with current-head truth: provider-native Subagents remain provider-owned, platform `spawn_agent` creates a Peer Agent, and the spawn brief itself states the task plus whether and what reply is expected. A follow-up `send_message(..., expect_reply=true, intent="...")` opens an Exchange only for later work owed by an existing participant; it is never the second half of a spawn sequence. The schedule paragraph stays unchanged. `apps/server/src/provider/T3OrchestrationInstructions.test.ts`, test `steers to provider-native Subagents and platform Peer Agents`, is the narrow terminology/tool-availability proof and guards against restoring spawn-then-ask prose. The composed-prompt assertions in `apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts` and `ClaudeAdapterV2.test.ts` import and assert `T3_CODE_ORCHESTRATION_INSTRUCTIONS` rather than pinning product wording; future authorized wording changes therefore require neither adapter-test edits nor another seam request. A rebase restoring an upstream literal is a protected-seam regression, not an authorized wording update. The ratified SP4 brief sentence lives exactly once in the actual J5 `spawn_agent` Tool description; global steering restates the contract without duplicating that sentence. Since 2026-09-17 (Jackson's proposal on PR #148) the steering names three shapes of help, Subagent, Peer Agent, and Crew, so an agent asked for a crew without the `/crew` prefix reaches `propose_crew` instead of the nearest thing it was told about; the same test pins the Crew bullet. Crew composition now uses ordinary conversation: the standing instructions cover proposing before working, custom members, direct collaboration, and inbox approval for additions; the retired `/crew` composer injection is removed. 9. SQ1's first-run gate, visible Squadron controls, and authenticated J5 route aggregate: `apps/web/src/routes/_chat.index.tsx:12-53` imports J5-owned gate/directory logic and wraps only the non-hosted-static `IndexDraftLanding` return; the hosted-static onboarding branch at `:30-32` remains unchanged. The gate's state is a real authenticated `GET /api/j5/squadrons` read per connected environment (case 34), not a hard-coded unavailable state; an unavailable read exposes only a forced retry, never an inferred home. `apps/web/src/components/Sidebar.tsx:140-150,1889-1904,,3580-3587` is the sole sidebar-zone `` mount and J5-owned scope consumer; it does not alter chrome, footer, inbox, or roster. `apps/web/src/j5/squadron/SquadronCreateForm.tsx` is shared by the first-run gate and the J5-only ScopeDropdown `Create Squadron…` action: both require explicit name plus one selected folder on a connected environment whose session may operate (case 34; originally primary-environment only), POST that environment's route, refresh its directory, then select only the created Squadron. `apps/web/src/j5/squadron/SquadronDraftState.ts:9-18,43-50,76-83` retains scalar ambient scope semantics and increments a separate selection generation on every explicit choice, including Alpha→Alpha. `apps/web/src/j5/squadron/ThreadHomesClient.ts:109-121,132-145` reads B6's opaque batch `threadId → Registrar-home` response unchanged; Sidebar alone supplies that generation to reread its current raw thread ids for a named scope (zoom-out never forces), replacing stale `unknown` or failed/missing entries with durable Registrar homes. Its pure predicate admits only known homes matching a selected Squadron, excludes unknown/native rows, and the explicit `All Squadrons` state remains zoomed-out/all rows; it never proxies through `projectId`. `apps/web/src/components/ChatView.tsx:249-261,1552-1554,1812-1830,5225-5247,5437-5462,6579-6587` reads the active server thread through that opaque response; a known durable home takes precedence for a visible disabled chip, while unknown/native/legacy threads have none. Its J5-owned first-send boundary uses the known durable home or the draft-local explicit carrier, freezes only the draft-local carrier before the initial RPC, and never promotes ambient context into a launch identity. `apps/web/src/components/chat/DraftHeroHeadline.tsx:1-10` is display-only. State, pure predicates, and UI stay in `apps/web/src/j5/squadron/`; no first choice is inferred. In `apps/server/src/server.ts:78-79,337-342,427-449`, `j5AuthenticatedRoutesLayer` is the one replacement for A4's raw route entry after `websocketRpcRouteLayer`, and the core J5 creation layer is provided once to the V2 runtime. The J5-owned aggregate in `apps/server/src/j5/a2a/J5AuthenticatedRoutes.ts:7-15` composes A4's human inbox with SQ1's Squadron list/create routes and B6's thread-home read. Future J5 authenticated route layers enter through this aggregate, never through another `server.ts` entry. On every rebase, verify these web mounts still have these boundaries, no project proxy, and the single server entry still follows `websocketRpcRouteLayer`. 10. SQ1's lossless launch carrier and durable attach boundary: `packages/contracts/src/orchestrationV2.ts:2322-2342` adds additive-optional unbranded `squadronId` at `:2325`; `packages/client-runtime/src/operations/commands.ts:144-167,546-612` carries it only from an explicit first-message caller into the launch RPC and loudly rejects the otherwise-silent no-bootstrap retry with the typed `SquadronLaunchRequiresBootstrapError`, directing the user to start a new thread. Bootstrap re-carry for that rare partial-failure recovery remains a queued improvement, not an invented fallback. `apps/server/src/ws.ts` forwards it only when present before the fixed `creationSource`; and `apps/server/src/orchestration-v2/ThreadLaunchService.ts:63-79,545-611` sends the carrier to J5's shared creation engine only after the thread is durable and before preparation is scheduled. The shared engine is provided once in `apps/server/src/server.ts:337-342`; the boundary rejects absence or invalid references without defaults, preserves the named durable orphan on failure, and retry uses the same deterministic registration command. On every rebase, verify this exact client → contract → WebSocket → launch → J5-engine chain, one runtime provider, and no parallel HTTP launch door. 11. SQ1's plan-provenance launch carrier is its own protected contract exception: `packages/contracts/src/orchestrationV2.ts:2322-2327` adds additive-optional `sourcePlanRef`; it is not an existing generic field and has no default. `packages/client-runtime/src/operations/commands.ts:152-167,590-596,624-629,669-683`, `apps/server/src/ws.ts`, and `apps/server/src/orchestration-v2/ThreadLaunchService.ts:63-79,548-592` pass it only from the plan→implementation launch. After durable child identity, ThreadLaunch reads the parent Registrar home through the shared engine: a known home is inherited; a legacy no-home parent remains the named native cohort, with neither refusal nor default. On every rebase, verify this additive contract field and the durable-before-preparation order. @@ -210,7 +210,7 @@ If a required change cannot fit this discipline, stop and review the exception b ### Saved-agent mentions (2026-09-08, revised 2026-09-14) -Bryant requested a shared agent picker for Codex and Claude, invoking a saved agent as a subagent within the current conversation. This is a narrow exception to the earlier native-only Subagent steering: the child uses the existing delegated-child engine with `lineage.relationshipToParent = subagent`. Its child thread is backing storage, not a top-level Peer Agent. Ordinary native subagent requests and `spawn_agent` retain their existing meanings; `spawn_agent`'s optional `agent` input (2026-09-09) resolves a saved agent into a Peer Agent's immutable assignment entirely inside `apps/server/src/j5/agents/agentPersonaSpawn.ts` and the J5 toolkit handlers, with no further upstream hook. Proposal-driven Crews (2026-09-09; the earlier definition-file, Crews-tab, and `@crew` slice was reverted before shipping): `apps/web/src/components/ChatView.tsx` gains one appended case in the send path, in three hunks (the import, the parse-and-refuse before any in-flight state, and the substitution of the wrapped brief for the raw command before the composer's contexts are appended) — it calls J5-owned `parseCrewCommand`/`crewCommandRefusal`/`crewLaunchPrompt` from `apps/web/src/j5/crew/crewCommand.ts` on the raw prompt and sends the brief wrapped in the crew-composition guidance instead of the raw command, with attached terminal, element, and review contexts still appended after it, on a fresh draft or an existing thread alike; the Claude effort prefix is applied after the wrapper as for any turn, and the card parser skips it; the thread keeps whatever agent and model it has, so no saved agent is attached and no `crew-captain` persona exists (decoupled 2026-09-16 at Bryant's request; the bundled example was removed with it). `apps/web/src/components/chat/ChatComposer.tsx` spreads J5-owned `j5CrewSlashCommandItems(selectedProvider)` into the slash-menu item list (one appended spread) so `/crew` is discoverable in every composer; the item reuses the provider-slash-command shape, so selection inserts `/crew ` with no new item variant. The `/crew` turn and the platform's `` decisions render as cards through case 24's existing `renderThreadA2ADelivery` delegate: J5-owned `apps/web/src/j5/crew/CrewNoticeRenderer.tsx` and `crewNotices.logic.ts` recognize the tagged blocks strictly (anything unparsed stays raw), and the one upstream call site in `MessagesTimeline.tsx` gains two fields on the same call, `threadRef` and `markdownCwd`, so a seat opens its thread and the brief renders as markdown (2026-09-16). Mobile has no delivery seam and shows these notices as text, as it does A2A deliveries. `ChatView.tsx` mounts J5-owned `CrewRosterGate` (`apps/web/src/j5/crew/CrewRosterGate.tsx`) immediately above `ChatComposer`, outside its form so roster focus does not expand the composer: the initial roster proposal is answered inline above the Captain's composer like a planning question, while mid-run seat additions stay on the inbox bell (`crewProposals.logic.ts` decides which is which). The human gate (open proposals, approve with edited seats, decline) lives entirely in J5 files: `apps/web/src/j5/crew/`, the J5 inbox page and bell, and server routes under `apps/server/src/j5/a2a/CrewProposalsHttp.ts`. One adapter append makes the gate reachable from Claude: `claudeMcpQueryOverrides` in `apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts` spreads J5-owned `J5_CLAUDE_MCP_ALLOWED_TOOLS` (`apps/server/src/j5/a2a/mcp/claudeAllowedTools.ts`) into the read-only pre-approval list, because Claude runs read-only personas in `dontAsk` mode and would otherwise deny every J5 coordination tool; the upstream `CLAUDE_READ_ONLY_T3_MCP_ALLOWED_TOOLS` constant and its toolkit cross-check stay untouched, and the two exact-allowlist assertions in `ClaudeAdapterV2.test.ts` include the J5 spread. The Codex counterpart is one spread in `codexThreadRuntimeParams` (`apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts`): the injected `mcp_servers."t3-code"` entry gains J5-owned `J5_CODEX_T3_MCP_SERVER_CONFIG` (`apps/server/src/j5/a2a/mcp/codexToolApproval.ts`; since 2026-09-14 a per-tool `tools..approval_mode = "approve"` map naming the J5 verbs plus `write_artifact` (a saved agent's declared handoff lives in application storage, never the workspace) and, since 2026-09-15, the provider-native Subagent verbs `delegate_task`, `task_status`, and `task_cancel` (a Crew member refused `spawn_agent` is sent there), never a server-wide default, so worktree handoff, browser preview, and scheduling keep Codex's own verdict for read-only personas), because Codex 0.153+ rejects every non-read-only MCP tool under approval policy `never` with "MCP tool call requires approval, but approval policy is never"; that policy is what full-access mode and every saved-agent runtime policy send, so without the spread `send_message`, `spawn_agent`, and `propose_crew` all fail on Codex. The spread applies only when the thread's resolved approval policy is `never` (`codexApprovalPolicyIsNever`), so interactive modes keep Codex's own prompting. The platform's own tools carry their gates server-side. The `codexThreadRuntimeParams` assertion in `CodexAdapterV2.test.ts` includes the field. +Bryant requested a shared agent picker for Codex and Claude, invoking a saved agent as a subagent within the current conversation. This is a narrow exception to the earlier native-only Subagent steering: the child uses the existing delegated-child engine with `lineage.relationshipToParent = subagent`. Its child thread is backing storage, not a top-level Peer Agent. Ordinary native subagent requests and `spawn_agent` retain their existing meanings; `spawn_agent`'s optional `agent` input (2026-09-09) resolves a saved agent into a Peer Agent's immutable assignment entirely inside `apps/server/src/j5/agents/agentPersonaSpawn.ts` and the J5 toolkit handlers, with no further upstream hook. Proposal-driven Crews use ordinary conversation. The former `/crew` send-path wrapper in `ChatView.tsx` and slash-menu spread in `ChatComposer.tsx` are removed; case 8's standing instructions guide the agent to propose a roster, including custom members, and wait for the person's decision before launching. The platform's `` decisions render as cards through case 24's existing `renderThreadA2ADelivery` delegate: J5-owned `apps/web/src/j5/crew/CrewNoticeRenderer.tsx` and `crewNotices.logic.ts` recognize the tagged blocks strictly (anything unparsed stays raw), and the one upstream call site in `MessagesTimeline.tsx` gains two fields on the same call, `threadRef` and `markdownCwd`, so a seat opens its thread and the brief renders as markdown (2026-09-16). Mobile has no delivery seam and shows these notices as text, as it does A2A deliveries. `ChatView.tsx` mounts J5-owned `CrewRosterGate` (`apps/web/src/j5/crew/CrewRosterGate.tsx`) immediately above `ChatComposer`, outside its form so roster focus does not expand the composer: the initial roster proposal is answered inline above the Captain's composer like a planning question, while mid-run seat additions stay on the inbox bell (`crewProposals.logic.ts` decides which is which). The human gate (open proposals, approve with edited seats, decline) lives entirely in J5 files: `apps/web/src/j5/crew/`, the J5 inbox page and bell, and server routes under `apps/server/src/j5/a2a/CrewProposalsHttp.ts`. One adapter append makes the gate reachable from Claude: `claudeMcpQueryOverrides` in `apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts` spreads J5-owned `J5_CLAUDE_MCP_ALLOWED_TOOLS` (`apps/server/src/j5/a2a/mcp/claudeAllowedTools.ts`) into the read-only pre-approval list, because Claude runs read-only personas in `dontAsk` mode and would otherwise deny every J5 coordination tool; the upstream `CLAUDE_READ_ONLY_T3_MCP_ALLOWED_TOOLS` constant and its toolkit cross-check stay untouched, and the two exact-allowlist assertions in `ClaudeAdapterV2.test.ts` include the J5 spread. The Codex counterpart is one spread in `codexThreadRuntimeParams` (`apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts`): the injected `mcp_servers."t3-code"` entry gains J5-owned `J5_CODEX_T3_MCP_SERVER_CONFIG` (`apps/server/src/j5/a2a/mcp/codexToolApproval.ts`; since 2026-09-14 a per-tool `tools..approval_mode = "approve"` map naming the J5 verbs plus `write_artifact` (a saved agent's declared handoff lives in application storage, never the workspace) and, since 2026-09-15, the provider-native Subagent verbs `delegate_task`, `task_status`, and `task_cancel` (a Crew member refused `spawn_agent` is sent there), never a server-wide default, so worktree handoff, browser preview, and scheduling keep Codex's own verdict for read-only personas), because Codex 0.153+ rejects every non-read-only MCP tool under approval policy `never` with "MCP tool call requires approval, but approval policy is never"; that policy is what full-access mode and every saved-agent runtime policy send, so without the spread `send_message`, `spawn_agent`, and `propose_crew` all fail on Codex. The spread applies only when the thread's resolved approval policy is `never` (`codexApprovalPolicyIsNever`), so interactive modes keep Codex's own prompting. The platform's own tools carry their gates server-side. The `codexThreadRuntimeParams` assertion in `CodexAdapterV2.test.ts` includes the field. The proposal flow's server modules are J5-owned and need no upstream registration: `AgentCrewInstanceService`, `AgentCrewProposalService`, `CrewLaunchService`, `CrewProposalService`, `CrewLaunchReporter`, `crewGateNotice`, `crewFailureAlert`, `CrewSeatFinishNotifier`, and `CrewProposalsHttp` under `apps/server/src/j5/a2a/`, the `list_personas`, `propose_crew`, `request_crew_member`, and `archive_crew` verbs in the J5 toolkit, and J5 migrations 14 (`j5_agent_crew_instance`, `j5_agent_crew_member`, `j5_agent_crew_proposal`) and 15 (`agent_id` nullable on `j5_agent_crew_member`: a custom seat names no saved agent and runs on the Captain's provider, model, and runtime mode, 2026-09-17). `archive_crew` composes the J5 `ArchiveAgentService` (which gained a side-effect-free `readFacts` and a `confirmationSatisfied` input for unit archives), and `archive_agent` refuses Crew members. Names are one line each and seat names are lowercase-hyphen words (`crewLimits.ts`), enforced by the MCP schemas and again in `CrewProposalService.validateSeats` for the human's edited card, because the platform writes them into the gate and seat notices ahead of the fields the Captain's card parser trusts. A roster launch records its Crew (every planned seat under deterministic ids) 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 approved 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 the failed launch created (2026-09-17, after Jackson's review of PR #148). Crew handoffs (2026-09-10, consolidated 2026-09-14) add no artifact path of their own: a seat whose definition declares an `outputArtifact` writes the same shared handoff file every saved agent writes (`handoffs//-.md`, PR #142), the handoff gate in `apps/server/src/j5/agents/agentHandoffObserver.ts` checks for it and reminds the seat once, and J5-owned `CrewSeatFinishNotifier.ts` tells the Captain in its thread when the seat finishes and whether the file exists; it settles no thread (2026-09-17, Jackson's review: upstream's settle rules apply to seats). That notice is keyed by the seat's participant (one Captain's several Crews reuse seat names), carries the handoff's size and digest so a rewritten body too long to inline still reads as changed, escapes the opening seat tag as well as the closing body tag in quoted handoffs, and a failed notice is left for the next pass rather than swallowed (2026-09-17, after Jackson's review of PR #150). The earlier `deliver_artifact` verb, `j5_agent_artifact` table, and crew-only read verbs were removed before shipping. Two leaf allowlists carry `write_artifact` for read-only personas so a seat can write its handoff into application storage: `J5_CLAUDE_MCP_ALLOWED_TOOLS` (spread into Claude's read-only pre-approval list) and `J5_CODEX_PREAPPROVED_TOOLS` (per-tool `approval_mode` entries on the `t3-code` server for Codex under approval policy `never`). The crew gate rides the J5 client-runtime surface (`packages/client-runtime/src/j5/http.ts`, `state.ts`; contracts in `packages/contracts/src/j5.ts`), so proposals are a per-environment read source like the inbox. Crew launch remains out of scope for `delegate_task`. diff --git a/apps/server/src/j5/a2a/crewGateNotice.ts b/apps/server/src/j5/a2a/crewGateNotice.ts index 59f15a84de695..888664effc241 100644 --- a/apps/server/src/j5/a2a/crewGateNotice.ts +++ b/apps/server/src/j5/a2a/crewGateNotice.ts @@ -135,5 +135,8 @@ export const crewLaunchReportText = (input: { prose.push( "Your crew is running. Each seat has your brief and this roster; coordinate with send_message, and ask the user through the inbox for decisions you cannot make from the brief.", ); + prose.push( + "Seats are told to message you with their results; do not ask for the same results again. End your turn and read them as they arrive.", + ); return `${block}\n\n${prose.join(" ")}`; }; diff --git a/apps/server/src/j5/a2a/mcp/tools.ts b/apps/server/src/j5/a2a/mcp/tools.ts index dae6313c10cfb..1e55e774e7863 100644 --- a/apps/server/src/j5/a2a/mcp/tools.ts +++ b/apps/server/src/j5/a2a/mcp/tools.ts @@ -384,10 +384,10 @@ export const J5_LIST_AGENTS_DESCRIPTION = "List the personas in this environment: id, purpose, runtime policy, whether each can start now, and the provider, model, and reasoning it would run on. Read this before choosing a persona for spawn_agent or a crew roster so the choice fits the task and the user's budget. Read-only."; export const J5_PROPOSE_CREW_DESCRIPTION = - "Propose the crew you need for the brief you were given. Use it when the user asks for a crew or the work splits into distinct responsibilities that should run at once. Call list_personas first and pick one persona per seat, or leave persona unset for a custom seat that runs on your own provider, model, and access mode with only its instructions (required) and the brief; name the crew for what it is for and give each seat a short lowercase-hyphen name like code-reviewer. The user reviews the roster in this thread, may remove or add seats, and approves or declines; you receive the decision and the roster as a message here. Approved seats run with their own persona's permissions, which may exceed yours. You become the crew's Captain and may command several crews at once; later requests, stops, and archives name the crew they mean. Reuse client_request_id to retry safely. This call is itself the human gate, so it works under every sandbox and approval policy, including approval policy never; never refuse the brief because approvals are disabled."; + "Propose the crew you need for the brief you were given. Use it when the user asks for a crew or the work splits into distinct responsibilities that should run at once. Mix saved personas and custom seats in the same roster: call list_personas when choosing a saved persona, or leave persona unset for a custom seat that runs on your own provider, model, and access mode with only its instructions (required) and the brief; name the crew for what it is for and give each seat a short lowercase-hyphen name like code-reviewer. The user reviews the roster and each seat's resolved provider, model, reasoning, and access in this thread, may remove or add seats, and approves or declines; you receive the decision and the roster as a message here. Approved seats run with their own persona's permissions, which may exceed yours. You become the crew's Captain and may command several crews at once; later requests, stops, and archives name the crew they mean. Use send_message for member-to-member, member-to-Captain, and Captain-to-Captain coordination, including intermediate findings and direct results; artifacts do not gate these conversations. Reuse client_request_id to retry safely. This call is itself the human gate, so it works under every sandbox and approval policy, including approval policy never; never refuse the brief because approvals are disabled."; export const J5_REQUEST_CREW_MEMBER_DESCRIPTION = - "Ask the user to add one seat to a crew you command when the work needs one the roster lacks: seat name, persona id from list_personas (or none for a custom seat that runs on your provider, model, and access and needs instructions), a one-line reason, and optionally instructions and a brief for the new seat. The user decides from their inbox; you receive the decision and the updated roster as a message here and can keep working meanwhile. Captain-only; a member escalates to its Captain. Reuse client_request_id to retry safely. Filing the request is the human gate itself and works under every approval policy, including approval policy never."; + "Ask the user to add one seat to a crew you command when the work needs one the roster lacks: seat name, persona id from list_personas (or none for a custom seat that runs on your provider, model, and access and needs instructions), a clear reason identifying the concern and missing expertise or responsibility, and optionally instructions and a brief for the new seat. The user decides from their inbox; you receive the decision and the updated roster as a message here. Continue the already-approved work and direct coordination while the addition is pending. Captain-only; a member sends the concern and needed expertise to its Captain with send_message. Reuse client_request_id to retry safely. Filing the request is the human gate itself and works under every approval policy, including approval policy never."; export const J5_STOP_AGENT_DESCRIPTION = "Stop one Peer Agent: interrupts its running turn now. The agent remains, stays readable, and can be messaged again later — stopping halts work, it retires nothing. Requires your current squadron_id. Reuse client_request_id to retry safely."; diff --git a/apps/server/src/j5/a2a/spawnIds.test.ts b/apps/server/src/j5/a2a/spawnIds.test.ts index 456ae7fefddef..48fa0325e03ab 100644 --- a/apps/server/src/j5/a2a/spawnIds.test.ts +++ b/apps/server/src/j5/a2a/spawnIds.test.ts @@ -20,3 +20,60 @@ describe("spawn thread ids", () => { ); }); }); + +import { assert } from "@effect/vitest"; + +import { spawnFirstTurnText, type CrewBriefContext } from "./spawnIds.ts"; + +const identity = { + brief: "Review the proposed change.", + participantId: "agent:reviewer", + squadronId: "squadron:review", + squadronName: "Review", +}; +const crew: CrewBriefContext = { + displayName: "Change review", + instanceId: "crew:review", + seatName: "reviewer", + captainParticipantId: "agent:captain", + seatInstructions: "Review correctness and report concrete risks.", + roster: [ + { seat: "reviewer", participantId: identity.participantId, agentDisplayName: "custom" }, + { seat: "builder", participantId: "agent:builder", agentDisplayName: "Builder" }, + ], +}; + +it("gives custom seats direct result and concern reporting without a mandatory artifact", () => { + const text = spawnFirstTurnText({ ...identity, crew }); + assert.include(text, "captain_participant_id: agent:captain"); + assert.include(text, "- builder: participant_id=agent:builder"); + assert.include( + text, + "use send_message to coordinate directly with your Captain and other members", + ); + assert.include(text, "final result, supporting evidence, and any remaining blockers"); + assert.include(text, "request_crew_member through the user's inbox"); + assert.include(text, "Continue already-approved work and coordination"); + assert.include(text, "A direct result is sufficient"); + assert.include(text, `\n${crew.seatInstructions}\n`); + assert.notInclude(text, ""); + assert.notInclude(text, "write_artifact"); +}); + +it("retains declared persona output while allowing conversation before that output exists", () => { + const text = spawnFirstTurnText({ + ...identity, + crew: { ...crew, obligation: { kind: "ReviewHandoff", path: "handoffs/review.md" } }, + }); + assert.include(text, "do not wait for an artifact or coordination approval"); + assert.include(text, "write_artifact to exactly `handoffs/review.md`"); + assert.include(text, "must not delay sharing intermediate findings or results"); + assert.notInclude(text, "a chat message is not a delivery"); +}); + +it("keeps an ordinary Peer Agent brief free of crew instructions", () => { + const text = spawnFirstTurnText(identity); + assert.include(text, `\n${identity.brief}\n`); + assert.notInclude(text, "crew_collaboration"); + assert.notInclude(text, "Captain"); +}); diff --git a/apps/server/src/j5/a2a/spawnIds.ts b/apps/server/src/j5/a2a/spawnIds.ts index a706c6e1e2ee1..71c40d6865f70 100644 --- a/apps/server/src/j5/a2a/spawnIds.ts +++ b/apps/server/src/j5/a2a/spawnIds.ts @@ -103,13 +103,15 @@ export const spawnFirstTurnText = (input: { crew.seatInstructions === undefined ? [] : [`\n${crew.seatInstructions}\n`]; - // The deliverable is a platform obligation, not wiring prose: it is the same handoff file every - // saved agent writes, and the Captain is told the moment it appears. + const collaboration = `\nResolve current participants with list_participants and use send_message to coordinate directly with your Captain and other members. Share useful findings, evidence, questions, and blockers immediately; do not wait for an artifact or coordination approval. Send your Captain your final result, supporting evidence, and any remaining blockers before finishing. If the roster lacks expertise needed to address a concern, tell your Captain what is missing and why so they can request_crew_member through the user's inbox. Continue already-approved work and coordination while an addition is pending. A direct result is sufficient unless the user or your persona explicitly requires an artifact.\n`; + // A persona's declared deliverable supplements the conversation; it never gates coordination. const obligation = crew.obligation === undefined ? [] : [ - `\nYour agent definition returns a ${crew.obligation.kind}. Before you finish, write it with write_artifact to exactly \`${crew.obligation.path}\` as Markdown (your instructions list the required contents); a chat message is not a delivery. Your Captain is told when the file appears; rewrite the same path to revise it.\n`, + `\nYour agent definition returns a ${crew.obligation.kind}. Before you finish, write it with write_artifact to exactly \`${crew.obligation.path}\` as Markdown (your instructions list the required contents); this is in addition to direct messages and must not delay sharing intermediate findings or results. Your Captain is told when the file appears; rewrite the same path to revise it.\n`, ]; - return [identity, crewContext, ...seatInstructions, ...obligation, brief].join("\n\n"); + return [identity, crewContext, collaboration, ...seatInstructions, ...obligation, brief].join( + "\n\n", + ); }; diff --git a/apps/server/src/provider/T3OrchestrationInstructions.test.ts b/apps/server/src/provider/T3OrchestrationInstructions.test.ts index 4ee7ef3ff44f7..dfc9fa8e094b6 100644 --- a/apps/server/src/provider/T3OrchestrationInstructions.test.ts +++ b/apps/server/src/provider/T3OrchestrationInstructions.test.ts @@ -46,6 +46,28 @@ describe("T3 orchestration provider instructions", () => { assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "bindToCurrentThread=false"); }); + it("creates mixed crews from chat and keeps coordination independent of approvals and artifacts", () => { + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "asks for a crew in ordinary chat"); + assert.include( + T3_CODE_ORCHESTRATION_INSTRUCTIONS, + "saved personas from `list_personas` with custom seats", + ); + assert.notInclude(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "`list_agents`"); + assert.include( + T3_CODE_ORCHESTRATION_INSTRUCTIONS, + "resolved provider, model, reasoning, and access", + ); + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "Captains of other crews"); + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "Do not wait for an artifact"); + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "`request_crew_member`"); + assert.include( + T3_CODE_ORCHESTRATION_INSTRUCTIONS, + "reason naming the concern and needed responsibility", + ); + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "while that request is pending"); + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "otherwise they queue for its next turn"); + }); + it("routes durable planning outputs into artifacts and excludes working files", () => { assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "durable, user-consumable planning outputs"); assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "`write_artifact`"); diff --git a/apps/server/src/provider/T3OrchestrationInstructions.ts b/apps/server/src/provider/T3OrchestrationInstructions.ts index 4b0ef4374cfd5..a686beaa9f303 100644 --- a/apps/server/src/provider/T3OrchestrationInstructions.ts +++ b/apps/server/src/provider/T3OrchestrationInstructions.ts @@ -8,8 +8,9 @@ The \`t3-code\` MCP server provides app-owned orchestration. When you need other ${AGENT_INVOCATION_INSTRUCTIONS} - 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 provider-native Subagents; the persona mention above instead creates a T3-owned delegated child. For cross-provider work that the user does not need to message directly, use \`delegate_task\`. - 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, including cross-provider work where the user needs a directly messageable participant. 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. -- 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. -- Use \`list_participants\` to resolve an already-addressable agent or the human. Only for later work owed by an existing participant, use \`send_message(..., expect_reply=true, intent="...")\` to open an Exchange. The reply arrives later as an incoming message; continue with other work instead of polling. +- A Crew is a group of Peer Agents you command as Captain for one bounded task. Use \`propose_crew\` when the user asks for a crew in ordinary chat or the work splits into distinct responsibilities that should run at once. Mix saved personas from \`list_personas\` with custom seats: set persona for a saved persona, or omit it and provide task-specific instructions for a custom seat. The app shows each seat's resolved provider, model, reasoning, and access before the user approves, edits, or declines the roster. Approved seats spawn under you, and each seat's finish reaches you as a message. +- Crew members, their Captain, and Captains of other crews can coordinate directly with \`send_message\`: share intermediate findings, concerns, questions, evidence, and final results as soon as useful. Do not wait for an artifact or a separate coordination approval. Return results in messages unless the user or a selected persona explicitly requires an artifact; that output requirement never blocks conversation. If a concern needs expertise missing from the roster, the member tells its Captain, and the Captain uses \`request_crew_member\` with the crew_instance_id and a clear reason naming the concern and needed responsibility. The user approves additions in the existing inbox; continue the already-approved work and coordination while that request is pending. +- Use \`list_participants\` to resolve an already-addressable agent or the human. Use \`send_message\` without expect_reply for updates or results that need no response. Only for later work owed by an existing participant, use \`send_message(..., expect_reply=true, intent="...")\` to open an Exchange. The reply arrives later as an incoming message; continue with other work instead of polling or holding a turn open to wait. Messages reach a supported active turn through provider steering; otherwise they queue for its next turn. - \`schedule_task\` creates persistent recurring work in the app scheduler. Pass \`schedule\` as a structured object, never as JSON text: \`{"type":"interval","everyMs":3600000}\` for an interval, or \`{"type":"fixed_time","timeOfDay":"09:00","weekdays":[1,2,3,4,5]}\` for a wall-clock schedule. By default runs return to the current thread; set \`bindToCurrentThread=false\` only when the user wants a fresh thread for every run. After scheduling, report the returned cadence and next run time. - Use \`write_artifact\` for durable, user-consumable planning outputs that should remain available across threads and agents in the project, such as plans, specifications, diagrams, and research notes. Use \`list_artifacts\` and \`read_artifact\` to access existing artifacts. Do not use artifacts for source code, build output, logs, temporary scratch files, or ordinary repository documentation. Refer to a saved artifact in chat by the logical path returned by \`write_artifact\` (for example \`artifacts/plan.md\`) so J5 opens it in the Artifacts panel. J5 stores artifacts in application data, outside the repository. Artifacts under \`handoffs/\` are versioned: rewriting one adds your content as a new version at the top of the same file rather than replacing it, and reading it back returns a versions header followed by every kept version, newest first. diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index b2000365236bb..20c96b1b813d0 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -304,7 +304,6 @@ import { startSquadronDraft, } from "../j5/squadron/SquadronPicker.logic"; import { refreshThreadHomes, useThreadHomes } from "../j5/squadron/ThreadHomesClient"; -import { crewCommandRefusal, crewLaunchPrompt, parseCrewCommand } from "../j5/crew/crewCommand"; import { resolveEffectiveSquadronId, resolveSquadronDraftChipState, @@ -7027,17 +7026,6 @@ export default function ChatView(props: ChatViewProps) { } } - // J5: `/crew ` sends the brief wrapped in the crew-composition guidance as this - // thread's next turn, whatever agent the thread runs as. Parsed from the raw prompt (attached - // contexts are not a brief) and refused before any in-flight state is set, so a refusal leaves - // the composer exactly as it was. - const j5Crew = parseCrewCommand(promptForSend); - const j5CrewRefusal = j5Crew === null ? null : crewCommandRefusal(j5Crew); - if (j5CrewRefusal !== null) { - toastManager.add(stackedThreadToast({ type: "warning", ...j5CrewRefusal })); - return; - } - sendInFlightRef.current = true; if (isDraftHeroState && activeThreadKey) { let resolveDockStarted: (() => void) | undefined; @@ -7063,12 +7051,8 @@ export default function ChatView(props: ChatViewProps) { const composerElementContextsSnapshot = [...composerElementContexts]; const composerPreviewAnnotationsSnapshot = [...composerPreviewAnnotations]; const composerReviewCommentsSnapshot: ReviewCommentContext[] = [...composerReviewComments]; - // J5: a `/crew` turn sends the wrapped brief in place of the raw command; the attached - // contexts below still ride along, so the Captain reads them with the brief. - const promptBodyForSend = - j5Crew?.kind === "launch" ? crewLaunchPrompt(j5Crew.brief) : promptForSend; const messageTextWithContexts = appendElementContextsToPrompt( - appendTerminalContextsToPrompt(promptBodyForSend, composerTerminalContextsSnapshot), + appendTerminalContextsToPrompt(promptForSend, composerTerminalContextsSnapshot), composerElementContextsSnapshot, ); const messageTextWithPreviewAnnotations = composerPreviewAnnotationsSnapshot.reduce( diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 8c668b4eea8f7..ea31cb3b3ca47 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -821,7 +821,6 @@ import { resolveComposerDispatchMode, type ComposerDispatchMode } from "./compos import { SteerUnavailableNotice } from "../../j5/composer/SteerUnavailableNotice"; import { useJ5SteerState } from "../../j5/composer/useJ5SteerState"; import { shouldRefuseComposerSteer } from "../../j5/composer/submitGuard"; -import { j5CrewSlashCommandItems } from "../../j5/crew/crewSlashCommand"; import type { ContextWindowSnapshot } from "../../lib/contextWindow"; import { formatProviderSkillDisplayName, @@ -2026,12 +2025,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) (item) => item.command.name !== "compact" || compactSlashCommandAvailable, ); const slashCommandItems = slashCommandItemsForPromptPosition( - [ - ...builtInSlashCommandItems, - ...j5CrewSlashCommandItems(selectedProvider), - ...visibleProviderSlashCommandItems, - ...skillItems, - ], + [...builtInSlashCommandItems, ...visibleProviderSlashCommandItems, ...skillItems], composerTrigger.rangeStart === 0, ); return searchSlashCommandItems(slashCommandItems, query); diff --git a/apps/web/src/j5/crew/CrewNoticeRenderer.tsx b/apps/web/src/j5/crew/CrewNoticeRenderer.tsx index 771972803de23..8b8b3490e0f38 100644 --- a/apps/web/src/j5/crew/CrewNoticeRenderer.tsx +++ b/apps/web/src/j5/crew/CrewNoticeRenderer.tsx @@ -7,7 +7,6 @@ import type { ReactNode } from "react"; import ChatMarkdown from "../../components/ChatMarkdown"; import { Badge } from "../../components/ui/badge"; import { Tooltip, TooltipTrigger, TooltipPopup } from "../../components/ui/tooltip"; -import { deriveDisplayedUserMessageState } from "../../lib/terminalContext"; import { buildThreadRouteParams } from "../../threadRoutes"; import { useRightPanelStore } from "../../rightPanelStore"; import { @@ -31,10 +30,6 @@ const TONE_CLASS = { export interface CrewNoticeRenderInput { readonly message: CrewNoticeMessage & { readonly createdAt: string; - /** What the person attached to the turn; the launch card lists them by name. */ - readonly attachments?: - | ReadonlyArray<{ readonly type: string; readonly name?: string }> - | undefined; }; readonly timestampLabel?: string | undefined; readonly participantLabels?: ReadonlyMap | undefined; @@ -43,69 +38,6 @@ export interface CrewNoticeRenderInput { readonly markdownCwd?: string | undefined; } -/** - * The person's `/crew` turn, shown as the brief it is. The guidance block the command sent with - * it stays one click away rather than filling the bubble, since the agent read it and the person - * wrote none of it. - */ -function CrewLaunchCard(props: { - readonly notice: Extract; - readonly input: CrewNoticeRenderInput; -}) { - const { notice, input } = props; - // Attached terminal and element contexts ride with the brief for the Captain; the card shows - // the brief the person typed, the way the ordinary user row hides its appended contexts. - const visibleBrief = deriveDisplayedUserMessageState(notice.brief).visibleText.trim(); - const attachmentNames = (input.message.attachments ?? []).map( - (attachment) => attachment.name ?? attachment.type, - ); - return ( -
-
-
- - Crew brief - {input.timestampLabel ? ( - - ) : null} -
- 0 ? visibleBrief : notice.brief} - cwd={input.markdownCwd} - threadRef={input.threadRef ?? undefined} - className="mt-1.5 text-sm text-foreground" - lineBreaks - parseRawHtml={false} - /> - {attachmentNames.length > 0 ? ( -
    - {attachmentNames.map((name, index) => ( -
  • - - {name} - -
  • - ))} -
- ) : null} -
- - Guidance sent with the brief - -

- {notice.guidance} -

-
-
-
- ); -} - /** * The gate's decision as the Captain received it: what was decided, about which Crew, and the * roster seat by seat. Every seat opens its thread; its label names the saved agent, not the thread title. @@ -349,7 +281,6 @@ function CrewSeatsCard(props: { export function renderCrewNotice(input: CrewNoticeRenderInput): ReactNode { const notice = presentCrewNotice(input.message); if (notice === null) return null; - if (notice.kind === "launch") return ; if (notice.kind === "seats") return ; return ; } diff --git a/apps/web/src/j5/crew/crewCommand.test.ts b/apps/web/src/j5/crew/crewCommand.test.ts deleted file mode 100644 index 8854a366e1eeb..0000000000000 --- a/apps/web/src/j5/crew/crewCommand.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { describe, expect, it } from "vite-plus/test"; - -import { CUSTOM_AGENT, addSeat, describeSeatAgent, removeSeat } from "./CrewProposalCard"; -import { crewCommandRefusal, crewLaunchPrompt, parseCrewCommand } from "./crewCommand"; -import { j5CrewSlashCommandItems } from "./crewSlashCommand"; -import { ProviderDriverKind } from "@t3tools/contracts"; - -describe("/crew command", () => { - it("recognizes the command with a brief and ignores ordinary text", () => { - expect(parseCrewCommand("/crew fix the flaky login test")).toEqual({ - kind: "launch", - brief: "fix the flaky login test", - }); - expect(parseCrewCommand(" /crew\n Follow @docs/runbook.md\n and report back ")).toEqual({ - kind: "launch", - brief: "Follow @docs/runbook.md\n and report back", - }); - expect(parseCrewCommand("/crew")).toEqual({ kind: "missing-brief" }); - expect(parseCrewCommand("/crews go")).toBeNull(); - expect(parseCrewCommand("please /crew this")).toBeNull(); - }); - - it("refuses only a missing brief; any thread and any agent may compose a crew", () => { - expect(crewCommandRefusal({ kind: "missing-brief" })?.title).toBe("Give the crew a brief"); - expect(crewCommandRefusal({ kind: "launch", brief: "x" })).toBeNull(); - }); - - it("sends the brief verbatim after the platform's crew guidance", () => { - const prompt = crewLaunchPrompt("Land the invoice-export PR: build, review, sit on CI."); - expect(prompt.startsWith("\n")).toBe(true); - expect( - prompt.endsWith("\n\nLand the invoice-export PR: build, review, sit on CI."), - ).toBe(true); - expect(prompt).toContain("propose_crew"); - expect(prompt).toContain("end your turn"); - }); -}); - -describe("crew proposal roster edits", () => { - const seats = [ - { seat: "builder", agentId: "builder", reason: "Implements" }, - { seat: "critic", agentId: "critic", reason: "Reviews" }, - ]; - - it("removes a seat by name and adds a normalized seat with the user's instructions", () => { - expect(removeSeat(seats, "critic").map((seat) => seat.seat)).toEqual(["builder"]); - const added = addSeat(seats, { seat: "Security Pass", agentId: "sentry", instructions: " " }); - expect(added.error).toBeNull(); - expect(added.seats.at(-1)).toEqual({ - seat: "security-pass", - agentId: "sentry", - reason: "Added by the user", - }); - const briefed = addSeat(seats, { - seat: "eyes", - agentId: "sentry", - instructions: " Review only the auth module. ", - }); - expect(briefed.seats.at(-1)?.instructions).toBe("Review only the auth module."); - }); - - it("names the persona and the access a seat is approved with, or admits it cannot", () => { - const rows = [ - { personaId: "critic", displayName: "Critic", authority: "Review only" }, - { personaId: "builder", displayName: "Builder", authority: "Workspace write" }, - ]; - expect(describeSeatAgent(rows, "builder")).toEqual({ - name: "Builder", - authority: "Workspace write", - }); - expect(describeSeatAgent(rows, "ghost")).toEqual({ name: "ghost", authority: null }); - expect(describeSeatAgent(rows, null)).toEqual({ - name: "Custom seat", - authority: "Runs with the Captain's model and access mode", - }); - }); - - it("adds a custom seat with no persona behind it once it has instructions", () => { - expect( - addSeat(seats, { seat: "scribe", agentId: CUSTOM_AGENT, instructions: " " }).error, - ).toContain("Give the custom seat"); - const custom = addSeat(seats, { - seat: "scribe", - agentId: CUSTOM_AGENT, - instructions: "Keep the running notes.", - }); - expect(custom.error).toBeNull(); - expect(custom.seats.at(-1)).toEqual({ - seat: "scribe", - agentId: null, - reason: "Added by the user", - instructions: "Keep the running notes.", - }); - }); - - it("rejects duplicate or malformed seat names and a missing persona", () => { - expect(addSeat(seats, { seat: "builder", agentId: "scout", instructions: "" }).error).toContain( - "already exists", - ); - expect(addSeat(seats, { seat: "9lives", agentId: "scout", instructions: "" }).error).toContain( - "lowercase", - ); - expect(addSeat(seats, { seat: "eyes", agentId: "", instructions: "" }).error).toContain( - "Pick a persona", - ); - }); -}); - -describe("/crew slash menu entry", () => { - it("offers /crew in every composer and inserts the command on selection", () => { - const codex = ProviderDriverKind.make("codex"); - const [item] = j5CrewSlashCommandItems(codex); - expect(item).toMatchObject({ - type: "provider-slash-command", - label: "/crew", - command: { name: "crew" }, - }); - }); -}); diff --git a/apps/web/src/j5/crew/crewCommand.ts b/apps/web/src/j5/crew/crewCommand.ts deleted file mode 100644 index 72b1d1f68c65f..0000000000000 --- a/apps/web/src/j5/crew/crewCommand.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * `/crew ` asks the agent in the current thread to compose a Crew for the brief instead of - * doing the work itself. The thread keeps whatever agent, model, and policy it already runs as; the - * platform's crew-composition guidance rides along with the brief as that turn's text, so no saved - * agent is required and the same thread can command more than one Crew. - */ -const CREW_COMMAND = /^\/crew(?:\s+([\s\S]+))?$/; - -export type CrewCommand = - | { readonly kind: "launch"; readonly brief: string } - | { readonly kind: "missing-brief" }; - -/** Recognize the crew command anywhere a composer text is about to be sent; null means not a command. */ -export function parseCrewCommand(text: string): CrewCommand | null { - const match = CREW_COMMAND.exec(text.trim()); - if (match === null) return null; - const brief = match[1]?.trim() ?? ""; - return brief.length === 0 ? { kind: "missing-brief" } : { kind: "launch", brief }; -} - -/** The user-facing reason a crew command cannot run; null when it can. */ -export function crewCommandRefusal( - command: CrewCommand, -): { readonly title: string; readonly description: string } | null { - if (command.kind === "missing-brief") - return { - title: "Give the crew a brief", - description: "Type /crew followed by what the crew should accomplish.", - }; - return null; -} - -/** - * The turn `/crew` sends: the platform's guidance for composing a Crew, then the brief verbatim. - * The guidance covers only what the propose_crew tool cannot say about this moment — compose - * rather than work, end the turn after proposing, and that several Crews may run at once. The - * roster, the gate, and every later verb are described by the tools themselves. - */ -export const crewLaunchPrompt = (brief: string) => - [ - "", - "Compose a Crew for the brief below with propose_crew instead of doing the work yourself, then end your turn: the person's decision, and later every seat's finish, arrive as new messages in this thread, so never wait or poll for them inside a turn.", - "", - "", - brief, - ].join("\n"); diff --git a/apps/web/src/j5/crew/crewNotices.logic.test.ts b/apps/web/src/j5/crew/crewNotices.logic.test.ts index 5f915b3f22832..3029f38c30be2 100644 --- a/apps/web/src/j5/crew/crewNotices.logic.test.ts +++ b/apps/web/src/j5/crew/crewNotices.logic.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vite-plus/test"; -import { crewLaunchPrompt } from "./crewCommand"; import { artifactPanelPath, crewGateFooter, @@ -28,35 +27,6 @@ const approvedGate = [ ].join("\n"); describe("crew notices in the Captain's thread", () => { - it("presents the person's /crew turn as its brief with the guidance set aside", () => { - const notice = presentCrewNotice({ - role: "user", - createdBy: "user", - text: crewLaunchPrompt("Land the invoice-export PR: build, review, sit on CI."), - }); - expect(notice).toMatchObject({ - kind: "launch", - brief: "Land the invoice-export PR: build, review, sit on CI.", - }); - expect(notice?.kind === "launch" && notice.guidance).toContain("propose_crew"); - // A message that merely mentions the tag, or an empty brief, stays an ordinary message. - expect(presentCrewNotice({ role: "user", text: "about tags" })).toBeNull(); - expect( - presentCrewNotice({ role: "user", text: "\nx\n\n\n" }), - ).toBeNull(); - expect(presentCrewNotice({ role: "assistant", text: crewLaunchPrompt("x") })).toBeNull(); - // Claude's effort prefix lands ahead of the wrapper; the card still recognizes the turn, and - // contexts appended after the brief stay part of what the Captain received. - const prefixed = presentCrewNotice({ - role: "user", - createdBy: "user", - text: `Ultrathink:\n${crewLaunchPrompt("Fix it.")}\n\n\n$ ls\n`, - }); - expect(prefixed).toMatchObject({ kind: "launch" }); - expect(prefixed?.kind === "launch" && prefixed.brief).toContain("Fix it."); - expect(prefixed?.kind === "launch" && prefixed.brief).toContain("terminal_context"); - }); - it("presents an approved roster with its seats and names the seats for the identity read", () => { const message = { role: "user", createdBy: "system", text: approvedGate }; const notice = presentCrewNotice(message); diff --git a/apps/web/src/j5/crew/crewNotices.logic.ts b/apps/web/src/j5/crew/crewNotices.logic.ts index c373bab750966..69886b9c4b968 100644 --- a/apps/web/src/j5/crew/crewNotices.logic.ts +++ b/apps/web/src/j5/crew/crewNotices.logic.ts @@ -57,12 +57,6 @@ export type CrewNoticePresentation = readonly kind: "seats"; readonly seats: ReadonlyArray; } - | { - /** The person's `/crew ` turn: the brief, with the guidance block set aside. */ - readonly kind: "launch"; - readonly brief: string; - readonly guidance: string; - } | { /** The gate's decision, posted to the Captain by the platform. */ readonly kind: "gate"; @@ -81,11 +75,6 @@ export type CrewNoticePresentation = readonly pendingSeats: ReadonlyArray; }; -// The Claude effort prefix (`applyClaudePromptEffortPrefix`) is applied after the wrapper, so a -// launch sent with ultrathink starts with that line; the card skips it rather than showing the -// raw block. -const LAUNCH_BLOCK = - /^(?:Ultrathink:\n)?\n([\s\S]*?)\n<\/j5_crew_launch>\n\n([\s\S]*)$/; const GATE_BLOCK = /^\n([\s\S]*?)\n<\/j5_crew_gate>/; const ROSTER_LINE = /^- ([^:]+): participant_id=(\S+) persona=(\S*) thread_id=(\S+)(?: start=(started|failed|pending))?( \(new\))?$/; @@ -104,13 +93,6 @@ const fields = (block: string, name: string) => const seatStart = (value: string | undefined): SeatStart | null => value === "started" || value === "failed" || value === "pending" ? value : null; -const parseLaunch = (text: string): CrewNoticePresentation | null => { - const match = LAUNCH_BLOCK.exec(text); - if (match === null) return null; - const brief = match[2]!.trim(); - return brief.length === 0 ? null : { kind: "launch", guidance: match[1]!.trim(), brief }; -}; - const parseGate = (text: string): CrewNoticePresentation | null => { const match = GATE_BLOCK.exec(text); if (match === null) return null; @@ -243,8 +225,6 @@ const parseSeats = (text: string): CrewNoticePresentation | null => { export const presentCrewNotice = (message: CrewNoticeMessage): CrewNoticePresentation | null => { if (message.role !== "user") return null; if (message.createdBy === "system") return parseGate(message.text) ?? parseSeats(message.text); - if (message.createdBy === undefined || message.createdBy === "user") - return parseLaunch(message.text); return null; }; diff --git a/apps/web/src/j5/crew/crewSlashCommand.ts b/apps/web/src/j5/crew/crewSlashCommand.ts deleted file mode 100644 index 23cc2a9e44841..0000000000000 --- a/apps/web/src/j5/crew/crewSlashCommand.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ProviderDriverKind } from "@t3tools/contracts"; - -/** - * The slash menu entry for `/crew`. It borrows the provider-slash-command item shape so the - * upstream composer inserts `/crew ` on selection without a new item variant; the brief is then - * typed after it and the send path wraps it in the crew guidance. Any thread, fresh or running, - * can compose a Crew, so every composer offers it. - */ -export const j5CrewSlashCommandItems = (provider: ProviderDriverKind) => [ - { - id: "j5:slash:crew", - type: "provider-slash-command" as const, - provider, - command: { - name: "crew", - description: "Ask this thread's agent to propose a crew for your brief", - }, - label: "/crew", - description: "Ask this thread's agent to propose a crew for your brief", - }, -]; diff --git a/docs/j5/product/features/crews.md b/docs/j5/product/features/crews.md index 22cb5e6023a91..bd824af74e6a6 100644 --- a/docs/j5/product/features/crews.md +++ b/docs/j5/product/features/crews.md @@ -11,13 +11,13 @@ Complex work needs a group of agents with defined jobs working together with a h ## Definition -A **Crew** is a group of agents launched as a unit from a **roster its Captain proposes and the person approves**, composed for the brief in front of it. Nobody writes a Crew file in advance. The person starts a Crew by sending `/crew ` in any thread (web and desktop): the brief goes to that thread's agent, whatever persona, model, and policy it already runs as, wrapped in the platform's guidance to compose a Crew rather than do the work. No dedicated Captain agent exists; the agent that proposes becomes the Captain. It reads the persona library and proposes a roster — a name, the brief every seat will start on, and one seat per persona with a one-line reason and, when useful, seat instructions. A seat may name no persona at all: a **custom seat**, named and briefed by the Captain or the person, runs on the Captain's own provider, model, and effective access mode (a persona Captain's sandbox is not carried: only a persona assignment can convey one, so the gate promises the access mode alone), so a Crew need not fit a mold anyone saved before (Jackson and Bryant, 2026-09-17). Its instructions are required at every door, since nothing else defines it. The proposal is a **human gate** answered where it was asked: inline above the Captain's composer, like a planning question, and never on the inbox bell (reaffirmed by Bryant, 2026-09-14). The person may drop seats, add personas from the library or a custom seat with a name and instructions, then approve the final roster or decline. Approval spawns every seat as a Peer Agent placed under the Captain, with a persona assignment only when the seat names a saved definition, records the roster snapshot (the _Manifest_ in implementation vocabulary: name, brief, members, who approved each and why, version), and tells the Captain how the launch went once every seat has opened its first turn or failed to: one **launch report** naming what the person changed against the proposal and, per seat, started, failed with the run's error, or startup still unconfirmed after a minute. Dispatching a brief commits intent, not a running seat, so nothing is said before that (Jackson's dogfood, 2026-09-17). A decline is told at once. Each seat's first turn carries the platform's identity facts, the crew context and roster, any seat instructions, a handoff obligation when its definition declares one, and the brief. +A **Crew** is a group of agents launched as a unit from a **roster its Captain proposes and the person approves**, composed for the brief in front of it. Nobody writes a Crew file in advance. The person asks the agent in an ordinary conversation to compose a Crew (web and desktop). The agent uses the conversation as context, keeps its current persona, model, and policy, and proposes the team before starting its work. There is no Crew slash command. No dedicated Captain agent exists; the agent that proposes becomes the Captain. It proposes a roster — a name, the brief every seat will start on, and a responsibility with a reason and instructions for each member. Saved personas are optional templates; a roster can mix them with custom members. A seat may name no persona at all: a **custom seat**, named and briefed by the Captain or the person, runs on the Captain's own provider, model, and effective access mode (a persona Captain's sandbox is not carried: only a persona assignment can convey one, so the gate promises the access mode alone), so a Crew need not fit a mold anyone saved before (Jackson and Bryant, 2026-09-17). Its instructions are required at every door, since nothing else defines it. The proposal is a **human gate** answered where it was asked: inline above the Captain's composer, like a planning question, and never on the inbox bell (reaffirmed by Bryant, 2026-09-14). The person may drop seats, add personas from the library or a custom seat with a name and instructions, then approve the final roster or decline. Approval spawns every seat as a Peer Agent placed under the Captain, with a persona assignment only when the seat names a saved definition, records the roster snapshot (the _Manifest_ in implementation vocabulary: name, brief, members, who approved each and why, version), and tells the Captain how the launch went once every seat has opened its first turn or failed to: one **launch report** naming what the person changed against the proposal and, per seat, started, failed with the run's error, or startup still unconfirmed after a minute. Dispatching a brief commits intent, not a running seat, so nothing is said before that (Jackson's dogfood, 2026-09-17). A decline is told at once. Each seat's first turn carries the platform's identity facts, the crew context and roster, any seat instructions, a handoff obligation when its definition declares one, and the brief. **Membership changes only through the gate.** When the work needs a seat the roster lacks, the Captain asks for one agent with a reason and an optional brief for that seat. The request lands in the person's Inbox and counts on the bell, because the person may be anywhere mid-run; approval appends the seat to the snapshot and bumps its version, and either decision is posted to the Captain, who keeps working meanwhile. A Crew holds at most **twelve seats**, initial roster and additions together, counting requests still open. Crew members cannot propose or grow Crews — wanting more hands is an escalation to the Captain. **Human approval is the authority.** The person who approves a seat decides what access it runs with, so approved seats run with their own persona's runtime policy even when the Captain is read-only; the gate shows each seat's persona (or that it is a custom seat and runs with the Captain's model and access), the access it will have, and any seat instructions before the person approves. Seats are checked where they are structured: a persona missing from the library or turned off, a duplicate seat name, or a roster past the cap is refused to the Captain with the next step, before anyone spawns. Prose in a brief is never parsed or validated. -**Handoffs are files, not chat.** A seat whose persona declares an output artifact (a Critic's ReviewHandoff, a Scout's ContextBrief) owes it as the same shared handoff file every persona writes — `handoffs//-.md` in the project's artifacts (see the [persona contract](../agent-personas/index.md)) — and its first turn names that exact path. The handoff gate checks for the file when a run ends and reminds the seat once. File existence is evidence of delivery, not validation or acceptance of the result; the Captain reviews the handoff against the task. **Every seat tells its Captain when it finishes**, as measured facts: how the run ended first, the run's error when it failed, and whether its handoff was written, missing, or never declared, with the handoff inline when short. A first turn that fails is a launch outcome, told once by the launch report and not again as a finish; and a seat's failure that its Captain hears this way is not repeated by the silence detector as an unanswered ask. The notice is posted the first time a seat finishes and again only when those facts change; a finish that would repeat the last notice word for word is silent, because the seat's own reply already reached the Captain. Notices that arrive while the Captain's turn is running fold into the one seat notice already queued behind that turn, so the Captain absorbs its Crew's news in one turn rather than one per seat. Notice delivery retries on a later finish or the next server startup if dispatch fails; it is not guaranteed promptly while the seat stays idle. Sign-in and permission failures also raise a platform notice in the person’s Inbox, with replies routed to the Captain; other failures remain for the Captain to assess. A Captain normally does not have to interview its Crew to learn a seat is done, and it never waits for one inside a turn: `t3_thread_wait` is not on its surface, because a turn that blocks cannot receive the notice it is waiting for. The person opens every handoff on the Artifacts page. A message in chat is never a handoff. +**Communication is direct; handoffs record durable outputs.** Members exchange findings, questions, blockers, and results directly with one another and their Captain; Captains can coordinate with other Captains. Neither an artifact nor a human approval is required for these messages. Members send their final result with evidence and unresolved issues to the Captain. Active Astra runs accept peer steering; other providers queue messages until the next safe turn boundary. A seat whose persona declares an output artifact (a Critic's ReviewHandoff, a Scout's ContextBrief) owes it as the same shared handoff file every persona writes — `handoffs//-.md` in the project's artifacts (see the [persona contract](../agent-personas/index.md)) — and its first turn names that exact path. The handoff gate checks for the file when a run ends and reminds the seat once. File existence is evidence of delivery, not validation or acceptance of the result; the Captain reviews the handoff against the task. **Every seat tells its Captain when it finishes**, as measured facts: how the run ended first, the run's error when it failed, and whether its handoff was written, missing, or never declared, with the handoff inline when short. A first turn that fails is a launch outcome, told once by the launch report and not again as a finish; and a seat's failure that its Captain hears this way is not repeated by the silence detector as an unanswered ask. The notice is posted the first time a seat finishes and again only when those facts change; a finish that would repeat the last notice word for word is silent, because the seat's own reply already reached the Captain. Notices that arrive while the Captain's turn is running fold into the one seat notice already queued behind that turn, so the Captain absorbs its Crew's news in one turn rather than one per seat. Notice delivery retries on a later finish or the next server startup if dispatch fails; it is not guaranteed promptly while the seat stays idle. Sign-in and permission failures also raise a platform notice in the person’s Inbox, with replies routed to the Captain; other failures remain for the Captain to assess. A Captain normally does not have to interview its Crew to learn a seat is done, and it never waits for one inside a turn: `t3_thread_wait` is not on its surface, because a turn that blocks cannot receive the notice it is waiting for. The person opens every handoff on the Artifacts page. A direct result message does not replace a handoff artifact explicitly required by a saved persona. **Members report their finish.** When a member's run ends and it owes no open reply, the platform tells its Captain how the run ended and where the handoff stands; a later message runs normally. The platform settles no seat: upstream settles by hand, by merged PR, or by idle days, and a seat marked settled seconds after launch read as wrong (Jackson, 2026-09-17). **A Crew's state is its seats' states**, summarized from measured facts — how many seats are running, need a person, or were settled — on the Crew's row of the Fleet page and on the Captain's expander in the sidebar; the person never has to interview a Crew to learn where it stands. Members stay out of the thread sidebar unless pinned; the Captain's row carries the Captain's anchor mark and an expander into its members; the Fleet page lists every agent by Squadron and placement with each Crew under its Captain. Crews are a web and desktop surface; mobile has no Crew surface today, deliberately. @@ -45,7 +45,7 @@ The platform ships the machinery — propose, gate, spawn, render, remind, notif ### Launch and the gate -1. `/crew ` in any web or desktop thread sends the brief, wrapped in the platform's crew-composition guidance, as that thread's next turn; the thread keeps its own persona, model, and policy, no persona is required, and a command without a brief is refused before anything is sent. +1. Asking for a Crew in ordinary chat causes the current agent to propose a roster before doing the delegated work. The thread retains its persona, model, and policy; saved personas are optional and there is no Crew slash command. 2. A roster proposal renders inline above the Captain's composer; the person can remove seats, add personas from the library or a custom seat (Custom agent is the first choice; it takes a name and required instructions and runs with the Captain's model and access), then approve the final roster or decline. 3. The gate shows, for every seat, the saved definition or custom-seat choice, the access mode it will run with, and any seat instructions, before the person approves. 4. A seat naming an agent missing from or disabled in the library, a duplicate seat name, or a roster past twelve seats is refused to the Captain with the next step, before anything spawns; prose is never parsed. @@ -60,7 +60,7 @@ The platform ships the machinery — propose, gate, spawn, render, remind, notif ### Growing -10. A Captain adds a seat only through a request the person answers in the Inbox, where it also counts on the bell; approval appends the seat and bumps the snapshot version; either decision is posted to the Captain. +10. When a member raises a concern or needs help, the Captain can request another seat with a reason describing the gap and its assignment. The person answers the request in the Inbox, where it also counts on the bell; approval appends the seat and bumps the snapshot version; either decision is posted to the Captain. 11. A Crew holds at most twelve seats, counting open requests; a member's request is refused naming escalation to its Captain. ### Handoffs @@ -88,8 +88,8 @@ The platform ships the machinery — propose, gate, spawn, render, remind, notif ## Scenarios -- **A pull-request Crew.** In Billing Migration the user sends `/crew Land the invoice-export PR: build, review, sit on CI.` The Captain proposes Builder, Critic, and Sitter with a reason each; the user drops the Sitter, approves, and the two seats appear under the Captain with the brief and the roster. The Critic finishes and writes its ReviewHandoff into the project's artifacts; the Captain's thread receives the notice with the findings inline. (AC1, AC2, AC6, AC8, AC12, AC13) -- **Two Crews at once.** In Website Redesign the user, mid-conversation with the agent that has been planning the launch, sends `/crew Migrate the blog to the new CMS` and then `/crew Rewrite the pricing page copy`. The same thread proposes two rosters, each answered above its composer; both appear under the Captain's row as named groups, Blog Migration and Pricing Copy, each with its own toggle, state, and Stop. (AC1, AC2, AC15) +- **A pull-request Crew.** In Billing Migration the user sends "Put together a crew to land the invoice-export PR: build, review, sit on CI." The Captain proposes Builder, Critic, and Sitter with a reason each; the user drops the Sitter, approves, and the two seats appear under the Captain with the brief and the roster. The Critic finishes and writes its ReviewHandoff into the project's artifacts; the Captain's thread receives the notice with the findings inline. (AC1, AC2, AC6, AC8, AC12, AC13) +- **Two Crews at once.** In Website Redesign the user, mid-conversation with the agent that has been planning the launch, sends "Create a crew to migrate the blog to the new CMS" and then "Create another crew to rewrite the pricing page copy". The same thread proposes two rosters, each answered above its composer; both appear under the Captain's row as named groups, Blog Migration and Pricing Copy, each with its own toggle, state, and Stop. (AC1, AC2, AC15) - **More hands.** The Captain judges the CI failures need a Sitter and requests one; the request appears in the Inbox and on the bell, the user approves, and the seat joins the roster at version 2. The Builder tries to request a helper itself and is refused, naming escalation to the Captain. (AC10, AC11) - **A flaky launch.** The provider is unavailable when the user approves an added seat. The gate reopens; the user approves again and the seat spawns onto its reserved place instead of being refused as taken. (AC7) - **Stop everything.** The Builder is heading down a path the person recognizes as wrong while the Critic is mid-review. The person clicks Stop crew on the Crew's Fleet header; both running turns end, the seats stay on the roster, and the Captain briefs them again. (AC21) diff --git a/docs/user/personas.md b/docs/user/personas.md index df57cfacae4a9..ed9cee5f2396e 100644 --- a/docs/user/personas.md +++ b/docs/user/personas.md @@ -49,19 +49,17 @@ An agent can also start a persona as a peer with its own top-level task by namin ## Crews -A crew is a group of agents that one agent, the Captain, runs as a unit for one piece of work. You do not define crews in advance, and there is no special Captain agent: any thread can compose a crew. On web or desktop, in a new or existing thread, type `/` and pick **/crew** from the menu (or type it), then add what the crew should accomplish, for example: +A crew is a group of agents that one agent, the Captain, runs as a unit for one piece of work. Ask the agent in your current conversation to put a crew together, using ordinary chat. For example: -```text -/crew Follow @docs/runbooks/release.md for the 2.4 release and report back when the PR is green. -``` +> Put together a crew to follow @docs/runbooks/release.md for the 2.4 release and report back when the PR is green. -The thread's agent, whichever persona or model it is running as, receives your brief together with instructions to compose a crew rather than do the work itself. It reads the persona library and proposes a roster: one seat per persona with a reason. The proposal appears above the composer in that thread, like a planning question. Remove seats you do not want, or add a seat: pick **Custom seat** and give it a name and instructions for a seat with no persona behind it (instructions are required; it runs on the Captain's provider and model with the Captain's access mode, not a persona's sandbox), or pick a persona from the library and add instructions if it needs them. Then **Approve** or **Decline**. Approval starts every seat as a peer under the Captain with your brief and the roster, and the Captain is told the decision in its thread, where it shows as a card listing the crew and its seats; click a seat to open its thread. Your `/crew` message shows as the brief you wrote, with the instructions the command sent alongside it one click away. Seats run with their own persona's permissions once you approve them, so a read-only Captain can command agents that write. +The agent uses the conversation to propose a roster above the composer. Each member has a responsibility, a reason for joining, and instructions. Members may use saved personas or be custom agents created for this task. You can remove members or add a custom seat with a name and instructions, then approve or decline. Custom members use the Captain's provider, model, and effective access mode; saved personas use their own configured runtime policy. No special Captain persona is needed, and one Captain can coordinate several crews in the same conversation. -One Captain can run several crews. Send `/crew` again in the same thread when there is separate work to hand off, and each proposal gets its own named crew; the Captain can also propose another crew on its own when the work splits. +Members can talk directly to each other and their Captain, and Captains can coordinate with other Captains. They share findings, questions, blockers, and results as messages while work continues; creating a report is not a prerequisite for communication. Active Astra runs can receive peer messages during work; other providers receive queued messages at the next safe turn boundary. -While the crew works, the Captain may ask for one more seat. That request lands under **Crew requests** in the Inbox and counts on the inbox bell; approve or decline it there. A crew holds at most twelve seats. Crew members cannot request seats themselves. +When a member needs help, it tells the Captain. The Captain can request another member, explaining the gap and the proposed assignment. The request appears under **Crew requests** in the Inbox and counts on the inbox bell. Approve or decline it there; independent work can continue while the request awaits your decision. A crew holds at most twelve seats, and only its Captain can request additions. -When a seat's persona is defined to return a report, such as a review handoff, it writes that report as a handoff artifact under `handoffs/` in the project's Artifacts page, exactly as the persona would when running on its own (see **Handoff artifacts** above). When a seat first finishes, and again whenever its result or report changes, the Captain gets a notice in its thread, shown as a card with the seat's result and its report, which opens in the artifacts panel and can be read inline when it is short; notices that arrive while the Captain is mid-turn are combined into one card. A seat that finishes without writing its report is reminded once, and the notice says the report is missing. Reports are never left in chat. +Every member is instructed to send its result, evidence, and unresolved issues to the Captain. A saved persona that explicitly declares a handoff artifact still owes that report in the project's Artifacts page, but it can share findings before the report is ready. Completion notices tell the Captain how the member's run ended and whether a declared report was written, missing, or never required. A missing declared report prompts one reminder; custom members do not need a handoff file unless their assignment asks for one. Crews are a web and desktop feature; the mobile app shows crew members as ordinary agents and has no roster gate. Members stay out of the thread list so it shows the conversations you started; the Captain's row shows an anchor mark (hover it for the crews it commands) and, for each crew it runs, a toggle named for that crew that opens into its members with seat, status, and last activity; agents it started outside a crew get a toggle of their own. When a member finishes and owes no reply, the Captain is told how it ended; the member's thread settles under the same rules as any other thread. **Fleet**, from the icon beside the inbox bell, shows every agent by Squadron with each crew under its Captain.