talking-stick 0.4.9 → 0.4.11
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 +8 -6
- package/dist/atomic-write.js +21 -0
- package/dist/cli/grok-session-hook.js +69 -0
- package/dist/cli/guardian.js +45 -9
- package/dist/cli/install-commands.js +34 -19
- package/dist/cli/instructions-commands.js +0 -17
- package/dist/cli/msg-commands.js +0 -9
- package/dist/cli/output.js +1 -1
- package/dist/cli/parser.js +30 -7
- package/dist/cli/registry.js +10 -0
- package/dist/cli/room-commands.js +1 -3
- package/dist/cli/turn-commands.js +1 -3
- package/dist/cli.js +18 -6
- package/dist/grok-session-store.js +143 -0
- package/dist/identity.js +77 -8
- package/dist/index.js +2 -1
- package/dist/install.js +127 -15
- package/dist/instructions.js +19 -4
- package/dist/process-utils.js +5 -1
- package/dist/service.js +20 -11
- package/dist/session-store.js +2 -2
- package/dist/skill-install.js +35 -4
- package/docs/releases/0.4.10.md +17 -0
- package/docs/releases/0.4.11.md +28 -0
- package/package.json +1 -1
- package/skills/talking-stick/SKILL.md +2 -2
package/dist/service.js
CHANGED
|
@@ -1416,10 +1416,13 @@ export class TalkingStickService {
|
|
|
1416
1416
|
return null;
|
|
1417
1417
|
}
|
|
1418
1418
|
const lastOwnership = this.getLastOwnershipByAgent(roomId);
|
|
1419
|
-
const
|
|
1419
|
+
const ordinalRanks = ordinalRankByAgent(members);
|
|
1420
|
+
const referenceRank = afterAgentId
|
|
1421
|
+
? ordinalRanks.get(afterAgentId) ?? -1
|
|
1422
|
+
: -1;
|
|
1420
1423
|
return candidates
|
|
1421
1424
|
.slice()
|
|
1422
|
-
.sort((left, right) => compareFairCandidates(left, right, lastOwnership,
|
|
1425
|
+
.sort((left, right) => compareFairCandidates(left, right, lastOwnership, ordinalRanks, referenceRank, members.length))[0];
|
|
1423
1426
|
}
|
|
1424
1427
|
getLastOwnershipByAgent(roomId) {
|
|
1425
1428
|
const rows = this.db
|
|
@@ -1691,7 +1694,7 @@ export class TalkingStickService {
|
|
|
1691
1694
|
: "gone";
|
|
1692
1695
|
state =
|
|
1693
1696
|
this.hasExpired(room.claim_expires_at, now) &&
|
|
1694
|
-
reservedLiveness
|
|
1697
|
+
this.isGonePersistent(reservedMember, reservedLiveness, now)
|
|
1695
1698
|
? "recipient_gone"
|
|
1696
1699
|
: "reserved";
|
|
1697
1700
|
}
|
|
@@ -1741,7 +1744,7 @@ export class TalkingStickService {
|
|
|
1741
1744
|
: "gone";
|
|
1742
1745
|
state =
|
|
1743
1746
|
this.hasExpired(room.claim_expires_at, now) &&
|
|
1744
|
-
reservedLiveness
|
|
1747
|
+
this.isGonePersistent(reservedMember, reservedLiveness, now)
|
|
1745
1748
|
? "recipient_gone"
|
|
1746
1749
|
: "reserved";
|
|
1747
1750
|
}
|
|
@@ -1899,7 +1902,7 @@ function mapNoteRow(row) {
|
|
|
1899
1902
|
resolved_by_agent_id: row.resolved_by_agent_id
|
|
1900
1903
|
};
|
|
1901
1904
|
}
|
|
1902
|
-
function compareFairCandidates(left, right, lastOwnership,
|
|
1905
|
+
function compareFairCandidates(left, right, lastOwnership, ordinalRanks, referenceRank, memberCount) {
|
|
1903
1906
|
const leftLastOwned = lastOwnership.get(left.agent_id);
|
|
1904
1907
|
const rightLastOwned = lastOwnership.get(right.agent_id);
|
|
1905
1908
|
if (!leftLastOwned && rightLastOwned) {
|
|
@@ -1911,18 +1914,24 @@ function compareFairCandidates(left, right, lastOwnership, referenceOrdinal, mem
|
|
|
1911
1914
|
if (leftLastOwned && rightLastOwned && leftLastOwned !== rightLastOwned) {
|
|
1912
1915
|
return Date.parse(leftLastOwned) - Date.parse(rightLastOwned);
|
|
1913
1916
|
}
|
|
1914
|
-
const leftDistance = sequenceDistance(left.ordinal,
|
|
1915
|
-
const rightDistance = sequenceDistance(right.ordinal,
|
|
1917
|
+
const leftDistance = sequenceDistance(ordinalRanks.get(left.agent_id) ?? left.ordinal, referenceRank, memberCount);
|
|
1918
|
+
const rightDistance = sequenceDistance(ordinalRanks.get(right.agent_id) ?? right.ordinal, referenceRank, memberCount);
|
|
1916
1919
|
if (leftDistance !== rightDistance) {
|
|
1917
1920
|
return leftDistance - rightDistance;
|
|
1918
1921
|
}
|
|
1919
1922
|
return left.ordinal - right.ordinal;
|
|
1920
1923
|
}
|
|
1921
|
-
function
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
+
function ordinalRankByAgent(members) {
|
|
1925
|
+
return new Map(members
|
|
1926
|
+
.slice()
|
|
1927
|
+
.sort((left, right) => left.ordinal - right.ordinal)
|
|
1928
|
+
.map((member, index) => [member.agent_id, index]));
|
|
1929
|
+
}
|
|
1930
|
+
function sequenceDistance(ordinalRank, referenceRank, memberCount) {
|
|
1931
|
+
if (memberCount <= 0 || referenceRank < 0) {
|
|
1932
|
+
return ordinalRank;
|
|
1924
1933
|
}
|
|
1925
|
-
const distance = (
|
|
1934
|
+
const distance = (ordinalRank - referenceRank + memberCount) % memberCount;
|
|
1926
1935
|
return distance === 0 ? memberCount : distance;
|
|
1927
1936
|
}
|
|
1928
1937
|
function parseTimestampMs(timestamp) {
|
package/dist/session-store.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { writeFileAtomic } from "./atomic-write.js";
|
|
3
4
|
import { resolveDataDir } from "./config.js";
|
|
4
5
|
import { ancestorPaths, resolveContextPath } from "./path-resolution.js";
|
|
5
6
|
export function resolveCliSessionPath(options = {}) {
|
|
@@ -22,8 +23,7 @@ export function readCliSessions(sessionPath) {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
export function writeCliSessions(sessionPath, sessions) {
|
|
25
|
-
|
|
26
|
-
fs.writeFileSync(sessionPath, `${JSON.stringify(sessions, null, 2)}\n`);
|
|
26
|
+
writeFileAtomic(sessionPath, `${JSON.stringify(sessions, null, 2)}\n`);
|
|
27
27
|
}
|
|
28
28
|
export function upsertCliSession(sessionPath, session) {
|
|
29
29
|
const sessions = readCliSessions(sessionPath);
|
package/dist/skill-install.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { MissingHarnessError, resolveHarnessConfigDir, skipAction } from "./install.js";
|
|
6
6
|
export const DEFAULT_SKILL_NAME = "talking-stick";
|
|
7
|
-
const FILE_SKILL_HARNESSES = ["claude-code", "codex", "opencode"];
|
|
7
|
+
const FILE_SKILL_HARNESSES = ["claude-code", "codex", "grok", "opencode"];
|
|
8
8
|
export function resolveBundledSkillPath(options = {}) {
|
|
9
9
|
return options.sourcePath ?? path.resolve(currentPackageDir(), "skills", DEFAULT_SKILL_NAME);
|
|
10
10
|
}
|
|
@@ -15,8 +15,10 @@ export function resolveSkillTargetPath(harness, options = {}) {
|
|
|
15
15
|
return path.join(homeDir, ".claude", "skills", options.skillName ?? DEFAULT_SKILL_NAME);
|
|
16
16
|
case "codex":
|
|
17
17
|
return path.join(homeDir, ".codex", "skills", options.skillName ?? DEFAULT_SKILL_NAME);
|
|
18
|
+
case "grok":
|
|
19
|
+
return path.join(resolveHarnessConfigDir("grok", options), "skills", options.skillName ?? DEFAULT_SKILL_NAME);
|
|
18
20
|
case "opencode":
|
|
19
|
-
return path.join(
|
|
21
|
+
return path.join(resolveHarnessConfigDir("opencode", options), "skills", options.skillName ?? DEFAULT_SKILL_NAME);
|
|
20
22
|
default:
|
|
21
23
|
throw new Error(`Unknown skill-install harness: ${harness}`);
|
|
22
24
|
}
|
|
@@ -33,14 +35,16 @@ export function planSkillInstall(harness, options = {}) {
|
|
|
33
35
|
harness,
|
|
34
36
|
command: "gemini",
|
|
35
37
|
args: ["skills", "link", sourcePath, "--scope", "user", "--consent"],
|
|
36
|
-
description: `gemini skills link ${sourcePath} --scope user --consent
|
|
38
|
+
description: `gemini skills link ${sourcePath} --scope user --consent`,
|
|
39
|
+
operation: "install"
|
|
37
40
|
}
|
|
38
41
|
: {
|
|
39
42
|
kind: "exec",
|
|
40
43
|
harness,
|
|
41
44
|
command: "gemini",
|
|
42
45
|
args: ["skills", "install", sourcePath, "--scope", "user", "--consent"],
|
|
43
|
-
description: `gemini skills install ${sourcePath} --scope user --consent
|
|
46
|
+
description: `gemini skills install ${sourcePath} --scope user --consent`,
|
|
47
|
+
operation: "install"
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
50
|
const targetPath = resolveSkillTargetPath(harness, options);
|
|
@@ -56,6 +60,8 @@ export function planSkillInstall(harness, options = {}) {
|
|
|
56
60
|
description: shouldLink
|
|
57
61
|
? `link ${sourcePath} -> ${targetPath}`
|
|
58
62
|
: `copy ${sourcePath} -> ${targetPath}`,
|
|
63
|
+
operation: "install",
|
|
64
|
+
inspect: () => inspectInstalledSkill(sourcePath, targetPath, shouldLink),
|
|
59
65
|
apply: () => installSkillDirectory(sourcePath, targetPath, harnessRootPath, shouldLink, options)
|
|
60
66
|
};
|
|
61
67
|
}
|
|
@@ -175,6 +181,31 @@ function syncInstalledFileSkill(harness, sourcePath, sourceDigest, options) {
|
|
|
175
181
|
};
|
|
176
182
|
}
|
|
177
183
|
}
|
|
184
|
+
function inspectInstalledSkill(sourcePath, targetPath, link) {
|
|
185
|
+
try {
|
|
186
|
+
const stat = fs.lstatSync(targetPath);
|
|
187
|
+
if (link) {
|
|
188
|
+
if (!stat.isSymbolicLink()) {
|
|
189
|
+
return "different";
|
|
190
|
+
}
|
|
191
|
+
const currentTarget = fs.readlinkSync(targetPath);
|
|
192
|
+
const resolvedCurrentTarget = path.resolve(path.dirname(targetPath), currentTarget);
|
|
193
|
+
return sameRealPath(resolvedCurrentTarget, sourcePath)
|
|
194
|
+
? "present"
|
|
195
|
+
: "different";
|
|
196
|
+
}
|
|
197
|
+
if (stat.isDirectory() && digestDirectory(targetPath) === digestDirectory(sourcePath)) {
|
|
198
|
+
return "present";
|
|
199
|
+
}
|
|
200
|
+
return "different";
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error.code === "ENOENT") {
|
|
204
|
+
return "absent";
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
178
209
|
function removeInstalledSkill(targetPath, harnessRootPath, options = {}) {
|
|
179
210
|
const pathExists = options.pathExists ?? fs.existsSync;
|
|
180
211
|
if (options.skipMissing && harnessRootPath && !pathExists(harnessRootPath)) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Talking Stick 0.4.10
|
|
2
|
+
|
|
3
|
+
Date: 2026-06-08
|
|
4
|
+
|
|
5
|
+
## Added
|
|
6
|
+
- **Grok Build harness support.** `tt install grok` now installs the native `~/.grok/skills/talking-stick` skill and a trusted global `~/.grok/hooks/talking-stick-session.json` hook. Grok-launched `tt` calls work without cmux by detecting a `grok` root process in ancestry; `CMUX_AGENT_LAUNCH_KIND=grok` remains optional fast evidence when present. The hook records `GROK_SESSION_ID` context in `${TALKING_STICK_DATA_DIR}/grok-sessions.jsonl` so identity can upgrade from pid-root identity to the real Grok session id when the record matches the workspace and harness process.
|
|
7
|
+
|
|
8
|
+
## Verification
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm run typecheck
|
|
12
|
+
npm test
|
|
13
|
+
npm run build
|
|
14
|
+
node dist/cli.js --help
|
|
15
|
+
git diff --check
|
|
16
|
+
npm pack --dry-run
|
|
17
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Talking Stick 0.4.11
|
|
2
|
+
|
|
3
|
+
Date: 2026-06-09
|
|
4
|
+
|
|
5
|
+
## Fixed
|
|
6
|
+
- **Guardian no longer leaks when readiness times out.** `spawnGuardian`'s readiness timeout now kills the detached child and clears its listeners before rejecting. Previously the orphaned guardian survived, wrote `READY` to a stream nobody read, and held the lease indefinitely with no recorded PID for `stopGuardian` to reach. (#31)
|
|
7
|
+
- **Fair-turn ordering survives member churn.** Round-robin distance is now computed from each member's rank within the current member list instead of raw join ordinals modulo member count, which inverted ordering once departures left sparse ordinals (e.g. `[0, 5, 7]`). (#32)
|
|
8
|
+
- **Reserved-member liveness gets the same gone grace as owners.** Both room-inspection paths now run the reserved branch through `isGonePersistent`, so a transient process-check misread right after claim expiry can no longer deny the rightful recipient its grant. (#33)
|
|
9
|
+
- **`cli-sessions.json` and harness config patches are written atomically.** Both now write to a temp sibling and `rename`, so a crash or full disk mid-write can no longer truncate the session store or a user-owned config such as `opencode.json`. (#34)
|
|
10
|
+
- **`ps` lstart parsing is locale-stable.** Process inspection invokes `ps` with `LC_ALL=C`, so non-C locales no longer silently degrade identity resolution and liveness to the weakest fallback. (#35)
|
|
11
|
+
- **Errors are machine-readable in JSON mode.** When `--json` is requested, plain CLI errors serialize as `{"error": "cli_error", "message": ...}` on stderr (matching `ProtocolError`'s shape) instead of bare text, keeping the non-zero exit code. (#36)
|
|
12
|
+
- **Boolean flags never consume positionals.** The CLI parser has a boolean-flag registry, retiring the `--json`-eats-positional footgun and the per-command repair shims; `--after`-style integer options now reject trailing garbage like `100ms`. (#37)
|
|
13
|
+
- **Non-harness `##` headings no longer bleed into harness sections.** In instruction files, an unrecognized `##` heading after harness sections ends the current section; its content is excluded from harness extraction. (#38)
|
|
14
|
+
- **OpenCode skill installs follow the XDG-aware config dir.** The skill now lands next to `opencode.json` (normally `~/.config/opencode/skills/talking-stick`, honoring `XDG_CONFIG_HOME`) instead of the hardcoded `~/.opencode` tree; verified against OpenCode source, which scans both `skill/` and `skills/` under the config dir. (#39)
|
|
15
|
+
- **Restarted Grok sessions mint fresh identity.** When PID identity is available but matches no recorded session, the workspace-candidate fallback no longer hands the new process the previous session's identity while the old `session_end` hook is pending. (#40)
|
|
16
|
+
- **`tt install` is idempotent for skills.** Skill install actions carry `operation`/`inspect`, so a second run reports `already_present` instead of deleting and re-copying the skill directory every time. (#41)
|
|
17
|
+
- **Docs drift from the MCP-to-skill migration cleaned up.** AGENTS.md/README no longer reference the removed `src/mcp-server.ts` entry point or stale install paths; `patchOpencodeConfig`'s dead install branch is gone and the legacy `tt mcp` command constant is marked as match-only. (#42)
|
|
18
|
+
|
|
19
|
+
## Verification
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm run typecheck
|
|
23
|
+
npm test
|
|
24
|
+
npm run build
|
|
25
|
+
node dist/cli.js --help
|
|
26
|
+
git diff --check
|
|
27
|
+
npm pack --dry-run
|
|
28
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: talking-stick
|
|
3
|
-
description: Use when working in a repo that coordinates multiple agent harnesses with Talking Stick (`tt` / `talking-stick`), or when the user asks you to avoid parallel work, wait your turn, pass structured handoffs, or coordinate with Claude, Codex, Gemini, or OpenCode in the same workspace. Also use when a workspace contains a `.talking-stick/` marker.
|
|
3
|
+
description: Use when working in a repo that coordinates multiple agent harnesses with Talking Stick (`tt` / `talking-stick`), or when the user asks you to avoid parallel work, wait your turn, pass structured handoffs, or coordinate with Claude, Codex, Gemini, Grok, or OpenCode in the same workspace. Also use when a workspace contains a `.talking-stick/` marker.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
This skill teaches a harness how to behave in a Talking Stick workspace.
|
|
@@ -49,7 +49,7 @@ Some workspaces may also have sibling receive processes running `tt events --fol
|
|
|
49
49
|
|
|
50
50
|
If coordination is required and `tt` is unavailable, say so briefly and ask the user whether they want to install or enable Talking Stick first. Do not pretend coordination is active.
|
|
51
51
|
|
|
52
|
-
Human CLI runs silently keep already-installed Claude Code, Codex, and OpenCode skill copies/symlinks aligned with the bundled Talking Stick skill. This is best effort and only updates existing installs; Gemini skills are registry-managed and should be refreshed with `tt install gemini` when needed.
|
|
52
|
+
Human CLI runs silently keep already-installed Claude Code, Codex, Grok, and OpenCode skill copies/symlinks aligned with the bundled Talking Stick skill. This is best effort and only updates existing installs; Gemini skills are registry-managed and should be refreshed with `tt install gemini` when needed.
|
|
53
53
|
|
|
54
54
|
### 2. Join The Workspace Room Once
|
|
55
55
|
|