talking-stick 0.1.0-alpha.2 → 0.1.0-alpha.3
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 +14 -6
- package/dist/cli.js +231 -23
- package/dist/commands.js +2 -1
- package/dist/config.js +2 -1
- package/dist/db.js +7 -0
- package/dist/index.js +1 -1
- package/dist/self-update.js +74 -0
- package/dist/service.js +144 -21
- package/dist/skill-install.js +106 -0
- package/docs/cli-refactor-plan.md +178 -0
- package/docs/releases/0.1.0-alpha.3.md +102 -0
- package/docs/talking-stick-plan.md +18 -15
- package/package.json +1 -1
- package/skills/talking-stick/SKILL.md +19 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
5
|
-
**Version:** 0.1.0-alpha.
|
|
5
|
+
**Version:** 0.1.0-alpha.3. Multi-process-safe (SQLite WAL), liveness-aware, no daemon. Supports Claude Code, Codex CLI, Gemini CLI, and OpenCode out of the box.
|
|
6
6
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
@@ -27,7 +27,7 @@ That's it. The next time two agents `cd` into the same repo, they see each other
|
|
|
27
27
|
|
|
28
28
|
| Method | Command | Notes |
|
|
29
29
|
|---|---|---|
|
|
30
|
-
| **From npm** | `npm i -g talking-stick` | Published as `0.1.0-alpha.
|
|
30
|
+
| **From npm** | `npm i -g talking-stick` | Published as `0.1.0-alpha.3`. Requires Node ≥ 22. |
|
|
31
31
|
| **From GitHub** | `npm i -g github:mostlydev/talking-stick` | Tracks the `master` branch; builds on install via the `prepare` hook. |
|
|
32
32
|
| **From source** | `git clone … && npm install && npm link` | For contributors. |
|
|
33
33
|
|
|
@@ -67,7 +67,7 @@ list_rooms — which rooms exist under a path
|
|
|
67
67
|
join_path — join the room for this workspace
|
|
68
68
|
wait_for_turn — block until the stick is available, with takeover signals
|
|
69
69
|
heartbeat — prove liveness while holding the stick
|
|
70
|
-
release_stick — normal handoff to the next
|
|
70
|
+
release_stick — normal handoff to the next fair waiter, with structured Handoff
|
|
71
71
|
pass_stick — explicit handoff to a named agent
|
|
72
72
|
takeover_stick — deliberate claim when the prior holder is gone/stuck
|
|
73
73
|
get_room_state — authoritative state projection
|
|
@@ -117,6 +117,8 @@ Talking Stick also ships with a portable `talking-stick` skill:
|
|
|
117
117
|
|
|
118
118
|
By default, `tt install-skill` links the bundled skill into each harness so local updates are picked up immediately. Pass `--copy` if you want a standalone snapshot instead.
|
|
119
119
|
|
|
120
|
+
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-skill gemini` after updating when needed.
|
|
121
|
+
|
|
120
122
|
## Human CLI
|
|
121
123
|
|
|
122
124
|
The same `tt` binary also works as a human CLI, useful for watching or participating in a room from your terminal:
|
|
@@ -130,8 +132,10 @@ tt try [path] # non-blocking claim a
|
|
|
130
132
|
tt state [path] # full room state
|
|
131
133
|
tt events [path] [--after N] [--limit N] # room event log
|
|
132
134
|
tt release [path] --status TEXT --next-action TEXT # normal handoff
|
|
133
|
-
tt pass [
|
|
134
|
-
tt
|
|
135
|
+
tt pass [path] --status TEXT --next-action TEXT # pass/end your turn
|
|
136
|
+
tt assign <target|next> [path] --status TEXT --next-action TEXT # explicit handoff
|
|
137
|
+
tt take [path] [--reason TEXT] # human-friendly take/override
|
|
138
|
+
tt takeover [path] [--reason TEXT] # alias for take
|
|
135
139
|
tt notes add <body> [--turn N] [--path DIR] [--stdin] # leave an async note
|
|
136
140
|
tt notes list [--all] [--after ID] [--limit N] [--path DIR] # read notes
|
|
137
141
|
tt mcp # run the MCP stdio server
|
|
@@ -139,9 +143,12 @@ tt install <harness...> | --all [--print] # register MCP server
|
|
|
139
143
|
tt uninstall <harness...> | --all [--print] # remove MCP server
|
|
140
144
|
tt install-skill <harness...> | --all [--print] [--copy] [--link] # install global talking-stick skill
|
|
141
145
|
tt uninstall-skill <harness...> | --all [--print] # remove global talking-stick skill
|
|
146
|
+
tt self-update [--print] [--manager npm|pnpm|yarn|bun] # update to the latest published tt
|
|
142
147
|
```
|
|
143
148
|
|
|
144
|
-
|
|
149
|
+
`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.
|
|
150
|
+
|
|
151
|
+
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`.
|
|
145
152
|
|
|
146
153
|
### CLI identity
|
|
147
154
|
|
|
@@ -160,6 +167,7 @@ Use `tt whoami --explain` to see which identity path the CLI chose.
|
|
|
160
167
|
|
|
161
168
|
- **Workspace-root room resolution.** An agent at any depth under `/repo/` joins the `/repo/` room automatically. Nested rooms require explicit `force_new`.
|
|
162
169
|
- **Structured handoffs.** `release_stick` and `pass_stick` carry a typed `Handoff` with required `status` / `next_action` and optional `artifacts[]` pointing at specific files and line ranges.
|
|
170
|
+
- **Fair handoff selection.** Normal release prefers a recent waiter that is new or has gone longest without holding the stick; if the best-known candidate is between wait polls, a short grace window prevents immediate recycling to a less-fair claimant.
|
|
163
171
|
- **Fencing tokens.** `lease_id` + `turn_id` make stale writes impossible — an agent who lost their turn cannot commit anything under the room's name.
|
|
164
172
|
- **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.
|
|
165
173
|
- **Multi-process safe.** Shared SQLite with WAL mode, `BEGIN IMMEDIATE` writes, 250 ms polling for `wait_for_turn`. No daemon required.
|
package/dist/cli.js
CHANGED
|
@@ -5,12 +5,15 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { clearCliSessionLease, createSystemProcessInspector, deriveHarnessCliIdentity, deriveHumanCliIdentity, findCliSessionByRoom, findCliSessionForContextPath, isProtocolError, resolveCliSessionPath, runStdioServer, TalkingStickCommands, TalkingStickService, terminateKnownProcess, upsertCliSession, upsertJoinedCliSession } from "./index.js";
|
|
7
7
|
import { SUPPORTED_HARNESSES, detectHarness, parseHarnessList, planInstall, planUninstall, runAction } from "./install.js";
|
|
8
|
-
import { planSkillInstall, planSkillUninstall } from "./skill-install.js";
|
|
8
|
+
import { planSkillInstall, planSkillUninstall, syncInstalledSkills } from "./skill-install.js";
|
|
9
9
|
import { resolveContextPath } from "./path-resolution.js";
|
|
10
|
+
import { detectInstallSource, isPackageManager, planSelfUpdate, resolveCurrentBinaryPath } from "./self-update.js";
|
|
10
11
|
const GUARD_READY = "READY";
|
|
12
|
+
const GUARD_READY_TIMEOUT_MS = 10_000;
|
|
11
13
|
const STALE_GUARD_ERRORS = new Set(["stale_lease", "turn_mismatch", "room_not_found"]);
|
|
12
14
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
13
15
|
const parsed = parseCommand(argv);
|
|
16
|
+
maybeSyncInstalledSkills(parsed);
|
|
14
17
|
if (!parsed.name || parsed.name === "help" || parsed.name === "--help") {
|
|
15
18
|
printHelp();
|
|
16
19
|
return;
|
|
@@ -39,6 +42,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
39
42
|
await runUninstallSkillCommand(parsed);
|
|
40
43
|
return;
|
|
41
44
|
}
|
|
45
|
+
if (parsed.name === "self-update") {
|
|
46
|
+
await runSelfUpdateCommand(parsed);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
42
49
|
if (parsed.name === "whoami") {
|
|
43
50
|
handleWhoAmICommand(parsed);
|
|
44
51
|
return;
|
|
@@ -64,6 +71,9 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
64
71
|
case "try":
|
|
65
72
|
await handleWaitCommand(runtime, parsed, true);
|
|
66
73
|
return;
|
|
74
|
+
case "take":
|
|
75
|
+
await handleTakeCommand(runtime, parsed);
|
|
76
|
+
return;
|
|
67
77
|
case "takeover":
|
|
68
78
|
await handleTakeoverCommand(runtime, parsed);
|
|
69
79
|
return;
|
|
@@ -73,6 +83,9 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
73
83
|
case "pass":
|
|
74
84
|
await handlePassCommand(runtime, parsed);
|
|
75
85
|
return;
|
|
86
|
+
case "assign":
|
|
87
|
+
await handleAssignCommand(runtime, parsed);
|
|
88
|
+
return;
|
|
76
89
|
case "notes":
|
|
77
90
|
await handleNotesCommand(runtime, parsed);
|
|
78
91
|
return;
|
|
@@ -269,21 +282,52 @@ async function handleWaitCommand(runtime, parsed, isTry) {
|
|
|
269
282
|
printResult(parsed, waitResult, () => formatWaitResult(waitResult));
|
|
270
283
|
}
|
|
271
284
|
async function handleTakeoverCommand(runtime, parsed) {
|
|
285
|
+
await handleTakeCommand(runtime, parsed);
|
|
286
|
+
}
|
|
287
|
+
async function handleTakeCommand(runtime, parsed) {
|
|
272
288
|
const contextPath = parsed.positionals[0] ?? process.cwd();
|
|
273
289
|
const identity = deriveCliIdentity(parsed);
|
|
290
|
+
const reason = resolveTakeoverReason(parsed);
|
|
291
|
+
const operatorOverride = shouldUseOperatorOverride(parsed);
|
|
274
292
|
const joined = runtime.commands.joinPath(identity, { context_path: contextPath });
|
|
275
293
|
upsertSessionFromJoin(identity, joined);
|
|
276
294
|
const availability = await runtime.commands.waitForTurn(identity, {
|
|
277
295
|
room_id: joined.room_id,
|
|
278
296
|
max_wait_ms: 0
|
|
279
297
|
});
|
|
280
|
-
if (availability.status
|
|
298
|
+
if (availability.status === "your_turn") {
|
|
299
|
+
const guardianPid = await spawnGuardian({
|
|
300
|
+
agentId: identity.agent_id,
|
|
301
|
+
canonicalPath: joined.canonical_path,
|
|
302
|
+
roomId: joined.room_id,
|
|
303
|
+
leaseId: availability.lease_id,
|
|
304
|
+
turnId: availability.turn_id
|
|
305
|
+
});
|
|
306
|
+
upsertCliSession(resolveCliSessionPath(), {
|
|
307
|
+
agent_id: identity.agent_id,
|
|
308
|
+
room_id: joined.room_id,
|
|
309
|
+
canonical_path: joined.canonical_path,
|
|
310
|
+
workspace_root: joined.workspace_root,
|
|
311
|
+
lease_id: availability.lease_id,
|
|
312
|
+
turn_id: availability.turn_id,
|
|
313
|
+
guardian_pid: guardianPid.pid,
|
|
314
|
+
guardian_process_started_at: guardianPid.process_started_at,
|
|
315
|
+
updated_at: new Date().toISOString()
|
|
316
|
+
});
|
|
317
|
+
printResult(parsed, { ...availability, guardian_pid: guardianPid.pid }, () => `Took the stick. Guardian ${guardianPid.pid} is holding the lease.`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (availability.status === "closed") {
|
|
321
|
+
throw new Error("Takeover is not available: room is closed.");
|
|
322
|
+
}
|
|
323
|
+
if (availability.status !== "takeover_available" && !operatorOverride) {
|
|
281
324
|
throw new Error(`Takeover is not available: ${formatWaitResult(availability)}`);
|
|
282
325
|
}
|
|
283
326
|
const result = runtime.commands.takeoverStick(identity, {
|
|
284
327
|
room_id: joined.room_id,
|
|
285
328
|
expected_turn_id: availability.turn_id,
|
|
286
|
-
reason
|
|
329
|
+
reason,
|
|
330
|
+
operator_override: operatorOverride
|
|
287
331
|
});
|
|
288
332
|
const guardianPid = await spawnGuardian({
|
|
289
333
|
agentId: identity.agent_id,
|
|
@@ -303,7 +347,7 @@ async function handleTakeoverCommand(runtime, parsed) {
|
|
|
303
347
|
guardian_process_started_at: guardianPid.process_started_at,
|
|
304
348
|
updated_at: new Date().toISOString()
|
|
305
349
|
});
|
|
306
|
-
printResult(parsed, { ...result, guardian_pid: guardianPid.pid }, () => `
|
|
350
|
+
printResult(parsed, { ...result, guardian_pid: guardianPid.pid }, () => `Took the stick. Guardian ${guardianPid.pid} is holding the lease.`);
|
|
307
351
|
}
|
|
308
352
|
async function handleReleaseCommand(runtime, parsed) {
|
|
309
353
|
const identity = deriveCliIdentity(parsed);
|
|
@@ -324,26 +368,37 @@ async function handleReleaseCommand(runtime, parsed) {
|
|
|
324
368
|
});
|
|
325
369
|
}
|
|
326
370
|
async function handlePassCommand(runtime, parsed) {
|
|
371
|
+
if (parsed.positionals[0]?.includes(":")) {
|
|
372
|
+
await handleAssignCommand(runtime, parsed);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
327
375
|
const identity = deriveCliIdentity(parsed);
|
|
328
|
-
const contextPath = parsed.positionals[
|
|
376
|
+
const contextPath = parsed.positionals[0] ?? process.cwd();
|
|
329
377
|
const session = requireLeaseSession(identity, contextPath);
|
|
330
378
|
const handoff = await resolveHandoff(parsed);
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
379
|
+
const result = runtime.commands.releaseStick(identity, {
|
|
380
|
+
room_id: session.room_id,
|
|
381
|
+
lease_id: session.lease_id,
|
|
382
|
+
expected_turn_id: session.turn_id,
|
|
383
|
+
handoff
|
|
384
|
+
});
|
|
385
|
+
clearCliSessionLease(resolveCliSessionPath(), identity.agent_id, session.room_id);
|
|
386
|
+
stopGuardian(session.guardian_pid, session.guardian_process_started_at ?? null);
|
|
387
|
+
printResult(parsed, result, () => {
|
|
388
|
+
const reserved = result.reserved_for ? ` Next: ${result.reserved_for}.` : "";
|
|
389
|
+
return `Passed turn.${reserved}`;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
async function handleAssignCommand(runtime, parsed) {
|
|
393
|
+
const targetSelector = parsed.positionals[0];
|
|
394
|
+
if (!targetSelector) {
|
|
395
|
+
throw new Error("Usage: tt assign <target|next> [path] (--status TEXT --next-action TEXT | --stdin)");
|
|
346
396
|
}
|
|
397
|
+
const identity = deriveCliIdentity(parsed);
|
|
398
|
+
const contextPath = parsed.positionals[1] ?? process.cwd();
|
|
399
|
+
const session = requireLeaseSession(identity, contextPath);
|
|
400
|
+
const handoff = await resolveHandoff(parsed);
|
|
401
|
+
const target = resolveAssignmentTarget(runtime, identity, session, targetSelector);
|
|
347
402
|
const result = runtime.commands.passStick(identity, {
|
|
348
403
|
room_id: session.room_id,
|
|
349
404
|
lease_id: session.lease_id,
|
|
@@ -585,6 +640,61 @@ function requireLeaseSession(identity, contextPath) {
|
|
|
585
640
|
}
|
|
586
641
|
return session;
|
|
587
642
|
}
|
|
643
|
+
function resolveAssignmentTarget(runtime, identity, session, selector) {
|
|
644
|
+
if (selector.includes(":")) {
|
|
645
|
+
return selector;
|
|
646
|
+
}
|
|
647
|
+
const state = runtime.commands.getRoomState({
|
|
648
|
+
room_id: session.room_id,
|
|
649
|
+
agent_id: identity.agent_id
|
|
650
|
+
});
|
|
651
|
+
const normalizedSelector = selector.toLowerCase();
|
|
652
|
+
const candidates = state.members.filter((member) => {
|
|
653
|
+
if (member.agent_id === identity.agent_id || member.status !== "active") {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
if (normalizedSelector === "next") {
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
return (member.agent_id.toLowerCase() === normalizedSelector ||
|
|
660
|
+
member.agent_id.toLowerCase().startsWith(`${normalizedSelector}:`) ||
|
|
661
|
+
member.display_name?.toLowerCase() === normalizedSelector);
|
|
662
|
+
});
|
|
663
|
+
if (candidates.length === 0) {
|
|
664
|
+
throw new Error(`No active room member matched assignment target: ${selector}`);
|
|
665
|
+
}
|
|
666
|
+
const events = runtime.commands.getRoomEvents({
|
|
667
|
+
room_id: session.room_id,
|
|
668
|
+
agent_id: identity.agent_id,
|
|
669
|
+
limit: 500
|
|
670
|
+
});
|
|
671
|
+
return pickFairAssignmentCandidate(candidates, events).agent_id;
|
|
672
|
+
}
|
|
673
|
+
function pickFairAssignmentCandidate(candidates, events) {
|
|
674
|
+
const lastOwnership = new Map();
|
|
675
|
+
for (const event of events) {
|
|
676
|
+
if ((event.event_type === "claim" || event.event_type === "takeover") &&
|
|
677
|
+
event.to_agent_id) {
|
|
678
|
+
lastOwnership.set(event.to_agent_id, event.created_at);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return candidates
|
|
682
|
+
.slice()
|
|
683
|
+
.sort((left, right) => {
|
|
684
|
+
const leftLastOwned = lastOwnership.get(left.agent_id);
|
|
685
|
+
const rightLastOwned = lastOwnership.get(right.agent_id);
|
|
686
|
+
if (!leftLastOwned && rightLastOwned) {
|
|
687
|
+
return -1;
|
|
688
|
+
}
|
|
689
|
+
if (leftLastOwned && !rightLastOwned) {
|
|
690
|
+
return 1;
|
|
691
|
+
}
|
|
692
|
+
if (leftLastOwned && rightLastOwned && leftLastOwned !== rightLastOwned) {
|
|
693
|
+
return Date.parse(leftLastOwned) - Date.parse(rightLastOwned);
|
|
694
|
+
}
|
|
695
|
+
return left.ordinal - right.ordinal;
|
|
696
|
+
})[0];
|
|
697
|
+
}
|
|
588
698
|
function upsertSessionFromJoin(identity, joined) {
|
|
589
699
|
upsertJoinedCliSession(resolveCliSessionPath(), {
|
|
590
700
|
agent_id: identity.agent_id,
|
|
@@ -629,6 +739,58 @@ function requireStringOption(parsed, key) {
|
|
|
629
739
|
}
|
|
630
740
|
return value;
|
|
631
741
|
}
|
|
742
|
+
function resolveTakeoverReason(parsed, env = process.env) {
|
|
743
|
+
const explicitReason = getStringOption(parsed, "reason");
|
|
744
|
+
if (explicitReason) {
|
|
745
|
+
return explicitReason;
|
|
746
|
+
}
|
|
747
|
+
if (hasOption(parsed, "operator-requested")) {
|
|
748
|
+
return "operator requested takeover";
|
|
749
|
+
}
|
|
750
|
+
if (isKnownHarnessCliEnv(env)) {
|
|
751
|
+
throw new Error("Missing required option --reason. Harness CLI takeovers must explain why, unless --operator-requested is set.");
|
|
752
|
+
}
|
|
753
|
+
return "operator takeover";
|
|
754
|
+
}
|
|
755
|
+
function shouldUseOperatorOverride(parsed, env = process.env) {
|
|
756
|
+
return (!isKnownHarnessCliEnv(env) ||
|
|
757
|
+
hasOption(parsed, "operator-requested") ||
|
|
758
|
+
hasOption(parsed, "force"));
|
|
759
|
+
}
|
|
760
|
+
function isKnownHarnessCliEnv(env = process.env) {
|
|
761
|
+
if (env.TT_HARNESS_AGENT_ID?.trim()) {
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
return deriveHarnessCliIdentity({ env }) !== null;
|
|
765
|
+
}
|
|
766
|
+
function maybeSyncInstalledSkills(parsed, env = process.env) {
|
|
767
|
+
if (!shouldAutoSyncInstalledSkills(parsed, env)) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
syncInstalledSkills({ skipMissing: true });
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
// Skill sync is a best-effort human CLI convenience. It must not make an
|
|
775
|
+
// unrelated tt command fail.
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
export function shouldAutoSyncInstalledSkills(parsed, env = process.env) {
|
|
779
|
+
if (env.TALKING_STICK_DISABLE_SKILL_SYNC?.trim()) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
if (isKnownHarnessCliEnv(env)) {
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
return !new Set([
|
|
786
|
+
"mcp",
|
|
787
|
+
"guard",
|
|
788
|
+
"install-skill",
|
|
789
|
+
"uninstall-skill",
|
|
790
|
+
"install",
|
|
791
|
+
"uninstall"
|
|
792
|
+
]).has(parsed.name);
|
|
793
|
+
}
|
|
632
794
|
function parseOptionalInteger(parsed, key) {
|
|
633
795
|
const value = getStringOption(parsed, key);
|
|
634
796
|
if (!value) {
|
|
@@ -748,7 +910,7 @@ async function spawnGuardian(input) {
|
|
|
748
910
|
let stderr = "";
|
|
749
911
|
const timeout = setTimeout(() => {
|
|
750
912
|
reject(new Error("Guardian did not signal readiness in time."));
|
|
751
|
-
},
|
|
913
|
+
}, GUARD_READY_TIMEOUT_MS);
|
|
752
914
|
child.stdout?.setEncoding("utf8");
|
|
753
915
|
child.stderr?.setEncoding("utf8");
|
|
754
916
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -1037,6 +1199,49 @@ function printActionPlan(action) {
|
|
|
1037
1199
|
}
|
|
1038
1200
|
process.stdout.write(`[${action.harness}] ${action.description}\n`);
|
|
1039
1201
|
}
|
|
1202
|
+
async function runSelfUpdateCommand(parsed) {
|
|
1203
|
+
normalizeBooleanFlag(parsed, "print");
|
|
1204
|
+
const dryRun = hasOption(parsed, "print");
|
|
1205
|
+
const managerOverride = getStringOption(parsed, "manager");
|
|
1206
|
+
let source;
|
|
1207
|
+
if (managerOverride) {
|
|
1208
|
+
if (!isPackageManager(managerOverride)) {
|
|
1209
|
+
throw new Error(`--manager must be one of npm | pnpm | yarn | bun (got ${managerOverride}).`);
|
|
1210
|
+
}
|
|
1211
|
+
source = managerOverride;
|
|
1212
|
+
}
|
|
1213
|
+
else {
|
|
1214
|
+
const binaryPath = resolveCurrentBinaryPath(import.meta.url);
|
|
1215
|
+
source = detectInstallSource({ binaryPath });
|
|
1216
|
+
}
|
|
1217
|
+
const plan = planSelfUpdate(source);
|
|
1218
|
+
if (!plan) {
|
|
1219
|
+
if (source === "dev") {
|
|
1220
|
+
throw new Error("tt is running from a development checkout. Use `git pull && npm install && npm run build` instead of `tt self-update`, or pass `--manager npm|pnpm|yarn|bun` if this is wrong.");
|
|
1221
|
+
}
|
|
1222
|
+
throw new Error(`Could not determine how tt was installed. Pass --manager npm|pnpm|yarn|bun to override.`);
|
|
1223
|
+
}
|
|
1224
|
+
if (dryRun) {
|
|
1225
|
+
process.stdout.write(`${plan.description}\n`);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
process.stdout.write(`Updating via: ${plan.description}\n`);
|
|
1229
|
+
await runInheritIo(plan.command, plan.args);
|
|
1230
|
+
process.stdout.write("Done. Restart your harness MCP subprocess to pick up the new dist.\n");
|
|
1231
|
+
}
|
|
1232
|
+
function runInheritIo(command, args) {
|
|
1233
|
+
return new Promise((resolve, reject) => {
|
|
1234
|
+
const child = spawn(command, args, { stdio: "inherit", shell: false });
|
|
1235
|
+
child.on("error", reject);
|
|
1236
|
+
child.on("close", (code) => {
|
|
1237
|
+
if (code === 0) {
|
|
1238
|
+
resolve();
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
reject(new Error(`${command} exited with code ${code}.`));
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1040
1245
|
function reportInstallResults(results, mode) {
|
|
1041
1246
|
let anyFailed = false;
|
|
1042
1247
|
for (const result of results) {
|
|
@@ -1063,8 +1268,10 @@ Commands:
|
|
|
1063
1268
|
tt state [path]
|
|
1064
1269
|
tt events [path] [--after N] [--limit N]
|
|
1065
1270
|
tt release [path] (--status TEXT --next-action TEXT | --stdin)
|
|
1066
|
-
tt pass [
|
|
1067
|
-
tt
|
|
1271
|
+
tt pass [path] (--status TEXT --next-action TEXT | --stdin)
|
|
1272
|
+
tt assign <target|next> [path] (--status TEXT --next-action TEXT | --stdin)
|
|
1273
|
+
tt take [path] [--reason TEXT] [--operator-requested]
|
|
1274
|
+
tt takeover [path] [--reason TEXT] [--operator-requested]
|
|
1068
1275
|
tt notes add <body> [--turn N] [--path DIR] [--stdin]
|
|
1069
1276
|
tt notes list [--all] [--after NOTE_ID] [--limit N] [--path DIR]
|
|
1070
1277
|
tt mcp
|
|
@@ -1072,6 +1279,7 @@ Commands:
|
|
|
1072
1279
|
tt uninstall <harness...> | --all [--print]
|
|
1073
1280
|
tt install-skill <harness...> | --all [--print] [--copy] [--link]
|
|
1074
1281
|
tt uninstall-skill <harness...> | --all [--print]
|
|
1282
|
+
tt self-update [--print] [--manager npm|pnpm|yarn|bun]
|
|
1075
1283
|
|
|
1076
1284
|
Harnesses: ${SUPPORTED_HARNESSES.join(", ")}
|
|
1077
1285
|
|
package/dist/commands.js
CHANGED
|
@@ -57,7 +57,8 @@ export class TalkingStickCommands {
|
|
|
57
57
|
agent_id: identity.agent_id,
|
|
58
58
|
room_id: input.room_id,
|
|
59
59
|
expected_turn_id: input.expected_turn_id,
|
|
60
|
-
reason: input.reason
|
|
60
|
+
reason: input.reason,
|
|
61
|
+
operator_override: input.operator_override
|
|
61
62
|
});
|
|
62
63
|
}
|
|
63
64
|
getRoomState(input) {
|
package/dist/config.js
CHANGED
|
@@ -6,7 +6,8 @@ export const defaultPolicy = {
|
|
|
6
6
|
claimTtlMs: 20 * 60 * 1000,
|
|
7
7
|
waitForTurnMaxWaitMs: 30 * 1000,
|
|
8
8
|
waitForTurnPollMs: 250,
|
|
9
|
-
presenceTtlMs: 4 * 60 * 60 * 1000
|
|
9
|
+
presenceTtlMs: 4 * 60 * 60 * 1000,
|
|
10
|
+
waiterGraceMs: 10 * 1000
|
|
10
11
|
};
|
|
11
12
|
export function resolveDataDir(options = {}) {
|
|
12
13
|
const env = options.env ?? process.env;
|
package/dist/db.js
CHANGED
|
@@ -89,6 +89,13 @@ const migrations = [
|
|
|
89
89
|
CREATE INDEX notes_by_room_created
|
|
90
90
|
ON notes (room_id, created_at, note_id);
|
|
91
91
|
`
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 4,
|
|
95
|
+
name: "room_member_wait_presence",
|
|
96
|
+
up: `
|
|
97
|
+
ALTER TABLE room_members ADD COLUMN last_wait_at TEXT;
|
|
98
|
+
`
|
|
92
99
|
}
|
|
93
100
|
];
|
|
94
101
|
export function resolveDatabasePath(options = {}) {
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { deriveHarnessCliIdentity, deriveHumanCliIdentity, deriveMcpHarnessIdent
|
|
|
6
6
|
export { ancestorPaths, canonicalizeContextPath, resolveContextPath, resolveWorkspaceRoot } from "./path-resolution.js";
|
|
7
7
|
export { createMcpServer, runStdioServer } from "./mcp-server.js";
|
|
8
8
|
export { SUPPORTED_HARNESSES, MissingHarnessError, detectHarness, parseHarnessList, planInstall, planUninstall, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
|
|
9
|
-
export { DEFAULT_SKILL_NAME, planSkillInstall, planSkillUninstall, resolveBundledSkillPath, resolveSkillTargetPath } from "./skill-install.js";
|
|
9
|
+
export { DEFAULT_SKILL_NAME, planSkillInstall, planSkillUninstall, resolveBundledSkillPath, resolveSkillTargetPath, syncInstalledSkills } from "./skill-install.js";
|
|
10
10
|
export { createSystemProcessInspector, terminateKnownProcess } from "./process-utils.js";
|
|
11
11
|
export { clearCliSessionLease, findCliSessionByRoom, findCliSessionForContextPath, readCliSessions, resolveCliSessionPath, upsertCliSession, upsertJoinedCliSession, writeCliSessions } from "./session-store.js";
|
|
12
12
|
export { TalkingStickService, createDefaultProcessLivenessChecker } from "./service.js";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export function detectInstallSource(input) {
|
|
5
|
+
const p = normalize(input.binaryPath);
|
|
6
|
+
// pnpm global layout: ~/.local/share/pnpm/... or pnpm-prefixed segments.
|
|
7
|
+
if (/(^|\/)\.local\/share\/pnpm\//.test(p))
|
|
8
|
+
return "pnpm";
|
|
9
|
+
if (/(^|\/)pnpm\/global\//.test(p))
|
|
10
|
+
return "pnpm";
|
|
11
|
+
// Yarn classic global layout: ~/.config/yarn/global/... or /yarn/global/.
|
|
12
|
+
if (/(^|\/)yarn\/global\//.test(p))
|
|
13
|
+
return "yarn";
|
|
14
|
+
// Bun global layout: ~/.bun/install/global/...
|
|
15
|
+
if (/(^|\/)\.bun\/install\//.test(p))
|
|
16
|
+
return "bun";
|
|
17
|
+
// npm-managed layouts (also covers Homebrew node, nvm, mise, asdf, volta — any
|
|
18
|
+
// tool that puts the package under a standard `node_modules/talking-stick`
|
|
19
|
+
// segment). Has to come AFTER the pnpm/yarn/bun checks because they also have
|
|
20
|
+
// node_modules segments.
|
|
21
|
+
if (/\/node_modules\/talking-stick\//.test(p))
|
|
22
|
+
return "npm";
|
|
23
|
+
// Anything else (a checked-out source tree, an unknown layout) is treated as
|
|
24
|
+
// a development install. Self-update there would be wrong; we tell the user
|
|
25
|
+
// to git pull instead.
|
|
26
|
+
return "dev";
|
|
27
|
+
}
|
|
28
|
+
export function planSelfUpdate(source) {
|
|
29
|
+
switch (source) {
|
|
30
|
+
case "npm":
|
|
31
|
+
return {
|
|
32
|
+
command: "npm",
|
|
33
|
+
args: ["install", "-g", "talking-stick@latest"],
|
|
34
|
+
description: "npm install -g talking-stick@latest"
|
|
35
|
+
};
|
|
36
|
+
case "pnpm":
|
|
37
|
+
return {
|
|
38
|
+
command: "pnpm",
|
|
39
|
+
args: ["install", "-g", "talking-stick@latest"],
|
|
40
|
+
description: "pnpm install -g talking-stick@latest"
|
|
41
|
+
};
|
|
42
|
+
case "yarn":
|
|
43
|
+
return {
|
|
44
|
+
command: "yarn",
|
|
45
|
+
args: ["global", "add", "talking-stick@latest"],
|
|
46
|
+
description: "yarn global add talking-stick@latest"
|
|
47
|
+
};
|
|
48
|
+
case "bun":
|
|
49
|
+
return {
|
|
50
|
+
command: "bun",
|
|
51
|
+
args: ["add", "-g", "talking-stick@latest"],
|
|
52
|
+
description: "bun add -g talking-stick@latest"
|
|
53
|
+
};
|
|
54
|
+
case "dev":
|
|
55
|
+
case "unknown":
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function resolveCurrentBinaryPath(metaUrl) {
|
|
60
|
+
const target = fileURLToPath(metaUrl);
|
|
61
|
+
try {
|
|
62
|
+
return fs.realpathSync(target);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return path.resolve(target);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function isPackageManager(value) {
|
|
69
|
+
return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun";
|
|
70
|
+
}
|
|
71
|
+
function normalize(value) {
|
|
72
|
+
// Treat backslashes as forward slashes for cross-platform regex matching.
|
|
73
|
+
return value.replace(/\\/g, "/");
|
|
74
|
+
}
|