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.
@@ -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 or when the MCP tools `list_rooms`, `join_path`, `leave_room`, `kick_member`, `wait_for_turn`, `heartbeat`, `release_stick`, `pass_stick`, `takeover_stick`, `get_room_state`, `get_room_events`, `send_message`, `wait_for_events`, `add_note`, or `list_notes` are available.
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. Check that Talking Stick is actually available
26
+ ### 1. Use The CLI
28
27
 
29
- Prefer the Talking Stick MCP tools when they are available. If they are not available but the `tt` CLI is on `PATH`, use the CLI instead (`tt list`, `tt join`, `tt leave`, `tt kick`, `tt wait`, `tt state`, `tt release`, `tt pass`, `tt assign`, `tt take`, `tt msg`). Do not treat missing MCP tools alone as proof that coordination is unavailable.
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
- Some workspaces may also have sibling receive processes running `tt msg recv --wait` or `tt msg recv --follow`; leave them alone unless the operator explicitly asks you to stop or restart them.
30
+ Useful commands:
32
31
 
33
- If coordination is required and neither the MCP tools nor the `tt` CLI are available, say so briefly and ask the user whether they want to install or enable Talking Stick first. Do not pretend coordination is active.
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 the workspace room once
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
- 1. call `join_path` with the current workspace path
42
- 2. keep the returned `room_id`
43
- 3. note the returned policy, especially `heartbeatIntervalMs`
57
+ ```sh
58
+ tt join --json
59
+ ```
44
60
 
45
- If the workspace is nested, accept the resolved canonical path the server returns.
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
- ### 3. Wait before doing shared work
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
- Before making shared edits or running owner-style actions, call `wait_for_turn`.
65
+ ```sh
66
+ tt events --follow --json
67
+ ```
50
68
 
51
- Use the `room_id` returned by `join_path`. Do not pass the original filesystem path to `wait_for_turn`; path resolution belongs to `join_path`, and waiting must target the exact resolved room. This avoids ambiguity when a nested workspace resolves to an ancestor room or when multiple rooms could exist under the same tree.
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
- Keep the wait input minimal:
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
- ```json
56
- {
57
- "room_id": "<room_id from join_path>",
58
- "max_wait_ms": 110000
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
- `max_wait_ms` is optional. Use the longest client-safe wait you can support: 110000 ms is a good MCP default when the harness can tolerate it; 180000 ms is fine only when the tool/client timeout is known to exceed that. If the call times out at the harness layer, fall back to a shorter value and call again. Do not send `cursor`, even if an old tool schema still exposes it; `wait_for_turn` is cursor-free, and resumable event replay belongs to `get_room_events`.
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
- ### 4. While waiting
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
- 1. Probe `wait_for_turn` with `max_wait_ms: 0`.
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
- Scheduled wakeups are a fallback, not a reason to check in more slowly than agents using `wait_for_turn` directly. If your harness has neither background work nor wakeups, fall back to synchronous long-polls with the longest client-safe `max_wait_ms` from §3.
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
- Whether the wait runs in the foreground or the background, call it **once** with the client-safe `max_wait_ms` budget from above and let the server long-poll. When it returns without `your_turn`, call it again. Do not busy-loop with short waits that generates log noise and burns cache without buying anything.
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
- Coordination is meant to be lightweight. `wait_for_turn` is the only long-running call you should make. Room-inspection RPCs (`get_room_state`, `get_room_events`) exist to answer specific questions ("who holds the stick right now?", "what was in my predecessor's handoff?") do not call them on a timer or repeatedly just to check on another agent's progress. If you find yourself inspecting the room more than a few times per turn, stop; long-poll on `wait_for_turn` instead and trust the protocol.
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 — or any other work that does not mutate shared state
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 *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 — a missed invariant, a related bug, a sharper plan — leave a note with `add_note` rather than sitting on it until your next turn. Notes do not grant permission to edit shared files; they are observations and pointers, not coordination bypasses. The point: while you wait you can still move the work forward by feeding the holder, not by stalling.
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
- When you do take the stick, first read the attached handoff and load any useful `artifacts[]`, then run `list_notes` once so you see what other members left for you. The owner's turn is the right place to act on a note, not to debate it with its author mid-turn.
99
-
100
- ### 4.5 Out-of-band messaging
109
+ ```sh
110
+ tt notes add "Finding or pointer for the current/next holder." --json
111
+ ```
101
112
 
102
- The talking stick guarantees single-writer authority over shared workspace state. It is not a chat protocol. For transient signaling -- paging the holder, asking a quick question, or broadcasting awareness -- use messages.
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
- **Send.** `tt msg send <recipient> "<body>"` or MCP `send_message`. 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.
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
- **Receive.** Use the receive mode your harness can observe. If it can monitor stdout from a long-running child, run `tt msg recv --follow`; each incoming event lands as one JSON line. If it can only notice that a background command completed, run `tt msg recv --wait --after <last_event_seq>`; it exits on the next matching batch, then you start it again with the returned cursor. Restart with `--after <last_event_seq>` to resume.
117
+ ### 4.5 Out-Of-Band Messaging
107
118
 
108
- **When to message vs note vs handoff.**
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
- - **Message** when the exchange is conversational, ephemeral, and tied to processes that are currently online: design questions, "are you about to break X?", live coordination.
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
- **Messages are not private.** Any room member can read any message via `get_room_events` or `tt events --follow --target any`. `to_agent_id` is routing, not an ACL.
123
+ ```sh
124
+ tt msg send <recipient|room> "message body" --json
125
+ ```
115
126
 
116
- **Messages do not grant the stick.** A non-holder paging the holder does not gain write authority. The holder may act on the message immediately or defer until handoff.
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
- **Stay in the wait loop in parallel.** A `tt msg recv --wait` or `--follow` subprocess does not replace `wait_for_turn`. Keep waiting for your turn; messages are a side channel.
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
- ### 5. While holding the stick
131
+ ```sh
132
+ tt events --follow --json
133
+ ```
121
134
 
122
- If the task may run longer than a few minutes, heartbeat periodically.
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
- Use the cadence from `join_path.policy.heartbeatIntervalMs` when available. Do not invent your own cadence if the server already told you one.
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
- **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 pass. 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.
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
- ### 6. Takeover is explicit
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
- If `wait_for_turn` reports `takeover_available`:
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
- - explain why takeover is available (`owner_timeout`, `owner_gone`, `claim_timeout`, `recipient_gone`)
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
- If the operator explicitly tells you to take over despite a reservation or live owner, use the CLI path when available: `tt take --operator-requested --reason "<operator requested takeover>"`. Do not invent this override yourself; it is for direct operator intervention.
150
+ ### 5. While Holding The Stick
138
151
 
139
- ### 7. Finish with a real handoff
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
- When you are done with your turn, default to `release_stick`.
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
- **Default to `release_stick`.** Releasing lets the server pick the next fair waiter: a recent waiter that is new or has gone longest without holding the stick. If the best-known candidate is between wait polls, the room can briefly stay claimable instead of pinning a stale reservation. This keeps the room open instead of silently turning agent-to-agent handoffs into a duopoly.
156
+ ### 6. Takeover Is Explicit
144
157
 
145
- Use `pass_stick` only when you have a concrete reason a specific named member must go next:
158
+ If `tt wait` reports `takeover_available`:
146
159
 
147
- - they have unique context the next step requires
148
- - they hold a credential or capability others lack
149
- - the operator explicitly addressed the work to them
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
- **Keep handoffs tight.** Handoffs are persisted in the event log and re-read on claims. Aim for roughly 150-300 words of `status`; reference commits by SHA instead of restating diffs, and use `artifacts[]` with path, line range, and role instead of pasting code. The handoff is the headline; long-form context belongs in `docs/` or a note.
165
+ If the operator explicitly tells you to take over despite a reservation or live owner, use:
156
166
 
157
- Minimum handoff quality:
167
+ ```sh
168
+ tt take --operator-requested --reason "operator requested takeover" --json
169
+ ```
158
170
 
159
- - `status`: what you finished, what changed, and what remains true
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
- Add `artifacts`, `open_questions`, and `do_not` when they will save the next harness real time or prevent rework.
173
+ ### 7. Finish With A Real Handoff
163
174
 
164
- Example:
175
+ When you are done with your turn, default to releasing:
165
176
 
166
- ```json
177
+ ```sh
178
+ tt release --stdin <<'JSON'
167
179
  {
168
- "status": "Added the MCP smoke test and verified it against two clients sharing one SQLite database.",
169
- "next_action": "Run the same handoff path through the human CLI and confirm pass/release behavior matches the MCP flow.",
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": "tests/mcp-smoke.test.ts",
184
+ "path": "docs/plans/2026-05-05-cli-only-coordination.md",
173
185
  "role": "review",
174
- "note": "End-to-end MCP adapter smoke coverage."
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
- **`pass_stick` requires the target to be an active room member.** If the intended recipient's harness session has ended and they show as `inactive` in `get_room_state.members`, `pass_stick` can return `unknown_member`. Use `release_stick` instead; the next fair waiter can claim through the normal sequence path.
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
- Remember that the operator can join their own room as `human:<user>`. Default behavior should leave room for them to claim turns naturally; releasing rather than passing keeps that door open.
203
+ Minimum handoff quality:
186
204
 
187
- ### 8. After passing or releasing, stay in the loop
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
- **The default after `release_stick` or `pass_stick` 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. In a multi-agent workspace, the expectation is: work on your turn, hand off, wait for your next turn, repeat.
210
+ ### 8. After Release, Stay In The Loop
190
211
 
191
- Stopping to ask questions after every handoff defeats the coordination protocol the operator wired you into a room so that you *would* keep showing up without being asked.
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 (the operator said so, or the final handoff marks the work complete)
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 ("that's enough," "drop out of the room," a new unrelated task, etc.)
218
+ - the operator gives a direct redirect or stop
198
219
 
199
- In every other case: after `release_stick` or `pass_stick`, go straight back into the wait loop (ideally backgrounded — see §4).
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, call `leave_room` or `tt leave`. Rooms with no active members are deleted instead of kept as history, and long-idle rooms may be purged on later invocations.
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 (visible as `inactive last seen ...` in `tt state`), call `kick_member` / `tt kick <agent_id>` to evict them. This is the right tool when liveness has already decided the target is dead — pass `force: true` only when the operator explicitly tells you to remove a still-active member.
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 and Inspection
226
+ ## Recovery And Inspection
206
227
 
207
228
  Use these reads when you need context:
208
229
 
209
- - `list_rooms`: discover active rooms under a path
210
- - `leave_room`: explicitly remove your membership from a room
211
- - `kick_member`: evict an idle member whose process is gone (use `force: true` only on operator instruction)
212
- - `get_room_state`: authoritative current room projection
213
- - `get_room_events`: replay recent claims, releases, passes, and takeovers
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 `get_room_state` over guessing from local memory when ownership may have changed.
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
 
@@ -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
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runStdioServer } from "./mcp-server.js";
3
- await runStdioServer();