Repository navigation
feat(j5): add join_squadron so a native thread can establish its Squadron home - #131
Merged
Merged
Conversation
…dron home A thread created without a Squadron (the mobile creation gap) has no way to become an A2A participant: list_participants and send_message fail closed and the registrar only runs from creation paths. Add a SquadronJoinService that composes the existing home registrar and placement writer in one transaction under the ledger append permit, expose it as the join_squadron MCP tool acting on the authenticated caller, and add a read-only list_squadrons directory so an unregistered caller can obtain the exact squadron_id. Joining records unknown-provenance root placement, keeps the thread and its running work untouched, replays harmlessly, and refuses conflicting homes, retired identities, archived threads, and project mismatches. Built by Claude Fable 5.1 in the Claude Code harness. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Jacksondr5
commented
Sep 12, 2026
| "Join the Squadron the human selected for you when your thread has no Squadron home yet — the state where list_participants and send_message refuse you. Pass the exact squadron_id, taken from list_squadrons or from the human; that Squadron must reference your thread's project. Your thread, conversation, worktree, and running work stay exactly as they are — only your participant registration is added, placed at the Squadron root with no invented spawner. Calling it again for the Squadron you already belong to returns your existing registration; a thread that already has a different home is refused, because this is not a move. Reuse client_request_id to retry safely."; | ||
|
|
||
| export const J5_LIST_SQUADRONS_DESCRIPTION = | ||
| "The Squadron directory for this environment: every Squadron's squadron_id, name, and the project ids it references, plus your own thread's project id so you can see which Squadron can home you. Use it to obtain the exact squadron_id before join_squadron; it works even when you have no Squadron home yet. Read-only."; |
Owner
Author
There was a problem hiding this comment.
Suggested change
| "The Squadron directory for this environment: every Squadron's squadron_id, name, and the project ids it references, plus your own thread's project id so you can see which Squadron can home you. Use it to obtain the exact squadron_id before join_squadron; it works even when you have no Squadron home yet. Read-only."; | |
| "The Squadron directory for this environment: every Squadron's squadron_id, name, and the project ids it references, plus your own thread's project id so you can see which Squadron can home you. Use it to obtain the exact squadron_id before join_squadron. Read-only."; |
| export type J5ListSquadronsResult = typeof J5ListSquadronsResult.Type; | ||
|
|
||
| export const J5_JOIN_SQUADRON_DESCRIPTION = | ||
| "Join the Squadron the human selected for you when your thread has no Squadron home yet — the state where list_participants and send_message refuse you. Pass the exact squadron_id, taken from list_squadrons or from the human; that Squadron must reference your thread's project. Your thread, conversation, worktree, and running work stay exactly as they are — only your participant registration is added, placed at the Squadron root with no invented spawner. Calling it again for the Squadron you already belong to returns your existing registration; a thread that already has a different home is refused, because this is not a move. Reuse client_request_id to retry safely."; |
Owner
Author
There was a problem hiding this comment.
Suggested change
| "Join the Squadron the human selected for you when your thread has no Squadron home yet — the state where list_participants and send_message refuse you. Pass the exact squadron_id, taken from list_squadrons or from the human; that Squadron must reference your thread's project. Your thread, conversation, worktree, and running work stay exactly as they are — only your participant registration is added, placed at the Squadron root with no invented spawner. Calling it again for the Squadron you already belong to returns your existing registration; a thread that already has a different home is refused, because this is not a move. Reuse client_request_id to retry safely."; | |
| "Join a Squadron when your thread has no Squadron home yet. Pass the exact squadron_id, taken from list_squadrons; that Squadron must reference your thread's project. Your thread, conversation, worktree, and running work stay exactly as they are. Calling it again for the Squadron you already belong to returns your existing registration. Reuse client_request_id to retry safely. Warning: you cannot switch Squadrons once you're assigned, be sure you're joining the right one."; |
Jackson's review shortened both tool descriptions and added the warning that a Squadron cannot be switched once assigned. The contract doc mirrors the same strings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tools The two tests that pin the complete MCP tool surface did not include join_squadron and list_squadrons. The worktree registration proof is the FORK.md case 4 gate of record for the J5 surface, so this is its sanctioned appended case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #129.
Problem
A thread created without a Squadron (the mobile creation gap, #128) has no supported way to become an A2A participant.
list_participantsandsend_messagefail closed withA2ASenderNotJoinedError, and the home registrar only runs from creation paths. Thread Merge 2 is stuck in exactly this state.Fix
join_squadronMCP tool. Acts on the authenticated calling thread. Takes an explicitsquadron_id(never inferred from folder or name) plus optionalclient_request_id, and returns the committed squadron, participant, thread ids and placement.list_squadronsMCP tool. Read-only directory of every Squadron with its project ids, plus the caller's own project id, callable by unregistered threads. The existing HTTP directory needs a session token an agent doesn't have, so this is the supported way to obtain the exact id.SquadronJoinService. Composes the existingA2AHomeRegistrationTransactionandParticipantPlacementTransactionWriterin one transaction under the ledger append permit. The home is resolved inside that permit, so a replay with the same request id and concurrent retries with different ids both commit exactly oneparticipant.joined. Placement isunknown/native_or_unobservedat the Squadron root, with no invented spawner or fork parent.A2AHomeConflictError), retired identity, and a Squadron that doesn't reference exactly the thread's project. A thread already homed in the requested Squadron gets its existing registration back.list_squadronsandjoin_squadron.agent-tools.md, the substrate verb list, the A2A README, and a dated amendment to the Squadron ruling "there is no join, no leave, no move" narrowing it to this one repair case. Onlyj5/paths anddocs/j5/were touched.Testing
SquadronJoinService.test.ts(real in-memory SQLite): fresh join, same-id replay, different-id retry, three concurrent joins, launch-homed thread gaining placement, project mismatch, missing squadron, home conflict, retired identity.mcp/joinSquadron.test.ts(real ledger/registrar/placement layers + toolkit dispatch): a previously unregistered caller is refused bylist_participants, lists squadrons, is refused on the wrong project, joins, replays with and without a request id, appears asself, and is refused on a move attempt. Archived caller refused before any write.handlers.test.ts,registration.test.ts,SendService.test.ts.tsgo --noEmitforapps/serverclean, targetedvp lintclean, 50 tests passing across 8 J5 files. Repo-wide checks left to CI.Built by Claude Fable 5.1 in the Claude Code harness.
🤖 Generated with Claude Code