Skip to content

feat(j5): add join_squadron so a native thread can establish its Squadron home - #131

Merged
Jacksondr5 merged 3 commits into
j5/mainfrom
j5/join-squadron-tool
Sep 12, 2026
Merged

Jacksondr5 merged 3 commits into
j5/mainfrom
j5/join-squadron-tool

Conversation

@Jacksondr5

Copy link
Copy Markdown
Owner

Closes #129.

Problem

A thread created without a Squadron (the mobile creation gap, #128) has no supported way to become an A2A participant. list_participants and send_message fail closed with A2ASenderNotJoinedError, and the home registrar only runs from creation paths. Thread Merge 2 is stuck in exactly this state.

Fix

  • join_squadron MCP tool. Acts on the authenticated calling thread. Takes an explicit squadron_id (never inferred from folder or name) plus optional client_request_id, and returns the committed squadron, participant, thread ids and placement.
  • list_squadrons MCP 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 existing A2AHomeRegistrationTransaction and ParticipantPlacementTransactionWriter in 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 one participant.joined. Placement is unknown / native_or_unobserved at the Squadron root, with no invented spawner or fork parent.
  • Refusals, each naming state and next command: archived or deleted thread, a different existing home (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.
  • The "no registered home" error messages now point at list_squadrons and join_squadron.
  • Docs: verb contract entries in 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. Only j5/ paths and docs/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 by list_participants, lists squadrons, is refused on the wrong project, joins, replays with and without a request id, appears as self, and is refused on a move attempt. Archived caller refused before any write.
  • Updated handlers.test.ts, registration.test.ts, SendService.test.ts.
  • tsgo --noEmit for apps/server clean, targeted vp lint clean, 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

…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>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 effective changed lines (test files excluded in mixed PRs). labels Sep 12, 2026
Comment thread apps/server/src/j5/a2a/mcp/tools.ts Outdated
"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.";

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.";

Comment thread apps/server/src/j5/a2a/mcp/tools.ts Outdated
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.";

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.";

Jacksondr5 and others added 2 commits September 12, 2026 16:16
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>
@Jacksondr5
Jacksondr5 marked this pull request as ready for review September 12, 2026 16:54
@Jacksondr5
Jacksondr5 merged commit f9da2c6 into j5/main Sep 12, 2026
20 checks passed
@Jacksondr5
Jacksondr5 deleted the j5/join-squadron-tool branch September 12, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(j5): add a tool to join an existing native thread to a Squadron

1 participant