talking-stick 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -50
- package/dist/cli/install-commands.js +76 -36
- package/dist/cli/output.js +2 -2
- package/dist/cli/registry.js +13 -32
- package/dist/cli/room-commands.js +1 -1
- package/dist/cli/startup-maintenance.js +27 -1
- package/dist/cli.js +2 -2
- package/dist/config.js +2 -2
- package/dist/identity.js +4 -4
- package/dist/index.js +2 -2
- package/dist/install-audit.js +21 -0
- package/dist/install-migration.js +84 -0
- package/dist/install.js +0 -69
- package/dist/update-migration.js +135 -0
- package/docs/plans/2026-05-04-diff-walker-design.md +585 -0
- package/docs/plans/2026-05-05-cli-only-coordination.md +224 -0
- package/docs/plans/out-of-band-signaling-implementation.md +5 -5
- package/docs/receive-consumer-contract.md +8 -6
- package/docs/releases/0.3.0.md +77 -0
- package/docs/talking-stick-plan.md +3 -2
- package/package.json +4 -3
- package/scripts/postinstall-mcp-cleanup.cjs +25 -0
- package/skills/talking-stick/SKILL.md +124 -103
- package/dist/mcp-server.js +0 -244
- package/dist/server.js +0 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: talking-stick
|
|
3
|
-
description: Use when working in a repo that coordinates multiple agent harnesses with Talking Stick (`tt` / `talking-stick`), or when the user asks you to avoid parallel work, wait your turn, pass structured handoffs, or coordinate with Claude, Codex, Gemini, or OpenCode in the same workspace. Also use when a workspace contains a `.talking-stick/` marker
|
|
3
|
+
description: Use when working in a repo that coordinates multiple agent harnesses with Talking Stick (`tt` / `talking-stick`), or when the user asks you to avoid parallel work, wait your turn, pass structured handoffs, or coordinate with Claude, Codex, Gemini, or OpenCode in the same workspace. Also use when a workspace contains a `.talking-stick/` marker.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
This skill teaches a harness how to behave in a Talking Stick workspace.
|
|
@@ -18,48 +18,67 @@ Use this skill when any of these are true:
|
|
|
18
18
|
- the user mentions `talking-stick`, `tt`, handoffs, turn-taking, or avoiding parallel work
|
|
19
19
|
- the repo is known to use Talking Stick coordination
|
|
20
20
|
- a `.talking-stick/` marker exists
|
|
21
|
-
- the Talking Stick MCP tools are available in the current harness
|
|
22
21
|
|
|
23
22
|
Do not use this skill for ordinary single-agent work in repos that are not using Talking Stick.
|
|
24
23
|
|
|
25
24
|
## Workflow
|
|
26
25
|
|
|
27
|
-
### 1.
|
|
26
|
+
### 1. Use The CLI
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
Use the `tt` CLI for all Talking Stick coordination. Do not use old Talking Stick MCP tools for repo coordination, even if an older install exposes them; the CLI is the source of truth. Current updates should remove stale Talking Stick MCP registrations automatically.
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
Useful commands:
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
- `tt whoami --json`
|
|
33
|
+
- `tt join --json`
|
|
34
|
+
- `tt wait --json`
|
|
35
|
+
- `tt try --json`
|
|
36
|
+
- `tt state --json`
|
|
37
|
+
- `tt events --after N --target any --json`
|
|
38
|
+
- `tt notes add "..." --json`
|
|
39
|
+
- `tt notes list --json`
|
|
40
|
+
- `tt events --follow --json`
|
|
41
|
+
- `tt msg send <recipient|room> "..." --json`
|
|
42
|
+
- `tt msg recv --follow --json` (messages-only fallback when an event-stream consumer is too broad)
|
|
43
|
+
- `tt release --stdin`
|
|
44
|
+
- `tt assign <agent_id|next> --stdin`
|
|
45
|
+
- `tt take --reason "..." --json`
|
|
46
|
+
|
|
47
|
+
Some workspaces may also have sibling receive processes running `tt events --follow`, `tt msg recv --wait`, or `tt msg recv --follow`; leave them alone unless the operator explicitly asks you to stop or restart them.
|
|
48
|
+
|
|
49
|
+
If coordination is required and `tt` is unavailable, say so briefly and ask the user whether they want to install or enable Talking Stick first. Do not pretend coordination is active.
|
|
34
50
|
|
|
35
51
|
Human CLI runs silently keep already-installed Claude Code, Codex, and OpenCode skill copies/symlinks aligned with the bundled Talking Stick skill. This is best effort and only updates existing installs; Gemini skills are registry-managed and should be refreshed with `tt install gemini` when needed.
|
|
36
52
|
|
|
37
|
-
### 2. Join
|
|
53
|
+
### 2. Join The Workspace Room Once
|
|
38
54
|
|
|
39
|
-
On the first substantial task in a Talking Stick workspace:
|
|
55
|
+
On the first substantial task in a Talking Stick workspace, run:
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
```sh
|
|
58
|
+
tt join --json
|
|
59
|
+
```
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
Keep the returned room id and canonical path in mind. The current working directory is the implicit path for normal commands; pass an explicit path only when coordinating a different directory or intentionally selecting a nested room.
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
Right after joining, start a background ambient receiver so direct messages and turn passes/reservations surface as soon as they happen instead of waiting for the next time you poll:
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
```sh
|
|
66
|
+
tt events --follow --json
|
|
67
|
+
```
|
|
50
68
|
|
|
51
|
-
|
|
69
|
+
For `tt events --wait` and `tt events --follow`, the default target is `self`; add `--target any` only for audit/debug views. If your harness can stream a child process's stdout into the model's context (Claude Code's Monitor, Codex `attach`-style), this is enough — each line becomes an event you see mid-task. If your harness can only notice that a backgrounded command exits, use the polling fallback in §4.5. Without an ambient receiver, neither messages nor turn handoffs reach you between deliberate `tt wait` / `tt events` calls.
|
|
52
70
|
|
|
53
|
-
|
|
71
|
+
The ambient receiver is not a turn claimant. It never grants the stick and never starts the lease guardian. Keep using `tt wait --json` for ownership.
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
### 3. Wait Before Shared Work
|
|
74
|
+
|
|
75
|
+
Before making shared edits or running owner-style actions, run:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
tt wait --json
|
|
60
79
|
```
|
|
61
80
|
|
|
62
|
-
|
|
81
|
+
The default wait timeout is `110s`, which is the normal active-coordination setting. If your harness has a shorter tool timeout, override with the longest safe value and immediately wait again when it returns without granting the turn. Do not busy-loop with short waits.
|
|
63
82
|
|
|
64
83
|
Possible outcomes:
|
|
65
84
|
|
|
@@ -68,151 +87,153 @@ Possible outcomes:
|
|
|
68
87
|
- `takeover_available`: surface the reason and make takeover explicit
|
|
69
88
|
- `closed`: stop and explain that the room is closed
|
|
70
89
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
**Prefer to run the wait in the background.** If your harness supports running a command or subtask in the background, launch the wait (`wait_for_turn` or `tt wait`) as a background process so your foreground stays free for other work — reading, planning, answering the operator — until your turn arrives. Blocking the whole harness on the wait defeats the point.
|
|
74
|
-
|
|
75
|
-
**Prefer wait cycles over scheduled wakeups.** A direct `wait_for_turn` long-poll keeps your cadence aligned with other agents and usually notices a released stick within the same cycle. Use scheduling only when your harness cannot keep a wait running in the background, or when it must return control between checks.
|
|
76
|
-
|
|
77
|
-
Wakeup pattern:
|
|
90
|
+
A successful `tt wait` or `tt take` starts an internal `tt guard` lease guardian and returns `guardian_pid` in JSON. Verify the field is present and the pid is alive before you start a long edit; the guardian is what keeps your lease from expiring after the foreground `tt wait` process exits. If `guardian_pid` is missing or the pid is gone, stop, run `tt wait` again to repair the guardian (it will detect the existing ownership and respawn the guardian), and only then continue. Do not kill that guardian.
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
2. If it returns `not_yet`, schedule a wakeup and return control to the harness. Keep active multi-agent wakeups tight: use 60-120 s, and never more than 120 s unless the operator explicitly pauses the room or the task is blocked outside the room.
|
|
81
|
-
3. On wakeup, repeat from step 1.
|
|
92
|
+
### 4. While Waiting
|
|
82
93
|
|
|
83
|
-
|
|
94
|
+
Prefer to run `tt wait` in the background if your harness supports background commands. That keeps the foreground free for reading, planning, answering the operator, and watching OOB messages until your turn arrives.
|
|
84
95
|
|
|
85
|
-
|
|
96
|
+
Prefer wait cycles over scheduled wakeups. A direct long-poll stays aligned with other agents and usually notices a released stick within the same cycle. Use scheduled wakeups only when your harness cannot keep a wait running in the background.
|
|
86
97
|
|
|
87
|
-
|
|
98
|
+
Do not replace `tt wait` with an event receiver. `tt events --wait` is only a wake channel for messages and handoff/reservation events. If it exits with a pass, release, assignment, or message, process the event, then run or continue `tt wait --json`; do not touch shared files unless that wait returns `your_turn` and a live `guardian_pid`.
|
|
88
99
|
|
|
89
100
|
If you do not have the stick:
|
|
90
101
|
|
|
91
102
|
- do not make shared repo changes
|
|
92
103
|
- do not silently race another harness
|
|
93
|
-
- it is fine to read, plan, review, or help the user think
|
|
104
|
+
- it is fine to read, plan, review, or help the user think
|
|
94
105
|
- tell the user who currently holds or is reserved the turn when that is useful
|
|
95
106
|
|
|
96
|
-
The wait is for
|
|
107
|
+
The wait is for active non-mutating work, not idle sleep. Re-read the holder's last handoff, follow up on its `artifacts[]`, investigate the area they are touching, and rethink the plan from your own angle. If you find something the holder should know, leave a durable note:
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
```sh
|
|
110
|
+
tt notes add "Finding or pointer for the current/next holder." --json
|
|
111
|
+
```
|
|
101
112
|
|
|
102
|
-
|
|
113
|
+
Room inspection exists to answer specific questions, not to poll. Do not run `tt state` after a routine `tt wait`; the wait result already says who owns or is reserved for the turn. Use `tt state`, `tt events --target any`, and `tt notes list` sparingly when the wait result is insufficient or you are debugging stale members, takeover, or history.
|
|
103
114
|
|
|
104
|
-
|
|
115
|
+
When you do take the stick, first read the attached handoff and load any useful `artifacts[]`, then run `tt notes list --json` once so you see what other members left for you.
|
|
105
116
|
|
|
106
|
-
|
|
117
|
+
### 4.5 Out-Of-Band Messaging
|
|
107
118
|
|
|
108
|
-
|
|
119
|
+
The talking stick guarantees single-writer authority over shared workspace state. It is not a chat protocol. For transient signaling, use messages.
|
|
109
120
|
|
|
110
|
-
|
|
111
|
-
- **Note** (`tt notes add`) when the artifact should outlive the moment: a finding the next holder should consider at handoff, or an observation that survives process churn.
|
|
112
|
-
- **Handoff** (release/pass with a structured payload) when transferring work. Messages do not replace handoffs; they live alongside them.
|
|
121
|
+
Send:
|
|
113
122
|
|
|
114
|
-
|
|
123
|
+
```sh
|
|
124
|
+
tt msg send <recipient|room> "message body" --json
|
|
125
|
+
```
|
|
115
126
|
|
|
116
|
-
|
|
127
|
+
Recipient is a full `agent_id`, an unambiguous active display name, or the literal `room` for broadcast. `--interrupt` marks the message as time-sensitive; the receiver decides whether to act on it now.
|
|
117
128
|
|
|
118
|
-
|
|
129
|
+
Receive with the mode your harness can observe. The recommended primary path is the unified event stream you started in §2:
|
|
119
130
|
|
|
120
|
-
|
|
131
|
+
```sh
|
|
132
|
+
tt events --follow --json
|
|
133
|
+
```
|
|
121
134
|
|
|
122
|
-
If the
|
|
135
|
+
That streams direct messages, broadcasts, and turn passes/reservations for you as a single ordered feed — one JSON event per line. Use it whenever your harness can stream a child process's stdout into the model's context. If the harness can only notice that a backgrounded command exits, use the polling fallbacks:
|
|
123
136
|
|
|
124
|
-
|
|
137
|
+
```sh
|
|
138
|
+
tt events --wait --after <last_event_seq> --json # all event types
|
|
139
|
+
tt msg recv --wait --after <last_event_seq> --json # messages only
|
|
140
|
+
```
|
|
125
141
|
|
|
126
|
-
|
|
142
|
+
Restart with the returned cursor to resume. `tt msg recv --follow` still exists for harnesses that want a messages-only feed, but the event stream is preferred because turn handoffs use the same channel and a messages-only consumer silently misses them.
|
|
127
143
|
|
|
128
|
-
|
|
144
|
+
For Codex-style harnesses that cannot consume a continuous stdout stream, the safe loop is: keep `tt wait --json` as the ownership wait, and separately run `tt events --wait --after <last_event_seq> --json` as a short-lived wake process. An event wake can tell you to read, reply, or retry `tt wait`; it is never permission to edit.
|
|
129
145
|
|
|
130
|
-
|
|
146
|
+
Messages are public room events. Any room member can read them with `tt events --target any`. `to_agent_id` is routing, not an ACL.
|
|
131
147
|
|
|
132
|
-
-
|
|
133
|
-
- do not silently take over just because it is possible
|
|
134
|
-
- if takeover is chosen, call `takeover_stick`
|
|
135
|
-
- after takeover, call `get_room_events` so you can reconstruct the last handoff before touching code
|
|
148
|
+
Messages do not grant the stick. A non-holder paging the holder does not gain write authority. Keep waiting for your turn; messages are only a side channel.
|
|
136
149
|
|
|
137
|
-
|
|
150
|
+
### 5. While Holding The Stick
|
|
138
151
|
|
|
139
|
-
|
|
152
|
+
Holding the stick is for active work. The moment you stop actively editing, reasoning through edits, or asking the operator a blocking question, release or assign the turn. Do not idle-hold the room while waiting on long verification, non-blocking operator input, CI, or any other pause where another harness could make progress.
|
|
140
153
|
|
|
141
|
-
|
|
154
|
+
The `tt guard` process spawned by `tt wait` keeps the lease alive during active work. Later owner commands such as `tt release`, `tt assign`, and `tt take` must run under the same harness identity. If identity is ambiguous, use the exact active id with `TT_HARNESS_AGENT_ID=<agent_id>`.
|
|
142
155
|
|
|
143
|
-
|
|
156
|
+
### 6. Takeover Is Explicit
|
|
144
157
|
|
|
145
|
-
|
|
158
|
+
If `tt wait` reports `takeover_available`:
|
|
146
159
|
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
|
|
151
|
-
Otherwise release. Ping-ponging `pass_stick` between two agents is an antipattern because it can lock humans out of their own room.
|
|
152
|
-
|
|
153
|
-
Always include a non-empty handoff.
|
|
160
|
+
- explain why takeover is available (`owner_timeout`, `owner_gone`, `claim_timeout`, `recipient_gone`)
|
|
161
|
+
- do not silently take over just because it is possible
|
|
162
|
+
- if takeover is chosen, run `tt take --reason "..." --json`
|
|
163
|
+
- after takeover, run `tt events --target any --json` so you can reconstruct the last handoff before touching code
|
|
154
164
|
|
|
155
|
-
|
|
165
|
+
If the operator explicitly tells you to take over despite a reservation or live owner, use:
|
|
156
166
|
|
|
157
|
-
|
|
167
|
+
```sh
|
|
168
|
+
tt take --operator-requested --reason "operator requested takeover" --json
|
|
169
|
+
```
|
|
158
170
|
|
|
159
|
-
|
|
160
|
-
- `next_action`: the concrete next step for the next owner
|
|
171
|
+
Do not invent this override yourself; it is for direct operator intervention.
|
|
161
172
|
|
|
162
|
-
|
|
173
|
+
### 7. Finish With A Real Handoff
|
|
163
174
|
|
|
164
|
-
|
|
175
|
+
When you are done with your turn, default to releasing:
|
|
165
176
|
|
|
166
|
-
```
|
|
177
|
+
```sh
|
|
178
|
+
tt release --stdin <<'JSON'
|
|
167
179
|
{
|
|
168
|
-
"status": "
|
|
169
|
-
"next_action": "
|
|
180
|
+
"status": "Updated the CLI-only coordination plan and the bundled skill so harnesses use tt subprocesses for join, wait, OOB messaging, notes, and handoffs.",
|
|
181
|
+
"next_action": "Review the plan and then start the code-removal pass.",
|
|
170
182
|
"artifacts": [
|
|
171
183
|
{
|
|
172
|
-
"path": "
|
|
184
|
+
"path": "docs/plans/2026-05-05-cli-only-coordination.md",
|
|
173
185
|
"role": "review",
|
|
174
|
-
"note": "
|
|
186
|
+
"note": "CLI-only migration plan."
|
|
175
187
|
}
|
|
176
|
-
],
|
|
177
|
-
"open_questions": [
|
|
178
|
-
"Should tt install default to copy or link for local development?"
|
|
179
188
|
]
|
|
180
189
|
}
|
|
190
|
+
JSON
|
|
181
191
|
```
|
|
182
192
|
|
|
183
|
-
|
|
193
|
+
Use `tt assign <agent_id> . --stdin` only when a specific named member must go next:
|
|
194
|
+
|
|
195
|
+
- they have unique context the next step requires
|
|
196
|
+
- they hold a credential or capability others lack
|
|
197
|
+
- the operator explicitly addressed the work to them
|
|
198
|
+
|
|
199
|
+
Otherwise release. Pinning turns between two agents is an antipattern because it can lock humans out of their own room.
|
|
200
|
+
|
|
201
|
+
Always include a non-empty handoff. Keep it tight: aim for roughly 150-300 words of `status`; reference commits by SHA instead of restating diffs, and use `artifacts[]` with path and role instead of pasting code.
|
|
184
202
|
|
|
185
|
-
|
|
203
|
+
Minimum handoff quality:
|
|
186
204
|
|
|
187
|
-
|
|
205
|
+
- `status`: what you finished, what changed, and what remains true
|
|
206
|
+
- `next_action`: the concrete next step for the next owner
|
|
207
|
+
|
|
208
|
+
Add `artifacts`, `open_questions`, and `do_not` when they will save the next harness real time or prevent rework.
|
|
188
209
|
|
|
189
|
-
|
|
210
|
+
### 8. After Release, Stay In The Loop
|
|
190
211
|
|
|
191
|
-
|
|
212
|
+
The default after `tt release` or `tt assign` is to re-enter the wait loop and keep waiting until your next turn arrives. Do not stop and ask the operator whether they want you back in the loop. Do not treat a handoff as end-of-session.
|
|
192
213
|
|
|
193
214
|
Exit the wait loop only when one of these is true:
|
|
194
215
|
|
|
195
|
-
- the shared task is explicitly finished
|
|
216
|
+
- the shared task is explicitly finished
|
|
196
217
|
- you are the only active member and there is no one to hand off to
|
|
197
|
-
- the operator gives a direct redirect or stop
|
|
218
|
+
- the operator gives a direct redirect or stop
|
|
198
219
|
|
|
199
|
-
In every other case
|
|
220
|
+
In every other case, after `tt release` or `tt assign`, go straight back into `tt wait --json`.
|
|
200
221
|
|
|
201
|
-
If the operator tells you to drop out of coordination,
|
|
222
|
+
If the operator tells you to drop out of coordination, run `tt leave --json`. Rooms with no active members are deleted instead of kept as history, and long-idle rooms may be purged on later invocations.
|
|
202
223
|
|
|
203
|
-
If the room state shows ghost members from past sessions whose processes are gone
|
|
224
|
+
If the room state shows ghost members from past sessions whose processes are gone, run `tt kick <agent_id> --json` to evict them. Use `--force` only when the operator explicitly tells you to remove a still-active member.
|
|
204
225
|
|
|
205
|
-
## Recovery
|
|
226
|
+
## Recovery And Inspection
|
|
206
227
|
|
|
207
228
|
Use these reads when you need context:
|
|
208
229
|
|
|
209
|
-
- `
|
|
210
|
-
- `
|
|
211
|
-
- `
|
|
212
|
-
- `
|
|
213
|
-
- `
|
|
230
|
+
- `tt list --json`: discover active rooms under the current path
|
|
231
|
+
- `tt state --json`: authoritative current room projection
|
|
232
|
+
- `tt events --target any --json`: replay recent claims, releases, assignments, messages, and takeovers
|
|
233
|
+
- `tt notes list --json`: list durable notes
|
|
234
|
+
- `tt whoami --explain`: inspect identity resolution
|
|
214
235
|
|
|
215
|
-
Prefer `
|
|
236
|
+
Prefer `tt state` over guessing from local memory when ownership may have changed and you are not already looking at a fresh `tt wait` result.
|
|
216
237
|
|
|
217
238
|
## Behavior Priorities
|
|
218
239
|
|
package/dist/mcp-server.js
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { isProtocolError } from "./errors.js";
|
|
5
|
-
import { deriveMcpHarnessIdentity } from "./identity.js";
|
|
6
|
-
import { createSystemProcessInspector } from "./process-utils.js";
|
|
7
|
-
import { TalkingStickCommands } from "./commands.js";
|
|
8
|
-
import { TalkingStickService } from "./service.js";
|
|
9
|
-
const handoffSchema = z
|
|
10
|
-
.object({
|
|
11
|
-
status: z.string(),
|
|
12
|
-
next_action: z.string(),
|
|
13
|
-
artifacts: z
|
|
14
|
-
.array(z.object({
|
|
15
|
-
path: z.string(),
|
|
16
|
-
lines: z.array(z.number().int()).length(2).optional(),
|
|
17
|
-
role: z.enum(["examine", "review", "edit", "context", "output"]),
|
|
18
|
-
note: z.string().optional()
|
|
19
|
-
}))
|
|
20
|
-
.optional(),
|
|
21
|
-
open_questions: z.array(z.string()).optional(),
|
|
22
|
-
do_not: z.array(z.string()).optional()
|
|
23
|
-
})
|
|
24
|
-
.passthrough();
|
|
25
|
-
export function createMcpServer(service = new TalkingStickService()) {
|
|
26
|
-
const commands = new TalkingStickCommands(service);
|
|
27
|
-
const resolveConnectionIdentity = createConnectionIdentityResolver();
|
|
28
|
-
const server = new McpServer({
|
|
29
|
-
name: "talking-stick",
|
|
30
|
-
version: "0.1.2"
|
|
31
|
-
});
|
|
32
|
-
server.registerTool("list_rooms", {
|
|
33
|
-
title: "List Rooms",
|
|
34
|
-
description: "List talking-stick rooms, optionally scoped to a path.",
|
|
35
|
-
inputSchema: {
|
|
36
|
-
context_path: z.string().optional()
|
|
37
|
-
}
|
|
38
|
-
}, async (input) => toolJson(() => service.listRooms(input)));
|
|
39
|
-
server.registerTool("join_path", {
|
|
40
|
-
title: "Join Path",
|
|
41
|
-
description: "Join the room resolved from an invocation context path.",
|
|
42
|
-
inputSchema: {
|
|
43
|
-
context_path: z.string().min(1),
|
|
44
|
-
force_new: z.boolean().optional(),
|
|
45
|
-
agent_id_override: z.string().min(1).optional()
|
|
46
|
-
}
|
|
47
|
-
}, async (input, extra) => toolJson(() => commands.joinPath(resolveConnectionIdentity(extra.sessionId, input.agent_id_override), {
|
|
48
|
-
context_path: input.context_path,
|
|
49
|
-
force_new: input.force_new
|
|
50
|
-
})));
|
|
51
|
-
server.registerTool("leave_room", {
|
|
52
|
-
title: "Leave Room",
|
|
53
|
-
description: "Explicitly leave a room. The room is deleted when no active members remain.",
|
|
54
|
-
inputSchema: {
|
|
55
|
-
room_id: z.string().min(1)
|
|
56
|
-
}
|
|
57
|
-
}, async (input, extra) => toolJson(() => commands.leaveRoom(resolveConnectionIdentity(extra.sessionId), input)));
|
|
58
|
-
server.registerTool("kick_member", {
|
|
59
|
-
title: "Kick Member",
|
|
60
|
-
description: "Remove an idle member from a room. Without force, only succeeds if the target's process is detected gone past the silence-grace window.",
|
|
61
|
-
inputSchema: {
|
|
62
|
-
room_id: z.string().min(1),
|
|
63
|
-
target_agent_id: z.string().min(1),
|
|
64
|
-
force: z.boolean().optional(),
|
|
65
|
-
reason: z.string().optional()
|
|
66
|
-
}
|
|
67
|
-
}, async (input, extra) => toolJson(() => commands.kickMember(resolveConnectionIdentity(extra.sessionId), input)));
|
|
68
|
-
server.registerTool("wait_for_turn", {
|
|
69
|
-
title: "Wait For Turn",
|
|
70
|
-
description: "Poll until the caller can claim the stick or takeover is available.",
|
|
71
|
-
inputSchema: {
|
|
72
|
-
room_id: z.string().min(1),
|
|
73
|
-
max_wait_ms: z.number().int().nonnegative().optional()
|
|
74
|
-
}
|
|
75
|
-
}, async (input, extra) => toolJson(() => commands.waitForTurn(resolveConnectionIdentity(extra.sessionId), input)));
|
|
76
|
-
server.registerTool("heartbeat", {
|
|
77
|
-
title: "Heartbeat",
|
|
78
|
-
description: "Renew the current owner's lease.",
|
|
79
|
-
inputSchema: ownerMutationSchema()
|
|
80
|
-
}, async (input, extra) => toolJson(() => commands.heartbeat(resolveConnectionIdentity(extra.sessionId), input)));
|
|
81
|
-
server.registerTool("release_stick", {
|
|
82
|
-
title: "Release Stick",
|
|
83
|
-
description: "Release the stick to the next active member in sequence.",
|
|
84
|
-
inputSchema: {
|
|
85
|
-
...ownerMutationSchema(),
|
|
86
|
-
handoff: handoffSchema
|
|
87
|
-
}
|
|
88
|
-
}, async (input, extra) => toolJson(() => commands.releaseStick(resolveConnectionIdentity(extra.sessionId), input)));
|
|
89
|
-
server.registerTool("pass_stick", {
|
|
90
|
-
title: "Pass Stick",
|
|
91
|
-
description: "Pass the stick to a specific active member.",
|
|
92
|
-
inputSchema: {
|
|
93
|
-
...ownerMutationSchema(),
|
|
94
|
-
to_agent_id: z.string().min(1),
|
|
95
|
-
handoff: handoffSchema
|
|
96
|
-
}
|
|
97
|
-
}, async (input, extra) => toolJson(() => commands.passStick(resolveConnectionIdentity(extra.sessionId), input)));
|
|
98
|
-
server.registerTool("takeover_stick", {
|
|
99
|
-
title: "Takeover Stick",
|
|
100
|
-
description: "Explicitly take over after claim timeout or owner lease timeout.",
|
|
101
|
-
inputSchema: {
|
|
102
|
-
room_id: z.string().min(1),
|
|
103
|
-
expected_turn_id: z.number().int().nonnegative(),
|
|
104
|
-
reason: z.string().min(1)
|
|
105
|
-
}
|
|
106
|
-
}, async (input, extra) => toolJson(() => commands.takeoverStick(resolveConnectionIdentity(extra.sessionId), input)));
|
|
107
|
-
server.registerTool("get_room_state", {
|
|
108
|
-
title: "Get Room State",
|
|
109
|
-
description: "Read the current projected room state and membership.",
|
|
110
|
-
inputSchema: {
|
|
111
|
-
room_id: z.string().min(1)
|
|
112
|
-
}
|
|
113
|
-
}, async (input, extra) => toolJson(() => commands.getRoomState({
|
|
114
|
-
...input,
|
|
115
|
-
agent_id: resolveConnectionIdentity(extra.sessionId).agent_id
|
|
116
|
-
})));
|
|
117
|
-
server.registerTool("get_room_events", {
|
|
118
|
-
title: "Get Room Events",
|
|
119
|
-
description: "Read the append-only event log for a room.",
|
|
120
|
-
inputSchema: {
|
|
121
|
-
room_id: z.string().min(1),
|
|
122
|
-
after_event_seq: z.number().int().nonnegative().optional(),
|
|
123
|
-
limit: z.number().int().positive().optional()
|
|
124
|
-
}
|
|
125
|
-
}, async (input, extra) => toolJson(() => commands.getRoomEvents({
|
|
126
|
-
...input,
|
|
127
|
-
agent_id: resolveConnectionIdentity(extra.sessionId).agent_id
|
|
128
|
-
})));
|
|
129
|
-
server.registerTool("send_message", {
|
|
130
|
-
title: "Send Message",
|
|
131
|
-
description: "Send a transient message into the room event log. Routes via to_agent_id; omit it for room broadcast.",
|
|
132
|
-
inputSchema: {
|
|
133
|
-
room_id: z.string().min(1),
|
|
134
|
-
body: z.string().min(1),
|
|
135
|
-
to_agent_id: z.string().min(1).optional(),
|
|
136
|
-
delivery_hint: z.enum(["normal", "interrupt"]).optional()
|
|
137
|
-
}
|
|
138
|
-
}, async (input, extra) => toolJson(() => commands.sendMessage(resolveConnectionIdentity(extra.sessionId), input)));
|
|
139
|
-
server.registerTool("wait_for_events", {
|
|
140
|
-
title: "Wait for Events",
|
|
141
|
-
description: "Long-poll the room event log past a cursor with optional event_type, target, and sender filters.",
|
|
142
|
-
inputSchema: {
|
|
143
|
-
room_id: z.string().min(1),
|
|
144
|
-
after_event_seq: z.number().int().nonnegative().optional(),
|
|
145
|
-
event_type: z
|
|
146
|
-
.union([z.string().min(1), z.array(z.string().min(1)).min(1)])
|
|
147
|
-
.optional(),
|
|
148
|
-
target_agent_id: z.string().min(1).optional(),
|
|
149
|
-
from_agent_id: z.string().min(1).optional(),
|
|
150
|
-
max_wait_ms: z.number().int().nonnegative().optional()
|
|
151
|
-
}
|
|
152
|
-
}, async (input, extra) => toolJson(() => commands.waitForEvents({
|
|
153
|
-
room_id: input.room_id,
|
|
154
|
-
after_event_seq: input.after_event_seq,
|
|
155
|
-
event_type: input.event_type,
|
|
156
|
-
target_agent_id: input.target_agent_id,
|
|
157
|
-
from_agent_id: input.from_agent_id,
|
|
158
|
-
max_wait_ms: input.max_wait_ms,
|
|
159
|
-
agent_id: resolveConnectionIdentity(extra.sessionId).agent_id
|
|
160
|
-
})));
|
|
161
|
-
server.registerTool("add_note", {
|
|
162
|
-
title: "Add Note",
|
|
163
|
-
description: "Leave an async note on a room. Any joined member can author; authoring refreshes presence.",
|
|
164
|
-
inputSchema: {
|
|
165
|
-
room_id: z.string().min(1),
|
|
166
|
-
body: z.string().min(1),
|
|
167
|
-
turn_id: z.number().int().nonnegative().optional()
|
|
168
|
-
}
|
|
169
|
-
}, async (input, extra) => toolJson(() => commands.addNote(resolveConnectionIdentity(extra.sessionId), input)));
|
|
170
|
-
server.registerTool("list_notes", {
|
|
171
|
-
title: "List Notes",
|
|
172
|
-
description: "List notes for a room. Default view hides resolved notes; pagination uses after_note_id.",
|
|
173
|
-
inputSchema: {
|
|
174
|
-
room_id: z.string().min(1),
|
|
175
|
-
after_note_id: z.string().min(1).optional(),
|
|
176
|
-
include_resolved: z.boolean().optional(),
|
|
177
|
-
limit: z.number().int().positive().max(200).optional()
|
|
178
|
-
}
|
|
179
|
-
}, async (input, extra) => toolJson(() => commands.listNotes(resolveConnectionIdentity(extra.sessionId), input)));
|
|
180
|
-
return server;
|
|
181
|
-
}
|
|
182
|
-
export function createConnectionIdentityResolver(options = {}) {
|
|
183
|
-
const inspector = options.inspector ?? createSystemProcessInspector({ cacheTtlMs: 60_000 });
|
|
184
|
-
const connectionOverrides = new Map();
|
|
185
|
-
const connectionIdentities = new Map();
|
|
186
|
-
return (sessionId, override) => {
|
|
187
|
-
const key = sessionId ?? "__stdio__";
|
|
188
|
-
if (override) {
|
|
189
|
-
connectionOverrides.set(key, override);
|
|
190
|
-
connectionIdentities.delete(key);
|
|
191
|
-
}
|
|
192
|
-
const cached = connectionIdentities.get(key);
|
|
193
|
-
if (cached) {
|
|
194
|
-
return cached;
|
|
195
|
-
}
|
|
196
|
-
const identity = deriveMcpHarnessIdentity({
|
|
197
|
-
sessionId,
|
|
198
|
-
agentId: connectionOverrides.get(key),
|
|
199
|
-
inspector
|
|
200
|
-
});
|
|
201
|
-
connectionIdentities.set(key, identity);
|
|
202
|
-
return identity;
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
export async function runStdioServer() {
|
|
206
|
-
const service = new TalkingStickService();
|
|
207
|
-
const server = createMcpServer(service);
|
|
208
|
-
process.on("exit", () => service.close());
|
|
209
|
-
await server.connect(new StdioServerTransport());
|
|
210
|
-
}
|
|
211
|
-
function ownerMutationSchema() {
|
|
212
|
-
return {
|
|
213
|
-
room_id: z.string().min(1),
|
|
214
|
-
lease_id: z.string().min(1),
|
|
215
|
-
expected_turn_id: z.number().int().nonnegative()
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
async function toolJson(fn) {
|
|
219
|
-
try {
|
|
220
|
-
const result = await fn();
|
|
221
|
-
return {
|
|
222
|
-
content: [
|
|
223
|
-
{
|
|
224
|
-
type: "text",
|
|
225
|
-
text: JSON.stringify(result, null, 2)
|
|
226
|
-
}
|
|
227
|
-
]
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
if (isProtocolError(error)) {
|
|
232
|
-
return {
|
|
233
|
-
isError: true,
|
|
234
|
-
content: [
|
|
235
|
-
{
|
|
236
|
-
type: "text",
|
|
237
|
-
text: JSON.stringify(error.toJSON(), null, 2)
|
|
238
|
-
}
|
|
239
|
-
]
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
throw error;
|
|
243
|
-
}
|
|
244
|
-
}
|
package/dist/server.js
DELETED