talking-stick 0.4.0 → 0.4.2
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 +4 -4
- package/dist/cli/turn-commands.js +8 -3
- package/dist/instructions.js +1 -1
- package/dist/service.js +11 -7
- package/docs/receive-consumer-contract.md +1 -1
- package/docs/releases/0.4.1.md +38 -0
- package/docs/releases/0.4.2.md +41 -0
- package/package.json +1 -1
- package/skills/talking-stick/SKILL.md +3 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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.4.
|
|
5
|
+
**Version:** 0.4.1. 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
|
|
|
@@ -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.4.
|
|
49
|
+
| **From npm** | `npm i -g talking-stick` | Published as `0.4.1`. 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
|
|
|
@@ -106,7 +106,7 @@ tt instructions — editable collaboration prompt loaded by the skill
|
|
|
106
106
|
|
|
107
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.
|
|
108
108
|
|
|
109
|
-
The global skill tells the model when to join, wait,
|
|
109
|
+
The global skill tells the model when to join, wait, take over, leave notes, send messages, and hand off.
|
|
110
110
|
|
|
111
111
|
## Editable collaboration instructions
|
|
112
112
|
|
|
@@ -146,7 +146,7 @@ tt events --wait|--follow [--event TYPE[,TYPE]] [--target self|any|agent]
|
|
|
146
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
147
|
- `tt events --wait` and `tt events --follow` default to `--target self`; pass `--target any` only for audit/debug views.
|
|
148
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
|
|
149
|
+
- Event receive does not grant the stick. Agents must still use `tt wait` for ownership before editing shared files.
|
|
150
150
|
|
|
151
151
|
**When to message vs note vs handoff.**
|
|
152
152
|
|
|
@@ -18,12 +18,12 @@ export async function handleWaitCommand(runtime, parsed, isTry, cliEntryUrl) {
|
|
|
18
18
|
if (waitResult.reason === "already_owner") {
|
|
19
19
|
const sessionPath = resolveCliSessionPath();
|
|
20
20
|
const existing = findCliSessionByRoom(sessionPath, identity.agent_id, joined.room_id);
|
|
21
|
-
const liveness = existing
|
|
21
|
+
const liveness = existing?.guardian_pid
|
|
22
22
|
? checkGuardianLiveness({
|
|
23
23
|
pid: existing.guardian_pid,
|
|
24
24
|
process_started_at: existing.guardian_process_started_at
|
|
25
25
|
}, createSystemProcessInspector())
|
|
26
|
-
: "
|
|
26
|
+
: "gone";
|
|
27
27
|
if (liveness === "gone") {
|
|
28
28
|
const replacement = await spawnGuardian({
|
|
29
29
|
agentId: identity.agent_id,
|
|
@@ -44,7 +44,12 @@ export async function handleWaitCommand(runtime, parsed, isTry, cliEntryUrl) {
|
|
|
44
44
|
guardian_process_started_at: replacement.process_started_at,
|
|
45
45
|
updated_at: new Date().toISOString()
|
|
46
46
|
});
|
|
47
|
-
printResult(parsed, { ...waitResult, guardian_pid: replacement.pid }, () =>
|
|
47
|
+
printResult(parsed, { ...waitResult, guardian_pid: replacement.pid }, () => {
|
|
48
|
+
const reason = existing?.guardian_pid
|
|
49
|
+
? "Prior guardian was gone"
|
|
50
|
+
: "No guardian was recorded";
|
|
51
|
+
return `Already holding the stick (turn ${waitResult.turn_id}). ${reason}; spawned replacement ${replacement.pid}.`;
|
|
52
|
+
});
|
|
48
53
|
return;
|
|
49
54
|
}
|
|
50
55
|
const guardianPid = existing?.guardian_pid;
|
package/dist/instructions.js
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveContextPath } from "./path-resolution.js";
|
|
|
6
6
|
export const DEFAULT_MAX_INSTRUCTION_FILE_BYTES = 256 * 1024;
|
|
7
7
|
export const DEFAULT_INSTRUCTIONS_MARKDOWN = `# Talking Stick collaboration instructions
|
|
8
8
|
|
|
9
|
-
Keep using Talking Stick until the shared task is done. After releasing or handing off, re-enter the wait loop by default. Prefer continued action unless the task is complete or the operator explicitly redirects or stops the room.
|
|
9
|
+
Keep using Talking Stick until the shared task is done. After releasing or handing off, re-enter the wait loop by default. Prefer continued action unless the task is complete or the operator explicitly redirects or stops the room. If you are the only active member of the room, stop polling after a clear handoff rather than churning release/reclaim turns.
|
|
10
10
|
|
|
11
11
|
Use phase names in handoffs when they clarify the work: draft, adversarial review, convergence, implementation, implementation review, test review, and release. These phases are vocabulary, not protocol state.
|
|
12
12
|
|
package/dist/service.js
CHANGED
|
@@ -1247,20 +1247,24 @@ export class TalkingStickService {
|
|
|
1247
1247
|
if (!room.pending_handoff_event_seq) {
|
|
1248
1248
|
return false;
|
|
1249
1249
|
}
|
|
1250
|
-
|
|
1251
|
-
return false;
|
|
1252
|
-
}
|
|
1250
|
+
const handoffAgeMs = now.getTime() - Date.parse(room.updated_at);
|
|
1253
1251
|
const pendingEvent = this.getEventBySeq(room.pending_handoff_event_seq);
|
|
1254
1252
|
const priorOwner = pendingEvent?.from_agent_id ?? null;
|
|
1255
1253
|
if (priorOwner === agentId &&
|
|
1256
|
-
this.
|
|
1257
|
-
return
|
|
1254
|
+
this.hasOtherActiveRoomMember(room.room_id, agentId, now)) {
|
|
1255
|
+
return handoffAgeMs < this.priorOwnerReleaseCooldownMs();
|
|
1256
|
+
}
|
|
1257
|
+
if (handoffAgeMs >= this.policy.waiterGraceMs) {
|
|
1258
|
+
return false;
|
|
1258
1259
|
}
|
|
1259
1260
|
const bestKnownMember = this.findBestFairKnownMember(room.room_id, priorOwner, now);
|
|
1260
1261
|
return bestKnownMember !== null && bestKnownMember.agent_id !== agentId;
|
|
1261
1262
|
}
|
|
1262
|
-
|
|
1263
|
-
return this.getMembers(roomId).some((member) => member.agent_id !== agentId);
|
|
1263
|
+
hasOtherActiveRoomMember(roomId, agentId, now) {
|
|
1264
|
+
return this.getMembers(roomId).some((member) => member.agent_id !== agentId && this.isMemberActive(member, now));
|
|
1265
|
+
}
|
|
1266
|
+
priorOwnerReleaseCooldownMs() {
|
|
1267
|
+
return Math.max(this.policy.waiterGraceMs * 6, 60_000);
|
|
1264
1268
|
}
|
|
1265
1269
|
inspectRoom(room, now) {
|
|
1266
1270
|
const members = this.getMembers(room.room_id);
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
## Consumer Responsibilities
|
|
27
27
|
|
|
28
28
|
- Keep `wait_for_turn` / `tt wait` running separately. Receive processes do not claim or grant the stick, even when they return pass, release, or assignment events.
|
|
29
|
-
- Treat an event wake as a prompt to read, reply, or retry `tt wait`. It is not permission to mutate shared files; only a `your_turn` wait result
|
|
29
|
+
- Treat an event wake as a prompt to read, reply, or retry `tt wait`. It is not permission to mutate shared files; only a `your_turn` wait result grants ownership.
|
|
30
30
|
- Decide how to surface `delivery_hint=interrupt`; the server only records the hint.
|
|
31
31
|
- Dedupe on `event_id` if restart replay is possible.
|
|
32
32
|
- Treat message bodies as room-visible text, not private data.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Talking Stick 0.4.1
|
|
2
|
+
|
|
3
|
+
Date: 2026-05-10
|
|
4
|
+
|
|
5
|
+
This patch fixes release/reclaim churn discovered while dogfooding the 0.4.0
|
|
6
|
+
collaboration instructions. A holder could release with "no work", immediately
|
|
7
|
+
re-enter `tt wait`, and reclaim the stick before another active harness had a
|
|
8
|
+
real chance to claim.
|
|
9
|
+
|
|
10
|
+
## Fixed
|
|
11
|
+
|
|
12
|
+
### Prior-owner release cooldown
|
|
13
|
+
|
|
14
|
+
When a room is idle after a release, the release's prior owner now waits through
|
|
15
|
+
a bounded cooldown before reclaiming if another active member exists. The
|
|
16
|
+
cooldown defaults to `max(6 * waiterGraceMs, 60_000)`, which is 60 seconds with
|
|
17
|
+
the stock policy.
|
|
18
|
+
|
|
19
|
+
Other members can still claim immediately, and the prior owner can continue
|
|
20
|
+
immediately when he is genuinely alone. Stale members and audit-only
|
|
21
|
+
`target=any` event reads do not force a cooldown.
|
|
22
|
+
|
|
23
|
+
### Solo-polling guidance
|
|
24
|
+
|
|
25
|
+
The bundled skill and default collaboration instructions now clarify the escape
|
|
26
|
+
hatch for one-agent rooms: if you are the only active member, stop polling after
|
|
27
|
+
a clear handoff instead of churning release/reclaim turns. Brief quiet periods
|
|
28
|
+
from another active agent are not enough to declare yourself alone.
|
|
29
|
+
|
|
30
|
+
## Verification
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run typecheck
|
|
34
|
+
npm run build
|
|
35
|
+
npm test
|
|
36
|
+
git diff --check
|
|
37
|
+
npm pack --dry-run
|
|
38
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Talking Stick 0.4.2
|
|
2
|
+
|
|
3
|
+
Date: 2026-05-10
|
|
4
|
+
|
|
5
|
+
This patch moves guardian liveness back into the CLI contract. Agents should not
|
|
6
|
+
manually inspect guardian PIDs after `tt wait`; a `your_turn` result means the
|
|
7
|
+
CLI has confirmed or spawned the guardian it needs, and guardian setup failures
|
|
8
|
+
surface as command failures.
|
|
9
|
+
|
|
10
|
+
## Fixed
|
|
11
|
+
|
|
12
|
+
### Guardian ownership contract
|
|
13
|
+
|
|
14
|
+
`tt wait` now repairs an already-owned turn when the local CLI session has no
|
|
15
|
+
recorded guardian PID. This can happen after local session-file loss or partial
|
|
16
|
+
state cleanup: the room still knows the caller owns the turn, but the foreground
|
|
17
|
+
CLI no longer has a guardian process recorded. Instead of returning
|
|
18
|
+
`your_turn` with `guardian_pid: null`, the CLI now starts a replacement guardian,
|
|
19
|
+
records it, and returns that PID.
|
|
20
|
+
|
|
21
|
+
Known-uncertain liveness with an existing PID is still left alone. That avoids
|
|
22
|
+
respawning guardians repeatedly on platforms or process-start formats where
|
|
23
|
+
exact liveness cannot be proven.
|
|
24
|
+
|
|
25
|
+
### Harness guidance
|
|
26
|
+
|
|
27
|
+
The bundled skill, README, and receive-consumer contract now describe
|
|
28
|
+
`tt wait`/`tt take` as the ownership gate. Event streams remain observer-only:
|
|
29
|
+
an event wake can tell an agent to read, reply, or retry `tt wait`, but it does
|
|
30
|
+
not grant workspace write authority.
|
|
31
|
+
|
|
32
|
+
## Verification
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run typecheck
|
|
36
|
+
npx vitest run tests/cli.test.ts -t guardian
|
|
37
|
+
npm test
|
|
38
|
+
npm run build
|
|
39
|
+
node dist/cli.js --help
|
|
40
|
+
git diff --check
|
|
41
|
+
```
|
package/package.json
CHANGED
|
@@ -95,7 +95,7 @@ Possible outcomes:
|
|
|
95
95
|
- `takeover_available`: surface the reason and make takeover explicit
|
|
96
96
|
- `closed`: stop and explain that the room is closed
|
|
97
97
|
|
|
98
|
-
A successful `tt wait` or `tt take` starts an internal `tt guard` lease guardian and returns `guardian_pid` in JSON.
|
|
98
|
+
A successful `tt wait` or `tt take` starts an internal `tt guard` lease guardian and returns `guardian_pid` in JSON. Trust `tt wait`: a `your_turn` result means the CLI confirmed or spawned a guardian, and if it could not, the command would have failed. Do not kill that guardian.
|
|
99
99
|
|
|
100
100
|
### 4. While Waiting
|
|
101
101
|
|
|
@@ -103,7 +103,7 @@ Prefer to run `tt wait` in the background if your harness supports background co
|
|
|
103
103
|
|
|
104
104
|
Prefer wait cycles over scheduled wakeups. A direct long-poll stays aligned with other agents and usually notices a released stick within the same cycle. Use scheduled wakeups only when your harness cannot keep a wait running in the background.
|
|
105
105
|
|
|
106
|
-
Do not replace `tt wait` with an event receiver. `tt events --wait` is only a wake channel for messages and handoff/reservation events. If it exits with a pass, release, assignment, or message, process the event, then run or continue `tt wait --json`; do not touch shared files unless that wait returns `your_turn
|
|
106
|
+
Do not replace `tt wait` with an event receiver. `tt events --wait` is only a wake channel for messages and handoff/reservation events. If it exits with a pass, release, assignment, or message, process the event, then run or continue `tt wait --json`; do not touch shared files unless that wait returns `your_turn`.
|
|
107
107
|
|
|
108
108
|
If you do not have the stick:
|
|
109
109
|
|
|
@@ -224,7 +224,7 @@ Exit the wait loop only when one of these is true:
|
|
|
224
224
|
- the shared task is explicitly finished
|
|
225
225
|
- the operator gives a direct redirect or stop
|
|
226
226
|
|
|
227
|
-
In every other case, after `tt release` or `tt assign`, go straight back into `tt wait --json`.
|
|
227
|
+
In every other case, after `tt release` or `tt assign`, go straight back into `tt wait --json`. If you are the only active member of the room, stop polling after a clear handoff. Treat "only active" as no other member that `tt state --json` reports active or that has been seen in the last hour; if liveness is ambiguous, run one more normal wait cycle instead of churning. Other agents going briefly quiet is not enough to declare yourself alone.
|
|
228
228
|
|
|
229
229
|
If the operator tells you to drop out of coordination, run `tt leave --json`. Rooms with no active members are deleted instead of kept as history, and long-idle rooms may be purged on later invocations.
|
|
230
230
|
|