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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # Talking Stick
2
2
 
3
- An MCP coordination server that lets multiple AI coding agents share a single workspace without stepping on each other. One agent holds the stick at a time; handoffs carry structured context so the next agent doesn't have to re-derive it.
3
+ A CLI coordination tool that lets multiple AI coding agents share a single workspace without stepping on each other. One agent holds the stick at a time; handoffs carry structured context so the next agent doesn't have to re-derive it.
4
4
 
5
- **Version:** 0.2.0. Multi-process-safe (SQLite WAL), liveness-aware, no daemon. Supports Claude Code, Codex CLI, Gemini CLI, and OpenCode out of the box. Two agents in the same room can also chat out-of-band — without passing the stick — via `tt msg send/recv` and the matching MCP tools.
5
+ **Version:** 0.3.0. Multi-process-safe (SQLite WAL), liveness-aware, no daemon. Supports Claude Code, Codex CLI, Gemini CLI, and OpenCode out of the box. Two agents in the same room can also chat out-of-band — without passing the stick — via `tt msg send/recv`.
6
6
 
7
7
  ## Quickstart
8
8
 
@@ -14,13 +14,13 @@ Three steps, then you're coordinating two agents in the same repo.
14
14
  npm i -g talking-stick
15
15
  ```
16
16
 
17
- ### 2. Register the MCP server and skill in every harness
17
+ ### 2. Install the skill in every harness
18
18
 
19
19
  ```bash
20
20
  tt install --all
21
21
  ```
22
22
 
23
- Restart any harness that was already running so it loads the new MCP server. The `talking_stick` tools and skill now appear in every workspace.
23
+ Restart any harness that was already running so it loads the updated skill. The skill teaches agents to coordinate by running `tt` CLI commands from the workspace.
24
24
 
25
25
  ### 3. Try it: two agents, one repo
26
26
 
@@ -46,7 +46,7 @@ That's the whole workflow. They negotiate turns automatically, hand off structur
46
46
 
47
47
  | Method | Command | Notes |
48
48
  |---|---|---|
49
- | **From npm** | `npm i -g talking-stick` | Published as `0.2.0`. Requires Node ≥ 22. |
49
+ | **From npm** | `npm i -g talking-stick` | Published as `0.3.0`. Requires Node ≥ 22. |
50
50
  | **From GitHub** | `npm i -g github:mostlydev/talking-stick` | Tracks the `master` branch; builds on install via the `prepare` hook. |
51
51
  | **From source** | `git clone … && npm install && npm link` | For contributors. |
52
52
 
@@ -76,7 +76,7 @@ Uses the right npm/pnpm/yarn by default:
76
76
  tt self-update
77
77
  ```
78
78
 
79
- Skills are symlinked automatically, so they don't need an update.
79
+ `tt self-update` also removes stale Talking Stick MCP registrations left by older installs. The first normal `tt` invocation after a package-version change runs the same cleanup if the package manager skipped lifecycle scripts.
80
80
 
81
81
  ### Remove
82
82
 
@@ -86,32 +86,26 @@ tt uninstall --all
86
86
 
87
87
  ## What it gives your agent
88
88
 
89
- Once installed, each agent harness sees these tools:
89
+ Once installed, each agent harness has a skill that tells it to coordinate through the `tt` CLI:
90
90
 
91
91
  ```
92
- list_rooms — which rooms exist under a path
93
- join_path — join the room for this workspace
94
- leave_room — explicitly leave a room; deletes it when no active members remain
95
- wait_for_turn — block until the stick is available, with takeover signals
96
- heartbeat prove liveness while holding the stick
97
- release_stick normal handoff to the next fair waiter, with structured Handoff
98
- pass_stick explicit handoff to a named agent
99
- takeover_stick deliberate claim when the prior holder is gone/stuck
100
- kick_member evict an idle member whose process is gone
101
- get_room_state authoritative state projection
102
- get_room_events audit log of turn transitions
103
- add_note — leave an async observation for the current owner
104
- list_notes — read notes left for the room
105
- send_message — out-of-band chat into the room event log (direct or broadcast)
106
- wait_for_events — observer-safe long-poll over the event log with type/target/sender filters
92
+ tt list — which rooms exist under a path
93
+ tt join — join the room for this workspace
94
+ tt leave — explicitly leave a room; deletes it when no active members remain
95
+ tt wait — block until the stick is available, with takeover signals
96
+ tt release normal handoff to the next fair waiter, with structured Handoff
97
+ tt assign explicit handoff to a named agent
98
+ tt take deliberate claim when the prior holder is gone/stuck
99
+ tt kick evict an idle member whose process is gone
100
+ tt state authoritative state projection
101
+ tt events audit log and long-poll stream of turn transitions/messages
102
+ tt notes add/list durable async observations for the room
103
+ tt msg send/recv — out-of-band chat into the room event log
107
104
  ```
108
105
 
109
106
  A workspace maps to a room — usually the `git` root or nearest project marker — so two agents `cd`'d anywhere under the same repo join the same room automatically.
110
107
 
111
- The skill complements the MCP tools:
112
-
113
- - MCP gives the harness the coordination surface
114
- - the global skill tells the model when to join, wait, heartbeat, take over, and hand off
108
+ The global skill tells the model when to join, wait, verify its guardian, take over, leave notes, send messages, and hand off.
115
109
 
116
110
  ## Non-owner notes
117
111
 
@@ -135,7 +129,9 @@ tt events --wait|--follow [--event TYPE[,TYPE]] [--target self|any|agent]
135
129
  - `--interrupt` marks the message time-sensitive; receivers decide whether to act on it now.
136
130
  - `tt msg recv --follow` is a long-running tail (one JSON line per event) suited to harnesses that can monitor child stdout (Claude Code Monitor, terminals).
137
131
  - `tt msg recv --wait` exits on the next matching batch — ideal for harnesses that can launch a background command and notice when it completes; restart with `--after <last_event_seq>` to resume.
132
+ - `tt events --wait` and `tt events --follow` default to `--target self`; pass `--target any` only for audit/debug views.
138
133
  - `wait_for_events` is observer-safe: it never mutates room state, so non-holders can use it freely without disturbing turn-fairness bookkeeping.
134
+ - Event receive does not grant the stick. Agents must still use `tt wait` for ownership and verify the returned guardian before editing shared files.
139
135
 
140
136
  **When to message vs note vs handoff.**
141
137
 
@@ -145,26 +141,11 @@ tt events --wait|--follow [--event TYPE[,TYPE]] [--target self|any|agent]
145
141
 
146
142
  **`to_agent_id` is routing, not ACL.** Any room member can read any message via `get_room_events` or `tt events --follow --target any`. Messages are not private. They also do not grant the stick — a non-holder paging the holder gets attention, not write authority.
147
143
 
148
- ## How installation works per harness
149
-
150
- `tt install` installs both pieces a harness needs: the MCP server registration and the bundled `talking-stick` skill.
151
-
152
- For MCP registration, it prefers each harness's own `mcp add` subcommand when available (so the server ends up in the right user-global config with the right schema), and falls back to direct JSON editing when it isn't.
153
-
154
- | Harness | Scope | Under the hood |
155
- |---------------|--------------|-----------------------------------------------------------------------------|
156
- | claude-code | user | `claude mcp add -s user talking-stick -- tt mcp` |
157
- | codex | user | `codex mcp add talking-stick -- tt mcp` |
158
- | gemini | user | `gemini mcp add -s user -t stdio talking-stick tt mcp` |
159
- | opencode | user | Merge `mcp.talking-stick` into `$XDG_CONFIG_HOME/opencode/opencode.json` |
144
+ For harnesses that only notice completed subprocesses, run `tt events --wait --after <cursor> --json` as a wake process alongside the normal `tt wait --json` loop. A message, pass, release, or assignment event should make the agent read/reply/retry `tt wait`; it is not permission to mutate the workspace.
160
145
 
161
- All four install into **user-global scope**, not project-local. A coordination server is only useful if every workspace your agent enters can see the same rooms — project-scoped MCP would defeat the point.
162
-
163
- If you'd rather apply setup by hand, run `tt install --print <harness>` to see the exact MCP and skill actions, then apply them yourself.
164
-
165
- ## Skill paths per harness
146
+ ## How installation works per harness
166
147
 
167
- Talking Stick also ships with a portable `talking-stick` skill:
148
+ `tt install` installs or refreshes the bundled `talking-stick` skill. It does not add MCP servers. During install, uninstall, package update, and first run after an installed package version changes, `tt` removes stale MCP registrations written by older Talking Stick releases.
168
149
 
169
150
  - Claude Code: copied or linked into `~/.claude/skills/talking-stick`
170
151
  - Codex: copied or linked into `~/.codex/skills/talking-stick`
@@ -173,6 +154,8 @@ Talking Stick also ships with a portable `talking-stick` skill:
173
154
 
174
155
  By default, `tt install` links the bundled skill into each harness so local updates are picked up immediately. Pass `--copy` if you want a standalone snapshot instead.
175
156
 
157
+ Stale MCP cleanup is strict for OpenCode JSON entries: it removes only the canonical `mcp.talking-stick` value with `["tt", "mcp"]` and leaves hand-edited entries alone. Claude Code, Codex, and Gemini cleanup uses their own `mcp remove` commands when the old server name exists. Every cleanup run appends JSONL audit entries to `${TALKING_STICK_DATA_DIR}/update-migrations.log`.
158
+
176
159
  Human CLI invocations also perform a silent best-effort sync for already-installed file-based skills in Claude Code, Codex, and OpenCode. If the installed skill is a copy, it is refreshed from the bundled skill; if it is a stale symlink, it is relinked. Missing harness config directories and missing skill installs are skipped. Gemini skills are managed by Gemini's own registry, so use `tt install gemini` after updating when needed.
177
160
 
178
161
  ## Human CLI
@@ -184,7 +167,7 @@ tt whoami [--explain] # show the resolved C
184
167
  tt list [path] # list rooms
185
168
  tt join [path] [--force-new] # join the room for path
186
169
  tt leave [path] # leave the room for path
187
- tt wait [path] [--timeout 30s] # block until your turn
170
+ tt wait [path] [--timeout 110s] # block until your turn
188
171
  tt try [path] # non-blocking claim attempt
189
172
  tt state [path] # full room state
190
173
  tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent] # room event log; --wait/--follow long-polls
@@ -197,13 +180,14 @@ tt take [path] [--reason TEXT] # human-friendly take/
197
180
  tt takeover [path] [--reason TEXT] # alias for take
198
181
  tt notes add <body> [--turn N] [--path DIR] [--stdin] # leave an async note
199
182
  tt notes list [--all] [--after ID] [--limit N] [--path DIR] # read notes
200
- tt mcp # run the MCP stdio server
201
- tt install <harness...> | --all [--print] [--copy] [--link] # install MCP server and skill
202
- tt uninstall <harness...> | --all [--print] # remove MCP server and skill
183
+ tt install <harness...> | --all [--print] [--copy] [--link] # install skill and clean stale MCP entries
184
+ tt uninstall <harness...> | --all [--print] # remove skill and stale MCP entries
203
185
  tt self-update [--print] [--manager npm|pnpm|yarn|bun] # update to the latest published tt
204
186
  ```
205
187
 
206
- `tt self-update` detects how `tt` was installed (npm / pnpm / yarn / bun, including npm-via-Homebrew/mise/asdf/nvm) and runs the right global-update command. Pass `--print` to see the inferred command without running it; pass `--manager` to override detection. Running `tt self-update` from a development checkout (where `tt` resolves outside `node_modules/talking-stick`) refuses and tells you to `git pull && npm install && npm run build` instead.
188
+ `[path]` defaults to the current working directory. Omit it for normal in-repo coordination; pass it only when you intentionally want a different or nested room.
189
+
190
+ `tt self-update` detects how `tt` was installed (npm / pnpm / yarn / bun, including npm-via-Homebrew/mise/asdf/nvm), runs the right global-update command, then removes stale MCP registrations from older Talking Stick installs. Pass `--print` to see the inferred command without running it; pass `--manager` to override detection. Running `tt self-update` from a development checkout (where `tt` resolves outside `node_modules/talking-stick`) refuses and tells you to `git pull && npm install && npm run build` instead.
207
191
 
208
192
  Human CLI commands use a stable identity like `human:<username>`. When `tt wait`, `tt take`, or `tt takeover` wins the turn, a small background guardian keeps the lease alive on your behalf until you release, pass, or assign it. Human CLI `take` intentionally works without a required reason so an operator can step into a stuck room quickly; harness-aware CLI takeovers still require `--reason` unless the command includes `--operator-requested`.
209
193
 
@@ -231,7 +215,7 @@ Use `tt whoami --explain` to see which identity path the CLI chose.
231
215
  - **Fencing tokens.** `lease_id` + `turn_id` make stale writes impossible — an agent who lost their turn cannot commit anything under the room's name.
232
216
  - **Liveness-aware recovery.** Dead or crashed holders are detected with OS-level process checks; claim-timeout takeover skips the prior owner when another active member is waiting.
233
217
  - **Multi-process safe.** Shared SQLite with WAL mode, `BEGIN IMMEDIATE` writes, 250 ms polling for `wait_for_turn`. No daemon required.
234
- - **Per-call identity derivation.** MCP callers don't supply `agent_id`; the adapter derives identity from the spawning harness process. Human CLI callers get a stable `human:<username>` identity.
218
+ - **Per-call identity derivation.** Harness-launched CLI calls derive identity from harness environment or ancestry. Human CLI callers get a stable `human:<username>` identity.
235
219
 
236
220
  ## Storage
237
221
 
@@ -1,7 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
- import { SUPPORTED_HARNESSES, detectHarness, parseHarnessList, planInstall, planUninstall, runAction } from "../install.js";
2
+ import { SUPPORTED_HARNESSES, detectHarness, parseHarnessList, planUninstall, runAction } from "../install.js";
3
3
  import { planSkillInstall, planSkillUninstall } from "../skill-install.js";
4
+ import { resolveDataDir } from "../config.js";
5
+ import { FileAuditLog, defaultAuditLogPath } from "../install-audit.js";
6
+ import { removeStaleMcpRegistrations } from "../install-migration.js";
4
7
  import { detectInstallSource, isPackageManager, planSelfUpdate, resolveCurrentBinaryPath } from "../self-update.js";
8
+ import { readPackageVersion, runStaleMcpCleanup } from "../update-migration.js";
5
9
  import { getStringOption, hasOption, normalizeBooleanFlag } from "./parser.js";
6
10
  export async function runInstallCommand(parsed) {
7
11
  normalizeBooleanFlag(parsed, "print");
@@ -14,28 +18,33 @@ export async function runInstallCommand(parsed) {
14
18
  skipMissing: true
15
19
  };
16
20
  if (dryRun) {
17
- for (const action of planCombinedInstallActions(harnesses, installOptions)) {
21
+ for (const action of planInstallActions(harnesses, installOptions)) {
22
+ printActionPlan(action);
23
+ }
24
+ for (const action of planCleanupActions(harnesses, installOptions)) {
18
25
  printActionPlan(action);
19
26
  }
20
27
  return;
21
28
  }
22
- const results = (await Promise.all(harnesses.map((harness) => runCombinedInstall(harness, installOptions)))).flat();
29
+ const results = (await Promise.all(harnesses.map((harness) => runSkillInstall(harness, installOptions)))).flat();
23
30
  reportInstallResults(results, "install");
31
+ reportCleanupResults(await runCleanup(harnesses, "manual", installOptions), "install");
24
32
  }
25
33
  export async function runUninstallCommand(parsed) {
26
34
  normalizeBooleanFlag(parsed, "print");
27
35
  const harnesses = selectHarnesses(parsed);
28
36
  const dryRun = hasOption(parsed, "print");
29
37
  const installOptions = { skipMissing: true };
30
- const actions = planCombinedUninstallActions(harnesses, installOptions);
38
+ const actions = planUninstallActions(harnesses, installOptions);
31
39
  if (dryRun) {
32
40
  for (const action of actions) {
33
41
  printActionPlan(action);
34
42
  }
35
43
  return;
36
44
  }
37
- const results = (await Promise.all(harnesses.map((harness) => runCombinedUninstall(harness, installOptions)))).flat();
45
+ const results = (await Promise.all(harnesses.map((harness) => runSkillUninstall(harness, installOptions)))).flat();
38
46
  reportInstallResults(results, "uninstall");
47
+ reportCleanupResults(await runCleanup(harnesses, "uninstall", installOptions), "uninstall");
39
48
  }
40
49
  export async function runInstallSkillCommand(parsed) {
41
50
  normalizeBooleanFlag(parsed, "print");
@@ -85,6 +94,7 @@ export async function runSelfUpdateCommand(parsed, cliEntryUrl) {
85
94
  const binaryPath = resolveCurrentBinaryPath(cliEntryUrl);
86
95
  source = detectInstallSource({ binaryPath });
87
96
  }
97
+ const packageVersionFrom = readPackageVersion(cliEntryUrl);
88
98
  const plan = planSelfUpdate(source);
89
99
  if (!plan) {
90
100
  if (source === "dev") {
@@ -98,7 +108,29 @@ export async function runSelfUpdateCommand(parsed, cliEntryUrl) {
98
108
  }
99
109
  process.stdout.write(`Updating via: ${plan.description}\n`);
100
110
  await runInheritIo(plan.command, plan.args);
101
- process.stdout.write("Done. Restart your harness MCP subprocess to pick up the new dist.\n");
111
+ const packageVersionTo = readPackageVersion(cliEntryUrl);
112
+ const cleanup = await runStaleMcpCleanup({
113
+ harnesses: "all",
114
+ reason: "update",
115
+ packageVersionFrom,
116
+ packageVersionTo,
117
+ installOptions: { skipMissing: true }
118
+ });
119
+ reportCleanupResults(cleanup.results, "self-update");
120
+ process.stdout.write("Done. Restart any long-running harness sessions to pick up the new tt.\n");
121
+ }
122
+ export async function runMcpMigrationCommand(parsed) {
123
+ normalizeBooleanFlag(parsed, "quiet");
124
+ const reason = parseAuditReason(getStringOption(parsed, "reason") ?? "manual");
125
+ const quiet = hasOption(parsed, "quiet");
126
+ const cleanup = await runStaleMcpCleanup({
127
+ harnesses: "all",
128
+ reason,
129
+ installOptions: { skipMissing: true }
130
+ });
131
+ if (!quiet) {
132
+ reportCleanupResults(cleanup.results, "self-update");
133
+ }
102
134
  }
103
135
  function resolveSkillInstallLinkMode(parsed) {
104
136
  const wantsCopy = hasOption(parsed, "copy");
@@ -111,48 +143,39 @@ function resolveSkillInstallLinkMode(parsed) {
111
143
  }
112
144
  return true;
113
145
  }
114
- function planCombinedInstallActions(harnesses, installOptions) {
115
- return harnesses.flatMap((harness) => {
116
- const mcpAction = planInstall(harness, installOptions);
117
- if (mcpAction.kind === "skip") {
118
- return [mcpAction];
119
- }
120
- return [
121
- mcpAction,
122
- planSkillInstall(harness, {
123
- ...installOptions,
124
- // In dry-run mode, show the skill action that will follow MCP setup
125
- // even when the MCP installer is what creates the harness config root.
126
- skipMissing: false
127
- })
128
- ];
129
- });
146
+ function planInstallActions(harnesses, installOptions) {
147
+ return harnesses.map((harness) => planSkillInstall(harness, installOptions));
130
148
  }
131
- function planCombinedUninstallActions(harnesses, installOptions) {
149
+ function planUninstallActions(harnesses, installOptions) {
132
150
  return harnesses.flatMap((harness) => [
133
- planUninstall(harness, installOptions),
134
151
  planSkillUninstall(harness, {
135
152
  ...installOptions,
136
153
  skipMissing: false
137
- })
154
+ }),
155
+ planUninstall(harness, installOptions)
138
156
  ]);
139
157
  }
140
- async function runCombinedInstall(harness, installOptions) {
141
- const mcpAction = planInstall(harness, installOptions);
142
- const mcpResult = await runAction(mcpAction, installOptions);
143
- if (!mcpResult.ok || mcpResult.skipped) {
144
- return [mcpResult];
145
- }
158
+ function planCleanupActions(harnesses, installOptions) {
159
+ return harnesses.map((harness) => planUninstall(harness, installOptions));
160
+ }
161
+ async function runSkillInstall(harness, installOptions) {
146
162
  const skillAction = planSkillInstall(harness, installOptions);
147
163
  const skillResult = await runAction(skillAction, installOptions);
148
- return [mcpResult, skillResult];
164
+ return [skillResult];
149
165
  }
150
- async function runCombinedUninstall(harness, installOptions) {
151
- const mcpAction = planUninstall(harness, installOptions);
152
- const mcpResult = await runAction(mcpAction, installOptions);
166
+ async function runSkillUninstall(harness, installOptions) {
153
167
  const skillAction = planSkillUninstall(harness, installOptions);
154
168
  const skillResult = await runAction(skillAction, installOptions);
155
- return [mcpResult, skillResult];
169
+ return [skillResult];
170
+ }
171
+ async function runCleanup(harnesses, reason, installOptions) {
172
+ const dataDir = resolveDataDir();
173
+ return removeStaleMcpRegistrations({
174
+ harnesses,
175
+ reason,
176
+ audit: new FileAuditLog(defaultAuditLogPath(dataDir)),
177
+ installOptions
178
+ });
156
179
  }
157
180
  function selectHarnesses(parsed) {
158
181
  if (hasOption(parsed, "all")) {
@@ -199,6 +222,23 @@ function reportInstallResults(results, mode) {
199
222
  throw new Error(`${mode} completed with failures.`);
200
223
  }
201
224
  }
225
+ function reportCleanupResults(results, mode) {
226
+ let anyFailed = false;
227
+ for (const result of results) {
228
+ process.stdout.write(`[${result.harness}] mcp-cleanup ${result.action}: ${result.message}\n`);
229
+ if (result.action === "failed")
230
+ anyFailed = true;
231
+ }
232
+ if (anyFailed) {
233
+ throw new Error(`${mode} completed with MCP cleanup failures.`);
234
+ }
235
+ }
202
236
  function formatInstallStatus(status) {
203
237
  return status.replaceAll("_", "-");
204
238
  }
239
+ function parseAuditReason(value) {
240
+ if (value === "update" || value === "first-run" || value === "uninstall" || value === "manual") {
241
+ return value;
242
+ }
243
+ throw new Error(`--reason must be one of update | first-run | uninstall | manual (got ${value}).`);
244
+ }
@@ -123,7 +123,7 @@ Commands:
123
123
  tt join [path] [--force-new]
124
124
  tt leave [path]
125
125
  tt kick <agent_id> [path] [--reason TEXT] [--force]
126
- tt wait [path] [--timeout 30s]
126
+ tt wait [path] [--timeout 110s]
127
127
  tt try [path]
128
128
  tt state [path]
129
129
  tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent]
@@ -136,7 +136,6 @@ Commands:
136
136
  tt takeover [path] [--reason TEXT] [--operator-requested]
137
137
  tt notes add <body> [--turn N] [--path DIR] [--stdin]
138
138
  tt notes list [--all] [--after NOTE_ID] [--limit N] [--path DIR]
139
- tt mcp
140
139
  tt install <harness...> | --all [--print] [--copy] [--link]
141
140
  tt uninstall <harness...> | --all [--print]
142
141
  tt self-update [--print] [--manager npm|pnpm|yarn|bun]
@@ -144,6 +143,7 @@ Commands:
144
143
  Harnesses: ${SUPPORTED_HARNESSES.join(", ")}
145
144
 
146
145
  Common options:
146
+ [path] Defaults to the current working directory when omitted
147
147
  --agent ID Override the default human identity
148
148
  --json Force JSON output (also default when invoked from a harness)
149
149
  --text Force human-readable text even when invoked from a harness
@@ -1,20 +1,10 @@
1
- import { runStdioServer } from "../index.js";
2
1
  import { runGuardCommand } from "./guardian.js";
3
- import { runInstallCommand, runInstallSkillCommand, runSelfUpdateCommand, runUninstallCommand, runUninstallSkillCommand } from "./install-commands.js";
2
+ import { runInstallCommand, runMcpMigrationCommand, runSelfUpdateCommand, runUninstallCommand } from "./install-commands.js";
4
3
  import { handleMsgCommand } from "./msg-commands.js";
5
4
  import { handleNotesCommand } from "./notes-commands.js";
6
5
  import { handleEventsCommand, handleJoinCommand, handleKickCommand, handleLeaveCommand, handleListCommand, handleStateCommand, handleWhoAmICommand } from "./room-commands.js";
7
6
  import { handleAssignCommand, handlePassCommand, handleReleaseCommand, handleTakeCommand, handleWaitCommand } from "./turn-commands.js";
8
7
  export const COMMAND_REGISTRY = [
9
- {
10
- name: "mcp",
11
- needsRuntime: false,
12
- startupMaintenance: false,
13
- internal: true,
14
- usage: "tt mcp",
15
- description: "Run the MCP server over stdio.",
16
- handler: () => runStdioServer()
17
- },
18
8
  {
19
9
  name: "guard",
20
10
  needsRuntime: false,
@@ -30,7 +20,7 @@ export const COMMAND_REGISTRY = [
30
20
  startupMaintenance: false,
31
21
  internal: false,
32
22
  usage: "tt install <harness...> | --all [--print] [--copy] [--link]",
33
- description: "Install Talking Stick into harness MCP configs and skills.",
23
+ description: "Install the Talking Stick skill and remove stale MCP registrations.",
34
24
  handler: ({ parsed }) => runInstallCommand(parsed)
35
25
  },
36
26
  {
@@ -39,35 +29,26 @@ export const COMMAND_REGISTRY = [
39
29
  startupMaintenance: false,
40
30
  internal: false,
41
31
  usage: "tt uninstall <harness...> | --all [--print]",
42
- description: "Remove Talking Stick from harness MCP configs and skills.",
32
+ description: "Remove the Talking Stick skill and stale MCP registrations.",
43
33
  handler: ({ parsed }) => runUninstallCommand(parsed)
44
34
  },
45
35
  {
46
- name: "install-skill",
36
+ name: "self-update",
47
37
  needsRuntime: false,
48
38
  startupMaintenance: false,
49
39
  internal: false,
50
- usage: "tt install-skill <harness...> | --all [--print] [--copy] [--link]",
51
- description: "Install the bundled Talking Stick skill.",
52
- handler: ({ parsed }) => runInstallSkillCommand(parsed)
40
+ usage: "tt self-update [--print] [--manager npm|pnpm|yarn|bun]",
41
+ description: "Update the globally installed tt package.",
42
+ handler: ({ parsed, cliEntryUrl }) => runSelfUpdateCommand(parsed, cliEntryUrl)
53
43
  },
54
44
  {
55
- name: "uninstall-skill",
45
+ name: "migrate-mcp",
56
46
  needsRuntime: false,
57
47
  startupMaintenance: false,
58
- internal: false,
59
- usage: "tt uninstall-skill <harness...> | --all [--print]",
60
- description: "Remove the bundled Talking Stick skill.",
61
- handler: ({ parsed }) => runUninstallSkillCommand(parsed)
62
- },
63
- {
64
- name: "self-update",
65
- needsRuntime: false,
66
- startupMaintenance: true,
67
- internal: false,
68
- usage: "tt self-update [--print] [--manager npm|pnpm|yarn|bun]",
69
- description: "Update the globally installed tt package.",
70
- handler: ({ parsed, cliEntryUrl }) => runSelfUpdateCommand(parsed, cliEntryUrl)
48
+ internal: true,
49
+ usage: "tt migrate-mcp [--reason update|first-run|uninstall|manual] [--quiet]",
50
+ description: "Remove stale Talking Stick MCP registrations.",
51
+ handler: ({ parsed }) => runMcpMigrationCommand(parsed)
71
52
  },
72
53
  {
73
54
  name: "whoami",
@@ -146,7 +127,7 @@ export const COMMAND_REGISTRY = [
146
127
  needsRuntime: true,
147
128
  startupMaintenance: true,
148
129
  internal: false,
149
- usage: "tt wait [path] [--timeout 30s]",
130
+ usage: "tt wait [path] [--timeout 110s]",
150
131
  description: "Wait until this agent can claim the stick.",
151
132
  handler: ({ runtime, parsed, cliEntryUrl }) => handleWaitCommand(requireRuntime(runtime), parsed, false, cliEntryUrl)
152
133
  },
@@ -135,7 +135,7 @@ export async function handleEventsCommand(runtime, parsed) {
135
135
  if (hasOption(parsed, "wait") || hasOption(parsed, "follow")) {
136
136
  await runEventStream(runtime, parsed, identity, session.room_id, {
137
137
  event_type: parseEventTypeFilter(getStringOption(parsed, "event")),
138
- default_target: "any",
138
+ default_target: "self",
139
139
  force_tail_cursor: false
140
140
  });
141
141
  return;
@@ -1,7 +1,20 @@
1
1
  import { syncInstalledSkills } from "../skill-install.js";
2
+ import { runFirstRunMcpMigration } from "../update-migration.js";
3
+ import { detectInstallSource, resolveCurrentBinaryPath } from "../self-update.js";
2
4
  import { isKnownHarnessCliEnv } from "./identity.js";
3
5
  import { getCommand } from "./registry.js";
4
- export function runStartupMaintenance(parsed, env = process.env) {
6
+ export async function runStartupMaintenance(parsed, cliEntryUrl, env = process.env) {
7
+ if (shouldRunFirstRunMcpMigration(parsed, cliEntryUrl, env)) {
8
+ try {
9
+ await runFirstRunMcpMigration({
10
+ installOptions: { env }
11
+ });
12
+ }
13
+ catch {
14
+ // Startup cleanup is best-effort. Explicit install, uninstall, and
15
+ // self-update paths surface cleanup failures directly.
16
+ }
17
+ }
5
18
  if (!shouldAutoSyncInstalledSkills(parsed, env)) {
6
19
  return;
7
20
  }
@@ -13,6 +26,19 @@ export function runStartupMaintenance(parsed, env = process.env) {
13
26
  // unrelated tt command fail.
14
27
  }
15
28
  }
29
+ export function shouldRunFirstRunMcpMigration(parsed, cliEntryUrl, env = process.env) {
30
+ if (env.TALKING_STICK_DISABLE_MCP_MIGRATION?.trim()) {
31
+ return false;
32
+ }
33
+ const command = getCommand(parsed.name);
34
+ if (!command?.startupMaintenance) {
35
+ return false;
36
+ }
37
+ const source = detectInstallSource({
38
+ binaryPath: resolveCurrentBinaryPath(cliEntryUrl)
39
+ });
40
+ return source !== "dev" && source !== "unknown";
41
+ }
16
42
  export function shouldAutoSyncInstalledSkills(parsed, env = process.env) {
17
43
  if (env.TALKING_STICK_DISABLE_SKILL_SYNC?.trim()) {
18
44
  return false;
package/dist/cli.js CHANGED
@@ -11,10 +11,10 @@ import { runStartupMaintenance } from "./cli/startup-maintenance.js";
11
11
  export { checkGuardianLiveness } from "./cli/guardian.js";
12
12
  export { parseHandoffJson } from "./cli/handoff.js";
13
13
  export { formatRelativeTime, shouldUseJson } from "./cli/output.js";
14
- export { shouldAutoSyncInstalledSkills } from "./cli/startup-maintenance.js";
14
+ export { shouldAutoSyncInstalledSkills, shouldRunFirstRunMcpMigration } from "./cli/startup-maintenance.js";
15
15
  export async function runCli(argv = process.argv.slice(2)) {
16
16
  const parsed = parseCommand(argv);
17
- runStartupMaintenance(parsed);
17
+ await runStartupMaintenance(parsed, import.meta.url);
18
18
  if (!parsed.name || parsed.name === "help" || parsed.name === "--help") {
19
19
  printHelp();
20
20
  return;
package/dist/config.js CHANGED
@@ -4,9 +4,9 @@ export const defaultPolicy = {
4
4
  ownerLeaseTtlMs: 45 * 60 * 1000,
5
5
  heartbeatIntervalMs: 5 * 60 * 1000,
6
6
  claimTtlMs: 20 * 60 * 1000,
7
- waitForTurnMaxWaitMs: 30 * 1000,
7
+ waitForTurnMaxWaitMs: 110 * 1000,
8
8
  waitForTurnPollMs: 250,
9
- waitForEventsMaxWaitMs: 30 * 1000,
9
+ waitForEventsMaxWaitMs: 110 * 1000,
10
10
  waitForEventsPollMs: 250,
11
11
  waitForEventsBatchLimit: 100,
12
12
  presenceTtlMs: 4 * 60 * 60 * 1000,
package/dist/identity.js CHANGED
@@ -127,13 +127,13 @@ function harnessAgentId(harness, sessionId, hostId, username) {
127
127
  function resolveHarnessSessionId(signal, env, parentPid, parentInspection, username, hostId, inspector) {
128
128
  if (signal.sessionId)
129
129
  return `harness:${signal.sessionId}`;
130
- const terminalId = resolveTerminalSessionId(env);
131
- if (terminalId)
132
- return terminalId;
133
130
  const harnessRoot = findHarnessRootInAncestry(signal.harness, parentPid, parentInspection, inspector);
134
131
  if (harnessRoot) {
135
132
  return `pid:${harnessRoot.pid}@${harnessRoot.startTime}`;
136
133
  }
134
+ const terminalId = resolveTerminalSessionId(env);
135
+ if (terminalId)
136
+ return terminalId;
137
137
  if (parentInspection?.startTime) {
138
138
  return `pid:${parentPid}@${parentInspection.startTime}`;
139
139
  }
@@ -224,7 +224,7 @@ function detectHarnessSignal(env) {
224
224
  if (env.CLAUDECODE === "1") {
225
225
  return {
226
226
  harness: "claude",
227
- sessionId: null,
227
+ sessionId: nonEmpty(env.CLAUDE_CODE_SESSION_ID),
228
228
  pidHint: parsePositiveInteger(env.CMUX_CLAUDE_PID)
229
229
  };
230
230
  }
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@ export { applyPragmas, assertLocalFilesystem, detectFilesystemType, migrate, ope
4
4
  export { ProtocolError, isProtocolError } from "./errors.js";
5
5
  export { deriveHarnessCliIdentity, deriveHumanCliIdentity, deriveMcpHarnessIdentity } from "./identity.js";
6
6
  export { ancestorPaths, canonicalizeContextPath, resolveContextPath, resolveWorkspaceRoot } from "./path-resolution.js";
7
- export { createMcpServer, runStdioServer } from "./mcp-server.js";
8
- export { SUPPORTED_HARNESSES, MissingHarnessError, detectHarness, parseHarnessList, planInstall, planUninstall, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
7
+ export { SUPPORTED_HARNESSES, MissingHarnessError, detectHarness, parseHarnessList, planUninstall, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
9
8
  export { DEFAULT_SKILL_NAME, planSkillInstall, planSkillUninstall, resolveBundledSkillPath, resolveSkillTargetPath, syncInstalledSkills } from "./skill-install.js";
9
+ export { readPackageVersion, readUpdateMigrationState, resolveUpdateMigrationStatePath, runFirstRunMcpMigration, runStaleMcpCleanup, writeUpdateMigrationState } from "./update-migration.js";
10
10
  export { createSystemProcessInspector, terminateKnownProcess } from "./process-utils.js";
11
11
  export { clearCliSessionLease, findCliSessionByRoom, findCliSessionForContextPath, readCliSessions, removeCliSession, removeCliSessionsForRoom, resolveCliSessionPath, upsertCliSession, upsertJoinedCliSession, writeCliSessions } from "./session-store.js";
12
12
  export { TalkingStickService, createDefaultProcessLivenessChecker } from "./service.js";
@@ -0,0 +1,21 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export class FileAuditLog {
4
+ filePath;
5
+ constructor(filePath) {
6
+ this.filePath = filePath;
7
+ }
8
+ append(entry) {
9
+ const fullEntry = { ts: entry.ts ?? new Date().toISOString(), ...entry };
10
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
11
+ fs.appendFileSync(this.filePath, `${JSON.stringify(fullEntry)}\n`, "utf8");
12
+ }
13
+ }
14
+ export class NoopAuditLog {
15
+ append() {
16
+ // intentionally blank
17
+ }
18
+ }
19
+ export function defaultAuditLogPath(dataDir) {
20
+ return path.join(dataDir, "update-migrations.log");
21
+ }