talking-stick 0.2.0 → 0.4.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 +51 -49
- package/dist/cli/install-commands.js +83 -36
- package/dist/cli/instructions-commands.js +113 -0
- package/dist/cli/output.js +5 -2
- package/dist/cli/registry.js +18 -27
- 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 +3 -2
- package/dist/install-audit.js +21 -0
- package/dist/install-migration.js +84 -0
- package/dist/install.js +0 -69
- package/dist/instructions.js +256 -0
- 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/2026-05-06-harness-instructions-v6-converged.md +220 -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/releases/0.4.0.md +71 -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 +132 -104
- package/dist/mcp-server.js +0 -244
- package/dist/server.js +0 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Talking Stick
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
5
|
+
**Version:** 0.4.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.
|
|
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
|
|
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. To tune the default collaboration prompt without editing installed package files, run `tt instructions edit`.
|
|
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.
|
|
49
|
+
| **From npm** | `npm i -g talking-stick` | Published as `0.4.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
|
-
|
|
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,41 @@ tt uninstall --all
|
|
|
86
86
|
|
|
87
87
|
## What it gives your agent
|
|
88
88
|
|
|
89
|
-
Once installed, each agent harness
|
|
89
|
+
Once installed, each agent harness has a skill that tells it to coordinate through the `tt` CLI:
|
|
90
90
|
|
|
91
91
|
```
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
104
|
+
tt instructions — editable collaboration prompt loaded by the skill
|
|
107
105
|
```
|
|
108
106
|
|
|
109
107
|
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
108
|
|
|
111
|
-
The skill
|
|
109
|
+
The global skill tells the model when to join, wait, verify its guardian, take over, leave notes, send messages, and hand off.
|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
## Editable collaboration instructions
|
|
112
|
+
|
|
113
|
+
The bundled skill is the safety floor. It is intentionally small and package-managed. Local collaboration preferences live in editable Markdown files that `tt instructions` shows to agents after they join.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
tt instructions show # effective prompt for the detected harness
|
|
117
|
+
tt instructions show --harness codex # view one harness's effective prompt
|
|
118
|
+
tt instructions edit # edit user defaults
|
|
119
|
+
tt instructions edit --project # edit this repo's overrides
|
|
120
|
+
tt instructions reset --project # remove this repo's override
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Effective instructions are layered in this order: bundled defaults, user defaults at `${TALKING_STICK_DATA_DIR}/instructions.md` (normally `~/.local/share/talking-stick/instructions.md`), then project overrides at `.talking-stick/instructions.md` in the workspace root. User and project files are created lazily on first edit, so installing `tt` does not litter repositories or harness config directories.
|
|
115
124
|
|
|
116
125
|
## Non-owner notes
|
|
117
126
|
|
|
@@ -135,7 +144,9 @@ tt events --wait|--follow [--event TYPE[,TYPE]] [--target self|any|agent]
|
|
|
135
144
|
- `--interrupt` marks the message time-sensitive; receivers decide whether to act on it now.
|
|
136
145
|
- `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
146
|
- `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.
|
|
147
|
+
- `tt events --wait` and `tt events --follow` default to `--target self`; pass `--target any` only for audit/debug views.
|
|
138
148
|
- `wait_for_events` is observer-safe: it never mutates room state, so non-holders can use it freely without disturbing turn-fairness bookkeeping.
|
|
149
|
+
- 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
150
|
|
|
140
151
|
**When to message vs note vs handoff.**
|
|
141
152
|
|
|
@@ -145,26 +156,11 @@ tt events --wait|--follow [--event TYPE[,TYPE]] [--target self|any|agent]
|
|
|
145
156
|
|
|
146
157
|
**`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
158
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`tt install` installs both pieces a harness needs: the MCP server registration and the bundled `talking-stick` skill.
|
|
159
|
+
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.
|
|
151
160
|
|
|
152
|
-
|
|
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` |
|
|
160
|
-
|
|
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
|
|
161
|
+
## How installation works per harness
|
|
166
162
|
|
|
167
|
-
|
|
163
|
+
`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
164
|
|
|
169
165
|
- Claude Code: copied or linked into `~/.claude/skills/talking-stick`
|
|
170
166
|
- Codex: copied or linked into `~/.codex/skills/talking-stick`
|
|
@@ -173,6 +169,8 @@ Talking Stick also ships with a portable `talking-stick` skill:
|
|
|
173
169
|
|
|
174
170
|
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
171
|
|
|
172
|
+
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`.
|
|
173
|
+
|
|
176
174
|
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
175
|
|
|
178
176
|
## Human CLI
|
|
@@ -184,12 +182,15 @@ tt whoami [--explain] # show the resolved C
|
|
|
184
182
|
tt list [path] # list rooms
|
|
185
183
|
tt join [path] [--force-new] # join the room for path
|
|
186
184
|
tt leave [path] # leave the room for path
|
|
187
|
-
tt wait [path] [--timeout
|
|
185
|
+
tt wait [path] [--timeout 110s] # block until your turn
|
|
188
186
|
tt try [path] # non-blocking claim attempt
|
|
189
187
|
tt state [path] # full room state
|
|
190
188
|
tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent] # room event log; --wait/--follow long-polls
|
|
191
189
|
tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR] # send an OOB message
|
|
192
190
|
tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR] # receive OOB messages
|
|
191
|
+
tt instructions show [path] [--harness claude|codex|gemini|opencode|all] [--scope effective|bundled|user|project] # show collaboration prompt
|
|
192
|
+
tt instructions edit [path] [--user|--project] # edit user or project prompt
|
|
193
|
+
tt instructions reset [path] (--user|--project) # delete a user or project prompt
|
|
193
194
|
tt release [path] --status TEXT --next-action TEXT # normal handoff
|
|
194
195
|
tt pass [path] --status TEXT --next-action TEXT # pass/end your turn
|
|
195
196
|
tt assign <target|next> [path] --status TEXT --next-action TEXT # explicit handoff
|
|
@@ -197,13 +198,14 @@ tt take [path] [--reason TEXT] # human-friendly take/
|
|
|
197
198
|
tt takeover [path] [--reason TEXT] # alias for take
|
|
198
199
|
tt notes add <body> [--turn N] [--path DIR] [--stdin] # leave an async note
|
|
199
200
|
tt notes list [--all] [--after ID] [--limit N] [--path DIR] # read notes
|
|
200
|
-
tt
|
|
201
|
-
tt
|
|
202
|
-
tt uninstall <harness...> | --all [--print] # remove MCP server and skill
|
|
201
|
+
tt install <harness...> | --all [--print] [--copy] [--link] # install skill and clean stale MCP entries
|
|
202
|
+
tt uninstall <harness...> | --all [--print] # remove skill and stale MCP entries
|
|
203
203
|
tt self-update [--print] [--manager npm|pnpm|yarn|bun] # update to the latest published tt
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
`
|
|
206
|
+
`[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.
|
|
207
|
+
|
|
208
|
+
`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
209
|
|
|
208
210
|
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
211
|
|
|
@@ -231,7 +233,7 @@ Use `tt whoami --explain` to see which identity path the CLI chose.
|
|
|
231
233
|
- **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
234
|
- **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
235
|
- **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.**
|
|
236
|
+
- **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
237
|
|
|
236
238
|
## Storage
|
|
237
239
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { SUPPORTED_HARNESSES, detectHarness, parseHarnessList,
|
|
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,34 @@ export async function runInstallCommand(parsed) {
|
|
|
14
18
|
skipMissing: true
|
|
15
19
|
};
|
|
16
20
|
if (dryRun) {
|
|
17
|
-
for (const action of
|
|
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) =>
|
|
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");
|
|
32
|
+
printInstructionHint(results);
|
|
24
33
|
}
|
|
25
34
|
export async function runUninstallCommand(parsed) {
|
|
26
35
|
normalizeBooleanFlag(parsed, "print");
|
|
27
36
|
const harnesses = selectHarnesses(parsed);
|
|
28
37
|
const dryRun = hasOption(parsed, "print");
|
|
29
38
|
const installOptions = { skipMissing: true };
|
|
30
|
-
const actions =
|
|
39
|
+
const actions = planUninstallActions(harnesses, installOptions);
|
|
31
40
|
if (dryRun) {
|
|
32
41
|
for (const action of actions) {
|
|
33
42
|
printActionPlan(action);
|
|
34
43
|
}
|
|
35
44
|
return;
|
|
36
45
|
}
|
|
37
|
-
const results = (await Promise.all(harnesses.map((harness) =>
|
|
46
|
+
const results = (await Promise.all(harnesses.map((harness) => runSkillUninstall(harness, installOptions)))).flat();
|
|
38
47
|
reportInstallResults(results, "uninstall");
|
|
48
|
+
reportCleanupResults(await runCleanup(harnesses, "uninstall", installOptions), "uninstall");
|
|
39
49
|
}
|
|
40
50
|
export async function runInstallSkillCommand(parsed) {
|
|
41
51
|
normalizeBooleanFlag(parsed, "print");
|
|
@@ -85,6 +95,7 @@ export async function runSelfUpdateCommand(parsed, cliEntryUrl) {
|
|
|
85
95
|
const binaryPath = resolveCurrentBinaryPath(cliEntryUrl);
|
|
86
96
|
source = detectInstallSource({ binaryPath });
|
|
87
97
|
}
|
|
98
|
+
const packageVersionFrom = readPackageVersion(cliEntryUrl);
|
|
88
99
|
const plan = planSelfUpdate(source);
|
|
89
100
|
if (!plan) {
|
|
90
101
|
if (source === "dev") {
|
|
@@ -98,7 +109,29 @@ export async function runSelfUpdateCommand(parsed, cliEntryUrl) {
|
|
|
98
109
|
}
|
|
99
110
|
process.stdout.write(`Updating via: ${plan.description}\n`);
|
|
100
111
|
await runInheritIo(plan.command, plan.args);
|
|
101
|
-
|
|
112
|
+
const packageVersionTo = readPackageVersion(cliEntryUrl);
|
|
113
|
+
const cleanup = await runStaleMcpCleanup({
|
|
114
|
+
harnesses: "all",
|
|
115
|
+
reason: "update",
|
|
116
|
+
packageVersionFrom,
|
|
117
|
+
packageVersionTo,
|
|
118
|
+
installOptions: { skipMissing: true }
|
|
119
|
+
});
|
|
120
|
+
reportCleanupResults(cleanup.results, "self-update");
|
|
121
|
+
process.stdout.write("Done. Restart any long-running harness sessions to pick up the new tt.\n");
|
|
122
|
+
}
|
|
123
|
+
export async function runMcpMigrationCommand(parsed) {
|
|
124
|
+
normalizeBooleanFlag(parsed, "quiet");
|
|
125
|
+
const reason = parseAuditReason(getStringOption(parsed, "reason") ?? "manual");
|
|
126
|
+
const quiet = hasOption(parsed, "quiet");
|
|
127
|
+
const cleanup = await runStaleMcpCleanup({
|
|
128
|
+
harnesses: "all",
|
|
129
|
+
reason,
|
|
130
|
+
installOptions: { skipMissing: true }
|
|
131
|
+
});
|
|
132
|
+
if (!quiet) {
|
|
133
|
+
reportCleanupResults(cleanup.results, "self-update");
|
|
134
|
+
}
|
|
102
135
|
}
|
|
103
136
|
function resolveSkillInstallLinkMode(parsed) {
|
|
104
137
|
const wantsCopy = hasOption(parsed, "copy");
|
|
@@ -111,48 +144,39 @@ function resolveSkillInstallLinkMode(parsed) {
|
|
|
111
144
|
}
|
|
112
145
|
return true;
|
|
113
146
|
}
|
|
114
|
-
function
|
|
115
|
-
return harnesses.
|
|
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
|
-
});
|
|
147
|
+
function planInstallActions(harnesses, installOptions) {
|
|
148
|
+
return harnesses.map((harness) => planSkillInstall(harness, installOptions));
|
|
130
149
|
}
|
|
131
|
-
function
|
|
150
|
+
function planUninstallActions(harnesses, installOptions) {
|
|
132
151
|
return harnesses.flatMap((harness) => [
|
|
133
|
-
planUninstall(harness, installOptions),
|
|
134
152
|
planSkillUninstall(harness, {
|
|
135
153
|
...installOptions,
|
|
136
154
|
skipMissing: false
|
|
137
|
-
})
|
|
155
|
+
}),
|
|
156
|
+
planUninstall(harness, installOptions)
|
|
138
157
|
]);
|
|
139
158
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return [mcpResult];
|
|
145
|
-
}
|
|
159
|
+
function planCleanupActions(harnesses, installOptions) {
|
|
160
|
+
return harnesses.map((harness) => planUninstall(harness, installOptions));
|
|
161
|
+
}
|
|
162
|
+
async function runSkillInstall(harness, installOptions) {
|
|
146
163
|
const skillAction = planSkillInstall(harness, installOptions);
|
|
147
164
|
const skillResult = await runAction(skillAction, installOptions);
|
|
148
|
-
return [
|
|
165
|
+
return [skillResult];
|
|
149
166
|
}
|
|
150
|
-
async function
|
|
151
|
-
const mcpAction = planUninstall(harness, installOptions);
|
|
152
|
-
const mcpResult = await runAction(mcpAction, installOptions);
|
|
167
|
+
async function runSkillUninstall(harness, installOptions) {
|
|
153
168
|
const skillAction = planSkillUninstall(harness, installOptions);
|
|
154
169
|
const skillResult = await runAction(skillAction, installOptions);
|
|
155
|
-
return [
|
|
170
|
+
return [skillResult];
|
|
171
|
+
}
|
|
172
|
+
async function runCleanup(harnesses, reason, installOptions) {
|
|
173
|
+
const dataDir = resolveDataDir();
|
|
174
|
+
return removeStaleMcpRegistrations({
|
|
175
|
+
harnesses,
|
|
176
|
+
reason,
|
|
177
|
+
audit: new FileAuditLog(defaultAuditLogPath(dataDir)),
|
|
178
|
+
installOptions
|
|
179
|
+
});
|
|
156
180
|
}
|
|
157
181
|
function selectHarnesses(parsed) {
|
|
158
182
|
if (hasOption(parsed, "all")) {
|
|
@@ -199,6 +223,29 @@ function reportInstallResults(results, mode) {
|
|
|
199
223
|
throw new Error(`${mode} completed with failures.`);
|
|
200
224
|
}
|
|
201
225
|
}
|
|
226
|
+
function printInstructionHint(results) {
|
|
227
|
+
if (!results.some((result) => result.ok && !result.skipped)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
process.stdout.write("Customize collaboration instructions with: tt instructions edit\n");
|
|
231
|
+
}
|
|
232
|
+
function reportCleanupResults(results, mode) {
|
|
233
|
+
let anyFailed = false;
|
|
234
|
+
for (const result of results) {
|
|
235
|
+
process.stdout.write(`[${result.harness}] mcp-cleanup ${result.action}: ${result.message}\n`);
|
|
236
|
+
if (result.action === "failed")
|
|
237
|
+
anyFailed = true;
|
|
238
|
+
}
|
|
239
|
+
if (anyFailed) {
|
|
240
|
+
throw new Error(`${mode} completed with MCP cleanup failures.`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
202
243
|
function formatInstallStatus(status) {
|
|
203
244
|
return status.replaceAll("_", "-");
|
|
204
245
|
}
|
|
246
|
+
function parseAuditReason(value) {
|
|
247
|
+
if (value === "update" || value === "first-run" || value === "uninstall" || value === "manual") {
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`--reason must be one of update | first-run | uninstall | manual (got ${value}).`);
|
|
251
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { editInstructions, parseInstructionScope, resetInstructions, showInstructions } from "../instructions.js";
|
|
2
|
+
import { deriveCliIdentity } from "./identity.js";
|
|
3
|
+
import { getStringOption, hasOption } from "./parser.js";
|
|
4
|
+
import { printResult } from "./output.js";
|
|
5
|
+
export async function handleInstructionsCommand(parsed) {
|
|
6
|
+
const [subcommand = "show", ...rest] = parsed.positionals;
|
|
7
|
+
const subParsed = {
|
|
8
|
+
name: `instructions ${subcommand}`,
|
|
9
|
+
positionals: rest,
|
|
10
|
+
options: parsed.options
|
|
11
|
+
};
|
|
12
|
+
switch (subcommand) {
|
|
13
|
+
case "show":
|
|
14
|
+
handleInstructionsShowCommand(subParsed);
|
|
15
|
+
return;
|
|
16
|
+
case "edit":
|
|
17
|
+
await handleInstructionsEditCommand(subParsed);
|
|
18
|
+
return;
|
|
19
|
+
case "reset":
|
|
20
|
+
handleInstructionsResetCommand(subParsed);
|
|
21
|
+
return;
|
|
22
|
+
default:
|
|
23
|
+
throw new Error(`Unknown instructions subcommand: ${subcommand}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function handleInstructionsShowCommand(parsed) {
|
|
27
|
+
repairBooleanFlag(parsed, "json", 0);
|
|
28
|
+
repairBooleanFlag(parsed, "text", 0);
|
|
29
|
+
const contextPath = resolveContextPathArg(parsed);
|
|
30
|
+
const scope = parseInstructionScope(getStringOption(parsed, "scope"));
|
|
31
|
+
const identity = deriveCliIdentity(parsed);
|
|
32
|
+
const result = showInstructions({
|
|
33
|
+
harness: getStringOption(parsed, "harness"),
|
|
34
|
+
scope,
|
|
35
|
+
options: {
|
|
36
|
+
contextPath,
|
|
37
|
+
identity
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
printResult(parsed, result, () => {
|
|
41
|
+
if (result.text.trim().length > 0) {
|
|
42
|
+
return result.text;
|
|
43
|
+
}
|
|
44
|
+
return `No ${result.scope} instructions found for ${result.harness}.`;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function handleInstructionsEditCommand(parsed) {
|
|
48
|
+
repairBooleanFlag(parsed, "json", 0);
|
|
49
|
+
repairBooleanFlag(parsed, "text", 0);
|
|
50
|
+
repairBooleanFlag(parsed, "user", 0);
|
|
51
|
+
repairBooleanFlag(parsed, "project", 0);
|
|
52
|
+
const contextPath = resolveContextPathArg(parsed);
|
|
53
|
+
const scope = resolveEditableScope(parsed, false);
|
|
54
|
+
const result = await editInstructions({
|
|
55
|
+
scope,
|
|
56
|
+
options: { contextPath }
|
|
57
|
+
});
|
|
58
|
+
printResult(parsed, result, () => {
|
|
59
|
+
if (result.opened) {
|
|
60
|
+
return `Opened ${result.scope} instructions: ${result.path}`;
|
|
61
|
+
}
|
|
62
|
+
return [
|
|
63
|
+
`Instructions file ready: ${result.path}`,
|
|
64
|
+
"Set $VISUAL or $EDITOR to edit it from this command."
|
|
65
|
+
].join("\n");
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function handleInstructionsResetCommand(parsed) {
|
|
69
|
+
repairBooleanFlag(parsed, "json", 0);
|
|
70
|
+
repairBooleanFlag(parsed, "text", 0);
|
|
71
|
+
repairBooleanFlag(parsed, "user", 0);
|
|
72
|
+
repairBooleanFlag(parsed, "project", 0);
|
|
73
|
+
const contextPath = resolveContextPathArg(parsed);
|
|
74
|
+
const scope = resolveEditableScope(parsed, true);
|
|
75
|
+
const result = resetInstructions({
|
|
76
|
+
scope,
|
|
77
|
+
options: { contextPath }
|
|
78
|
+
});
|
|
79
|
+
printResult(parsed, result, () => {
|
|
80
|
+
if (result.removed) {
|
|
81
|
+
return `Removed ${result.scope} instructions: ${result.path}`;
|
|
82
|
+
}
|
|
83
|
+
return `No ${result.scope} instructions file at ${result.path}`;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function resolveEditableScope(parsed, requireExplicit) {
|
|
87
|
+
const wantsUser = hasOption(parsed, "user");
|
|
88
|
+
const wantsProject = hasOption(parsed, "project");
|
|
89
|
+
if (wantsUser && wantsProject) {
|
|
90
|
+
throw new Error("Pass only one of --user or --project.");
|
|
91
|
+
}
|
|
92
|
+
if (wantsProject) {
|
|
93
|
+
return "project";
|
|
94
|
+
}
|
|
95
|
+
if (wantsUser || !requireExplicit) {
|
|
96
|
+
return "user";
|
|
97
|
+
}
|
|
98
|
+
throw new Error("Pass --user or --project to choose which instructions to reset.");
|
|
99
|
+
}
|
|
100
|
+
function resolveContextPathArg(parsed) {
|
|
101
|
+
const pathOption = parsed.options.get("path");
|
|
102
|
+
if (pathOption === true) {
|
|
103
|
+
throw new Error("--path requires a value.");
|
|
104
|
+
}
|
|
105
|
+
return pathOption ?? parsed.positionals[0] ?? process.cwd();
|
|
106
|
+
}
|
|
107
|
+
function repairBooleanFlag(parsed, key, insertAt) {
|
|
108
|
+
const value = parsed.options.get(key);
|
|
109
|
+
if (typeof value === "string") {
|
|
110
|
+
parsed.positionals.splice(insertAt, 0, value);
|
|
111
|
+
parsed.options.set(key, true);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/cli/output.js
CHANGED
|
@@ -123,12 +123,15 @@ 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
|
|
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]
|
|
130
130
|
tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR]
|
|
131
131
|
tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR]
|
|
132
|
+
tt instructions show [path] [--harness claude|codex|gemini|opencode|all] [--scope effective|bundled|user|project]
|
|
133
|
+
tt instructions edit [path] [--user|--project]
|
|
134
|
+
tt instructions reset [path] (--user|--project)
|
|
132
135
|
tt release [path] (--status TEXT --next-action TEXT | --stdin)
|
|
133
136
|
tt pass [path] (--status TEXT --next-action TEXT | --stdin)
|
|
134
137
|
tt assign <target|next> [path] (--status TEXT --next-action TEXT | --stdin)
|
|
@@ -136,7 +139,6 @@ Commands:
|
|
|
136
139
|
tt takeover [path] [--reason TEXT] [--operator-requested]
|
|
137
140
|
tt notes add <body> [--turn N] [--path DIR] [--stdin]
|
|
138
141
|
tt notes list [--all] [--after NOTE_ID] [--limit N] [--path DIR]
|
|
139
|
-
tt mcp
|
|
140
142
|
tt install <harness...> | --all [--print] [--copy] [--link]
|
|
141
143
|
tt uninstall <harness...> | --all [--print]
|
|
142
144
|
tt self-update [--print] [--manager npm|pnpm|yarn|bun]
|
|
@@ -144,6 +146,7 @@ Commands:
|
|
|
144
146
|
Harnesses: ${SUPPORTED_HARNESSES.join(", ")}
|
|
145
147
|
|
|
146
148
|
Common options:
|
|
149
|
+
[path] Defaults to the current working directory when omitted
|
|
147
150
|
--agent ID Override the default human identity
|
|
148
151
|
--json Force JSON output (also default when invoked from a harness)
|
|
149
152
|
--text Force human-readable text even when invoked from a harness
|
package/dist/cli/registry.js
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import { runStdioServer } from "../index.js";
|
|
2
1
|
import { runGuardCommand } from "./guardian.js";
|
|
3
|
-
import { runInstallCommand,
|
|
2
|
+
import { runInstallCommand, runMcpMigrationCommand, runSelfUpdateCommand, runUninstallCommand } from "./install-commands.js";
|
|
3
|
+
import { handleInstructionsCommand } from "./instructions-commands.js";
|
|
4
4
|
import { handleMsgCommand } from "./msg-commands.js";
|
|
5
5
|
import { handleNotesCommand } from "./notes-commands.js";
|
|
6
6
|
import { handleEventsCommand, handleJoinCommand, handleKickCommand, handleLeaveCommand, handleListCommand, handleStateCommand, handleWhoAmICommand } from "./room-commands.js";
|
|
7
7
|
import { handleAssignCommand, handlePassCommand, handleReleaseCommand, handleTakeCommand, handleWaitCommand } from "./turn-commands.js";
|
|
8
8
|
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
9
|
{
|
|
19
10
|
name: "guard",
|
|
20
11
|
needsRuntime: false,
|
|
@@ -30,7 +21,7 @@ export const COMMAND_REGISTRY = [
|
|
|
30
21
|
startupMaintenance: false,
|
|
31
22
|
internal: false,
|
|
32
23
|
usage: "tt install <harness...> | --all [--print] [--copy] [--link]",
|
|
33
|
-
description: "Install Talking Stick
|
|
24
|
+
description: "Install the Talking Stick skill and remove stale MCP registrations.",
|
|
34
25
|
handler: ({ parsed }) => runInstallCommand(parsed)
|
|
35
26
|
},
|
|
36
27
|
{
|
|
@@ -39,35 +30,35 @@ export const COMMAND_REGISTRY = [
|
|
|
39
30
|
startupMaintenance: false,
|
|
40
31
|
internal: false,
|
|
41
32
|
usage: "tt uninstall <harness...> | --all [--print]",
|
|
42
|
-
description: "Remove Talking Stick
|
|
33
|
+
description: "Remove the Talking Stick skill and stale MCP registrations.",
|
|
43
34
|
handler: ({ parsed }) => runUninstallCommand(parsed)
|
|
44
35
|
},
|
|
45
36
|
{
|
|
46
|
-
name: "
|
|
37
|
+
name: "self-update",
|
|
47
38
|
needsRuntime: false,
|
|
48
39
|
startupMaintenance: false,
|
|
49
40
|
internal: false,
|
|
50
|
-
usage: "tt
|
|
51
|
-
description: "
|
|
52
|
-
handler: ({ parsed }) =>
|
|
41
|
+
usage: "tt self-update [--print] [--manager npm|pnpm|yarn|bun]",
|
|
42
|
+
description: "Update the globally installed tt package.",
|
|
43
|
+
handler: ({ parsed, cliEntryUrl }) => runSelfUpdateCommand(parsed, cliEntryUrl)
|
|
53
44
|
},
|
|
54
45
|
{
|
|
55
|
-
name: "
|
|
46
|
+
name: "migrate-mcp",
|
|
56
47
|
needsRuntime: false,
|
|
57
48
|
startupMaintenance: false,
|
|
58
|
-
internal:
|
|
59
|
-
usage: "tt
|
|
60
|
-
description: "Remove
|
|
61
|
-
handler: ({ parsed }) =>
|
|
49
|
+
internal: true,
|
|
50
|
+
usage: "tt migrate-mcp [--reason update|first-run|uninstall|manual] [--quiet]",
|
|
51
|
+
description: "Remove stale Talking Stick MCP registrations.",
|
|
52
|
+
handler: ({ parsed }) => runMcpMigrationCommand(parsed)
|
|
62
53
|
},
|
|
63
54
|
{
|
|
64
|
-
name: "
|
|
55
|
+
name: "instructions",
|
|
65
56
|
needsRuntime: false,
|
|
66
57
|
startupMaintenance: true,
|
|
67
58
|
internal: false,
|
|
68
|
-
usage: "tt
|
|
69
|
-
description: "
|
|
70
|
-
handler: ({ parsed
|
|
59
|
+
usage: "tt instructions [show|edit|reset] [--harness NAME] [--scope SCOPE]",
|
|
60
|
+
description: "Show, edit, or reset editable collaboration instructions.",
|
|
61
|
+
handler: ({ parsed }) => handleInstructionsCommand(parsed)
|
|
71
62
|
},
|
|
72
63
|
{
|
|
73
64
|
name: "whoami",
|
|
@@ -146,7 +137,7 @@ export const COMMAND_REGISTRY = [
|
|
|
146
137
|
needsRuntime: true,
|
|
147
138
|
startupMaintenance: true,
|
|
148
139
|
internal: false,
|
|
149
|
-
usage: "tt wait [path] [--timeout
|
|
140
|
+
usage: "tt wait [path] [--timeout 110s]",
|
|
150
141
|
description: "Wait until this agent can claim the stick.",
|
|
151
142
|
handler: ({ runtime, parsed, cliEntryUrl }) => handleWaitCommand(requireRuntime(runtime), parsed, false, cliEntryUrl)
|
|
152
143
|
},
|
|
@@ -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: "
|
|
138
|
+
default_target: "self",
|
|
139
139
|
force_tail_cursor: false
|
|
140
140
|
});
|
|
141
141
|
return;
|