talking-stick 0.4.9 → 0.4.10
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 +7 -5
- package/dist/cli/grok-session-hook.js +69 -0
- package/dist/cli/install-commands.js +33 -8
- package/dist/cli/output.js +1 -1
- package/dist/cli/registry.js +10 -0
- package/dist/grok-session-store.js +140 -0
- package/dist/identity.js +77 -8
- package/dist/index.js +2 -1
- package/dist/install.js +104 -1
- package/dist/instructions.js +9 -1
- package/dist/skill-install.js +3 -1
- package/docs/releases/0.4.10.md +17 -0
- package/package.json +1 -1
- package/skills/talking-stick/SKILL.md +2 -2
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.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`.
|
|
5
|
+
**Version:** 0.4.1. Multi-process-safe (SQLite WAL), liveness-aware, no daemon. Supports Claude Code, Codex CLI, Gemini CLI, Grok Build, 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
|
|
|
@@ -166,13 +166,14 @@ For harnesses that only notice completed subprocesses, run `tt events --wait --a
|
|
|
166
166
|
- Claude Code: copied or linked into `~/.claude/skills/talking-stick`
|
|
167
167
|
- Codex: copied or linked into `~/.codex/skills/talking-stick`
|
|
168
168
|
- Gemini: installed with `gemini skills install ... --scope user` or linked with `gemini skills link ... --scope user`
|
|
169
|
+
- Grok Build: copied or linked into `~/.grok/skills/talking-stick`, plus a trusted global session hook at `~/.grok/hooks/talking-stick-session.json`
|
|
169
170
|
- OpenCode: copied or linked into `~/.opencode/skills/talking-stick`
|
|
170
171
|
|
|
171
172
|
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.
|
|
172
173
|
|
|
173
|
-
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`.
|
|
174
|
+
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. Grok Build has no Talking Stick MCP registration path; install is native skill plus hook only. Every cleanup run appends JSONL audit entries to `${TALKING_STICK_DATA_DIR}/update-migrations.log`.
|
|
174
175
|
|
|
175
|
-
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.
|
|
176
|
+
Human CLI invocations also perform a silent best-effort sync for already-installed file-based skills in Claude Code, Codex, Grok Build, 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.
|
|
176
177
|
|
|
177
178
|
## Human CLI
|
|
178
179
|
|
|
@@ -189,7 +190,7 @@ tt state [path] # full room state
|
|
|
189
190
|
tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent] # room event log; --wait/--follow long-polls
|
|
190
191
|
tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR] # send an OOB message
|
|
191
192
|
tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR] # receive OOB messages
|
|
192
|
-
tt instructions show [path] [--harness claude|codex|gemini|opencode|all] [--scope effective|bundled|user|project] # show collaboration prompt
|
|
193
|
+
tt instructions show [path] [--harness claude|codex|gemini|grok|opencode|all] [--scope effective|bundled|user|project] # show collaboration prompt
|
|
193
194
|
tt instructions edit [path] [--user|--project] # edit user or project prompt
|
|
194
195
|
tt instructions reset [path] (--user|--project) # delete a user or project prompt
|
|
195
196
|
tt release [path] --status TEXT --next-action TEXT # normal handoff
|
|
@@ -216,7 +217,8 @@ By default, `tt` behaves like a human CLI and resolves to `human:<username>` onl
|
|
|
216
217
|
|
|
217
218
|
Harness-aware CLI identity is resolved before the human fallback:
|
|
218
219
|
|
|
219
|
-
- Known harness environment markers such as `CLAUDECODE=1`, `CODEX_THREAD_ID`, `GEMINI_CLI=1`, or `OPENCODE=1` make `tt` derive a harness-style identity automatically.
|
|
220
|
+
- Known harness environment markers such as `CLAUDECODE=1`, `CODEX_THREAD_ID`, `GEMINI_CLI=1`, `CMUX_AGENT_LAUNCH_KIND=grok`, or `OPENCODE=1` make `tt` derive a harness-style identity automatically. The cmux Grok marker is optional; Grok Build also works without cmux by walking process ancestry for a `grok` root process.
|
|
221
|
+
- Grok Build's installed hook records hook-only `GROK_SESSION_ID` context into `${TALKING_STICK_DATA_DIR}/grok-sessions.jsonl`, letting later Grok-launched `tt` calls upgrade from process identity to the real Grok session id. `GROK_SESSION_ID` by itself is not treated as a normal shell marker, and the hook is not required for basic Grok detection.
|
|
220
222
|
- Set `TT_HARNESS_AGENT_ID=<agent-id>` if the harness wants to export the exact agent id directly.
|
|
221
223
|
- Set `TT_HARNESS_EXPORT=1` only when you need ancestry-based harness detection without a known harness environment marker.
|
|
222
224
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createSystemProcessInspector } from "../process-utils.js";
|
|
2
|
+
import { appendGrokSessionRecord } from "../grok-session-store.js";
|
|
3
|
+
import { findHarnessRootInAncestry } from "../identity.js";
|
|
4
|
+
export async function runGrokSessionHookCommand(options = {}) {
|
|
5
|
+
try {
|
|
6
|
+
const env = options.env ?? process.env;
|
|
7
|
+
const input = parseHookInput(options.stdin ?? await readStdin());
|
|
8
|
+
const sessionId = firstNonEmptyString(env.GROK_SESSION_ID, input.sessionId);
|
|
9
|
+
const workspaceRoot = firstNonEmptyString(env.GROK_WORKSPACE_ROOT, env.CLAUDE_PROJECT_DIR, input.workspaceRoot);
|
|
10
|
+
if (!sessionId || !workspaceRoot) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const event = firstNonEmptyString(env.GROK_HOOK_EVENT, input.hookEventName) ?? "unknown";
|
|
14
|
+
const inspector = options.inspector ?? createSystemProcessInspector();
|
|
15
|
+
const parentPid = options.parentPid ?? process.ppid;
|
|
16
|
+
const parentInspection = inspector.inspect(parentPid);
|
|
17
|
+
const grokRoot = findHarnessRootInAncestry("grok", parentPid, parentInspection, inspector, 20);
|
|
18
|
+
const record = {
|
|
19
|
+
source: "grok_hook",
|
|
20
|
+
grok_session_id: sessionId,
|
|
21
|
+
workspace_root: workspaceRoot,
|
|
22
|
+
cwd: firstNonEmptyString(input.cwd),
|
|
23
|
+
event,
|
|
24
|
+
observed_at: firstNonEmptyString(input.timestamp) ??
|
|
25
|
+
(options.now ?? new Date()).toISOString(),
|
|
26
|
+
grok_pid: grokRoot?.pid ?? null,
|
|
27
|
+
grok_process_started_at: grokRoot?.startTime ?? null
|
|
28
|
+
};
|
|
29
|
+
appendGrokSessionRecord(record, { logPath: options.logPath });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Grok hooks must fail open. Identity can fall back to pid-root detection
|
|
33
|
+
// when the hook cannot record a session row.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function parseHookInput(raw) {
|
|
37
|
+
const trimmed = raw.trim();
|
|
38
|
+
if (!trimmed)
|
|
39
|
+
return {};
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(trimmed);
|
|
42
|
+
return isObjectRecord(parsed) ? parsed : {};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function readStdin() {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
let raw = "";
|
|
51
|
+
process.stdin.setEncoding("utf8");
|
|
52
|
+
process.stdin.on("data", (chunk) => {
|
|
53
|
+
raw += chunk;
|
|
54
|
+
});
|
|
55
|
+
process.stdin.on("error", reject);
|
|
56
|
+
process.stdin.on("end", () => resolve(raw));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function firstNonEmptyString(...values) {
|
|
60
|
+
for (const value of values) {
|
|
61
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
62
|
+
return value.trim();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function isObjectRecord(value) {
|
|
68
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
69
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { SUPPORTED_HARNESSES, detectHarness, parseHarnessList, planUninstall, runAction } from "../install.js";
|
|
2
|
+
import { SUPPORTED_HARNESSES, detectHarness, parseHarnessList, planGrokSessionHookInstall, planGrokSessionHookUninstall, planUninstall, runAction } from "../install.js";
|
|
3
3
|
import { planSkillInstall, planSkillUninstall } from "../skill-install.js";
|
|
4
4
|
import { resolveDataDir } from "../config.js";
|
|
5
5
|
import { FileAuditLog, defaultAuditLogPath } from "../install-audit.js";
|
|
@@ -145,7 +145,7 @@ function resolveSkillInstallLinkMode(parsed) {
|
|
|
145
145
|
return true;
|
|
146
146
|
}
|
|
147
147
|
function planInstallActions(harnesses, installOptions) {
|
|
148
|
-
return harnesses.
|
|
148
|
+
return harnesses.flatMap((harness) => planInstallActionsForHarness(harness, installOptions));
|
|
149
149
|
}
|
|
150
150
|
function planUninstallActions(harnesses, installOptions) {
|
|
151
151
|
return harnesses.flatMap((harness) => [
|
|
@@ -153,6 +153,14 @@ function planUninstallActions(harnesses, installOptions) {
|
|
|
153
153
|
...installOptions,
|
|
154
154
|
skipMissing: false
|
|
155
155
|
}),
|
|
156
|
+
...(harness === "grok"
|
|
157
|
+
? [
|
|
158
|
+
planGrokSessionHookUninstall({
|
|
159
|
+
...installOptions,
|
|
160
|
+
skipMissing: false
|
|
161
|
+
})
|
|
162
|
+
]
|
|
163
|
+
: []),
|
|
156
164
|
planUninstall(harness, installOptions)
|
|
157
165
|
]);
|
|
158
166
|
}
|
|
@@ -160,14 +168,31 @@ function planCleanupActions(harnesses, installOptions) {
|
|
|
160
168
|
return harnesses.map((harness) => planUninstall(harness, installOptions));
|
|
161
169
|
}
|
|
162
170
|
async function runSkillInstall(harness, installOptions) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
return [skillResult];
|
|
171
|
+
const actions = planInstallActionsForHarness(harness, installOptions);
|
|
172
|
+
return Promise.all(actions.map((action) => runAction(action, installOptions)));
|
|
166
173
|
}
|
|
167
174
|
async function runSkillUninstall(harness, installOptions) {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
const actions = [
|
|
176
|
+
planSkillUninstall(harness, {
|
|
177
|
+
...installOptions,
|
|
178
|
+
skipMissing: false
|
|
179
|
+
}),
|
|
180
|
+
...(harness === "grok"
|
|
181
|
+
? [
|
|
182
|
+
planGrokSessionHookUninstall({
|
|
183
|
+
...installOptions,
|
|
184
|
+
skipMissing: false
|
|
185
|
+
})
|
|
186
|
+
]
|
|
187
|
+
: [])
|
|
188
|
+
];
|
|
189
|
+
return Promise.all(actions.map((action) => runAction(action, installOptions)));
|
|
190
|
+
}
|
|
191
|
+
function planInstallActionsForHarness(harness, installOptions) {
|
|
192
|
+
return [
|
|
193
|
+
planSkillInstall(harness, installOptions),
|
|
194
|
+
...(harness === "grok" ? [planGrokSessionHookInstall(installOptions)] : [])
|
|
195
|
+
];
|
|
171
196
|
}
|
|
172
197
|
async function runCleanup(harnesses, reason, installOptions) {
|
|
173
198
|
const dataDir = resolveDataDir();
|
package/dist/cli/output.js
CHANGED
|
@@ -164,7 +164,7 @@ Commands:
|
|
|
164
164
|
tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent]
|
|
165
165
|
tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR]
|
|
166
166
|
tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR]
|
|
167
|
-
tt instructions show [path] [--harness claude|codex|gemini|opencode|all] [--scope effective|bundled|user|project]
|
|
167
|
+
tt instructions show [path] [--harness claude|codex|gemini|grok|opencode|all] [--scope effective|bundled|user|project]
|
|
168
168
|
tt instructions edit [path] [--user|--project]
|
|
169
169
|
tt instructions reset [path] (--user|--project)
|
|
170
170
|
tt release [path] (--status TEXT --next-action TEXT | --stdin)
|
package/dist/cli/registry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runGuardCommand } from "./guardian.js";
|
|
2
|
+
import { runGrokSessionHookCommand } from "./grok-session-hook.js";
|
|
2
3
|
import { runInstallCommand, runMcpMigrationCommand, runSelfUpdateCommand, runUninstallCommand } from "./install-commands.js";
|
|
3
4
|
import { handleInstructionsCommand } from "./instructions-commands.js";
|
|
4
5
|
import { handleMsgCommand } from "./msg-commands.js";
|
|
@@ -15,6 +16,15 @@ export const COMMAND_REGISTRY = [
|
|
|
15
16
|
description: "Run an internal lease heartbeat guardian.",
|
|
16
17
|
handler: ({ parsed }) => runGuardCommand(parsed)
|
|
17
18
|
},
|
|
19
|
+
{
|
|
20
|
+
name: "grok-session-hook",
|
|
21
|
+
needsRuntime: false,
|
|
22
|
+
startupMaintenance: false,
|
|
23
|
+
internal: true,
|
|
24
|
+
usage: "tt grok-session-hook",
|
|
25
|
+
description: "Record Grok hook session context for identity resolution.",
|
|
26
|
+
handler: () => runGrokSessionHookCommand()
|
|
27
|
+
},
|
|
18
28
|
{
|
|
19
29
|
name: "install",
|
|
20
30
|
needsRuntime: false,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveDataDir } from "./config.js";
|
|
4
|
+
export const DEFAULT_GROK_SESSION_RECORD_MAX_AGE_MS = 4 * 60 * 60 * 1000;
|
|
5
|
+
export function resolveGrokSessionLogPath(options = {}) {
|
|
6
|
+
return path.join(resolveDataDir(options), "grok-sessions.jsonl");
|
|
7
|
+
}
|
|
8
|
+
export function appendGrokSessionRecord(record, options = {}) {
|
|
9
|
+
const logPath = options.logPath ?? resolveGrokSessionLogPath(options.dataDirOptions ?? {});
|
|
10
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
11
|
+
fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`, "utf8");
|
|
12
|
+
}
|
|
13
|
+
export function readGrokSessionRecords(logPath) {
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = fs.readFileSync(logPath, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === "ENOENT") {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
const records = [];
|
|
25
|
+
for (const line of raw.split("\n")) {
|
|
26
|
+
if (!line.trim())
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(line);
|
|
30
|
+
const record = parseGrokSessionRecord(parsed);
|
|
31
|
+
if (record)
|
|
32
|
+
records.push(record);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Hook logs are append-only and best-effort; one bad line should not
|
|
36
|
+
// break identity resolution for the whole session.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return records;
|
|
40
|
+
}
|
|
41
|
+
export function findGrokSessionRecord(input) {
|
|
42
|
+
const workspaceRoot = normalizeWorkspaceRoot(input.workspaceRoot);
|
|
43
|
+
if (!workspaceRoot)
|
|
44
|
+
return null;
|
|
45
|
+
const logPath = input.logPath ?? resolveGrokSessionLogPath();
|
|
46
|
+
const nowMs = input.now?.getTime() ?? Date.now();
|
|
47
|
+
const maxAgeMs = input.maxAgeMs ?? DEFAULT_GROK_SESSION_RECORD_MAX_AGE_MS;
|
|
48
|
+
const records = readGrokSessionRecords(logPath);
|
|
49
|
+
const endedSessionIds = new Set();
|
|
50
|
+
const workspaceCandidates = [];
|
|
51
|
+
for (const record of records.slice().reverse()) {
|
|
52
|
+
if (normalizeWorkspaceRoot(record.workspace_root) !== workspaceRoot) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (isStaleRecord(record, nowMs, maxAgeMs)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (isGrokSessionEndEvent(record.event)) {
|
|
59
|
+
endedSessionIds.add(record.grok_session_id);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (endedSessionIds.has(record.grok_session_id)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
workspaceCandidates.push(record);
|
|
66
|
+
if (input.grokPid != null &&
|
|
67
|
+
input.grokProcessStartedAt != null &&
|
|
68
|
+
record.grok_pid === input.grokPid &&
|
|
69
|
+
record.grok_process_started_at === input.grokProcessStartedAt) {
|
|
70
|
+
return record;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const uniqueSessionIds = new Set(workspaceCandidates.map((record) => record.grok_session_id));
|
|
74
|
+
if (uniqueSessionIds.size === 1) {
|
|
75
|
+
return workspaceCandidates[0] ?? null;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
export function isGrokSessionEndEvent(event) {
|
|
80
|
+
return normalizeEventName(event) === "sessionend";
|
|
81
|
+
}
|
|
82
|
+
function parseGrokSessionRecord(value) {
|
|
83
|
+
if (!isObjectRecord(value))
|
|
84
|
+
return null;
|
|
85
|
+
if (value.source !== "grok_hook")
|
|
86
|
+
return null;
|
|
87
|
+
const grokSessionId = nonEmptyString(value.grok_session_id);
|
|
88
|
+
const workspaceRoot = nonEmptyString(value.workspace_root);
|
|
89
|
+
const event = nonEmptyString(value.event);
|
|
90
|
+
const observedAt = nonEmptyString(value.observed_at);
|
|
91
|
+
if (!grokSessionId || !workspaceRoot || !event || !observedAt) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
source: "grok_hook",
|
|
96
|
+
grok_session_id: grokSessionId,
|
|
97
|
+
workspace_root: workspaceRoot,
|
|
98
|
+
cwd: nullableString(value.cwd),
|
|
99
|
+
event,
|
|
100
|
+
observed_at: observedAt,
|
|
101
|
+
grok_pid: nullableInteger(value.grok_pid),
|
|
102
|
+
grok_process_started_at: nullableString(value.grok_process_started_at)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function isStaleRecord(record, nowMs, maxAgeMs) {
|
|
106
|
+
const observedAtMs = Date.parse(record.observed_at);
|
|
107
|
+
if (Number.isNaN(observedAtMs))
|
|
108
|
+
return true;
|
|
109
|
+
return nowMs - observedAtMs > maxAgeMs;
|
|
110
|
+
}
|
|
111
|
+
function normalizeWorkspaceRoot(value) {
|
|
112
|
+
const trimmed = value?.trim();
|
|
113
|
+
if (!trimmed)
|
|
114
|
+
return null;
|
|
115
|
+
try {
|
|
116
|
+
return fs.realpathSync.native(trimmed);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return path.resolve(trimmed);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function normalizeEventName(event) {
|
|
123
|
+
return event.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
124
|
+
}
|
|
125
|
+
function nonEmptyString(value) {
|
|
126
|
+
return typeof value === "string" && value.trim().length > 0
|
|
127
|
+
? value
|
|
128
|
+
: null;
|
|
129
|
+
}
|
|
130
|
+
function nullableString(value) {
|
|
131
|
+
return typeof value === "string" && value.trim().length > 0
|
|
132
|
+
? value
|
|
133
|
+
: null;
|
|
134
|
+
}
|
|
135
|
+
function nullableInteger(value) {
|
|
136
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
137
|
+
}
|
|
138
|
+
function isObjectRecord(value) {
|
|
139
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
140
|
+
}
|
package/dist/identity.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { findGrokSessionRecord, resolveGrokSessionLogPath } from "./grok-session-store.js";
|
|
5
|
+
import { resolveContextPath } from "./path-resolution.js";
|
|
4
6
|
import { createSystemProcessInspector } from "./process-utils.js";
|
|
5
7
|
const HARNESS_CLI_EXPORT_ENV = "TT_HARNESS_EXPORT";
|
|
6
8
|
const HARNESS_CLI_AGENT_ID_ENV = "TT_HARNESS_AGENT_ID";
|
|
@@ -41,7 +43,11 @@ export function deriveMcpHarnessIdentity(options = {}) {
|
|
|
41
43
|
const signal = detectHarnessSignal(env);
|
|
42
44
|
if (signal) {
|
|
43
45
|
const processRef = resolveSignalProcessRef(signal, parentPid, parentInspection, inspector);
|
|
44
|
-
const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector
|
|
46
|
+
const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector, {
|
|
47
|
+
contextPath: options.contextPath,
|
|
48
|
+
grokSessionLogPath: options.grokSessionLogPath,
|
|
49
|
+
now: options.now
|
|
50
|
+
});
|
|
45
51
|
const harnessProcess = resolveHarnessProcessRef(signal, processRef, inspector);
|
|
46
52
|
const agentId = options.agentId ?? harnessAgentId(signal.harness, sessionId, hostId, username);
|
|
47
53
|
return {
|
|
@@ -100,7 +106,9 @@ export function deriveHarnessCliIdentity(options = {}) {
|
|
|
100
106
|
}
|
|
101
107
|
let signal = detectHarnessSignal(env);
|
|
102
108
|
if (!signal && !isHarnessCliExportEnabled(env)) {
|
|
103
|
-
|
|
109
|
+
signal = detectGrokViaAncestry(parentPid, parentInspection, inspector);
|
|
110
|
+
if (!signal)
|
|
111
|
+
return null;
|
|
104
112
|
}
|
|
105
113
|
if (!signal) {
|
|
106
114
|
signal = detectHarnessViaAncestry(parentPid, inspector);
|
|
@@ -109,7 +117,11 @@ export function deriveHarnessCliIdentity(options = {}) {
|
|
|
109
117
|
return null;
|
|
110
118
|
const processRef = resolveSignalProcessRef(signal, parentPid, parentInspection, inspector);
|
|
111
119
|
const username = options.username ?? safeUsername();
|
|
112
|
-
const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector
|
|
120
|
+
const sessionId = resolveHarnessSessionId(signal, env, processRef.pid, processRef.inspection, username, hostId, inspector, {
|
|
121
|
+
contextPath: options.contextPath,
|
|
122
|
+
grokSessionLogPath: options.grokSessionLogPath,
|
|
123
|
+
now: options.now
|
|
124
|
+
});
|
|
113
125
|
const agentId = options.agentId ?? harnessAgentId(signal.harness, sessionId, hostId, username);
|
|
114
126
|
const harnessProcess = resolveHarnessProcessRef(signal, processRef, inspector);
|
|
115
127
|
return {
|
|
@@ -136,10 +148,16 @@ function harnessAgentId(harness, sessionId, hostId, username) {
|
|
|
136
148
|
sanitizeIdentityComponent(username)
|
|
137
149
|
])}`;
|
|
138
150
|
}
|
|
139
|
-
function resolveHarnessSessionId(signal, env, parentPid, parentInspection, username, hostId, inspector) {
|
|
151
|
+
function resolveHarnessSessionId(signal, env, parentPid, parentInspection, username, hostId, inspector, options = {}) {
|
|
140
152
|
if (signal.sessionId)
|
|
141
153
|
return `harness:${signal.sessionId}`;
|
|
142
154
|
const harnessRoot = findHarnessRootInAncestry(signal.harness, parentPid, parentInspection, inspector);
|
|
155
|
+
if (signal.harness === "grok") {
|
|
156
|
+
const grokSessionId = resolveGrokHookSessionId(env, harnessRoot, options);
|
|
157
|
+
if (grokSessionId) {
|
|
158
|
+
return `harness:${grokSessionId}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
143
161
|
if (harnessRoot) {
|
|
144
162
|
return `pid:${harnessRoot.pid}@${harnessRoot.startTime}`;
|
|
145
163
|
}
|
|
@@ -165,7 +183,7 @@ function resolveHarnessProcessRef(signal, processRef, inspector) {
|
|
|
165
183
|
// process whose command matches the named harness. Anchoring session id to
|
|
166
184
|
// that root keeps `tt` invocations stable whether they're spawned directly
|
|
167
185
|
// by the harness (MCP subprocess) or through intermediate shells (CLI shell-out).
|
|
168
|
-
function findHarnessRootInAncestry(harness, startPid, startInspection, inspector, maxDepth = 10) {
|
|
186
|
+
export function findHarnessRootInAncestry(harness, startPid, startInspection, inspector, maxDepth = 10) {
|
|
169
187
|
let result = null;
|
|
170
188
|
let currentPid = startPid;
|
|
171
189
|
let currentInspection = startInspection;
|
|
@@ -220,6 +238,7 @@ const HARNESS_COMMAND_MAPPING = {
|
|
|
220
238
|
"claude-code": "claude",
|
|
221
239
|
codex: "codex",
|
|
222
240
|
gemini: "gemini",
|
|
241
|
+
grok: "grok",
|
|
223
242
|
opencode: "opencode"
|
|
224
243
|
};
|
|
225
244
|
function detectHarnessViaAncestry(pid, inspector, maxDepth = 10) {
|
|
@@ -231,10 +250,13 @@ function detectHarnessViaAncestry(pid, inspector, maxDepth = 10) {
|
|
|
231
250
|
if (!inspection)
|
|
232
251
|
break;
|
|
233
252
|
const label = deriveCommandLabel(inspection.command);
|
|
234
|
-
|
|
253
|
+
const harness = HARNESS_COMMAND_MAPPING[label];
|
|
254
|
+
if (harness) {
|
|
235
255
|
return {
|
|
236
|
-
harness
|
|
237
|
-
sessionId:
|
|
256
|
+
harness,
|
|
257
|
+
sessionId: harness === "grok"
|
|
258
|
+
? null
|
|
259
|
+
: `pid:${inspection.pid}@${inspection.startTime}`,
|
|
238
260
|
pidHint: null
|
|
239
261
|
};
|
|
240
262
|
}
|
|
@@ -267,8 +289,51 @@ function detectHarnessSignal(env) {
|
|
|
267
289
|
pidHint: null
|
|
268
290
|
};
|
|
269
291
|
}
|
|
292
|
+
const cmuxHarness = resolveCmuxLaunchHarness(env);
|
|
293
|
+
if (cmuxHarness) {
|
|
294
|
+
return {
|
|
295
|
+
harness: cmuxHarness,
|
|
296
|
+
sessionId: null,
|
|
297
|
+
pidHint: null
|
|
298
|
+
};
|
|
299
|
+
}
|
|
270
300
|
return null;
|
|
271
301
|
}
|
|
302
|
+
function detectGrokViaAncestry(parentPid, parentInspection, inspector) {
|
|
303
|
+
const grokRoot = findHarnessRootInAncestry("grok", parentPid, parentInspection, inspector, 20);
|
|
304
|
+
return grokRoot
|
|
305
|
+
? { harness: "grok", sessionId: null, pidHint: null }
|
|
306
|
+
: null;
|
|
307
|
+
}
|
|
308
|
+
function resolveGrokHookSessionId(env, harnessRoot, options) {
|
|
309
|
+
const workspaceRoot = resolveGrokWorkspaceRoot(env, options.contextPath);
|
|
310
|
+
const record = findGrokSessionRecord({
|
|
311
|
+
logPath: options.grokSessionLogPath ??
|
|
312
|
+
resolveGrokSessionLogPath({ env }),
|
|
313
|
+
workspaceRoot,
|
|
314
|
+
grokPid: harnessRoot?.pid ?? null,
|
|
315
|
+
grokProcessStartedAt: harnessRoot?.startTime ?? null,
|
|
316
|
+
now: options.now
|
|
317
|
+
});
|
|
318
|
+
return record?.grok_session_id ?? null;
|
|
319
|
+
}
|
|
320
|
+
function resolveGrokWorkspaceRoot(env, contextPath) {
|
|
321
|
+
const explicit = nonEmpty(env.GROK_WORKSPACE_ROOT) ??
|
|
322
|
+
nonEmpty(env.CLAUDE_PROJECT_DIR);
|
|
323
|
+
if (explicit)
|
|
324
|
+
return path.resolve(explicit);
|
|
325
|
+
const candidate = contextPath ?? nonEmpty(env.PWD) ?? process.cwd();
|
|
326
|
+
try {
|
|
327
|
+
return resolveContextPath(candidate).workspace_root;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return path.resolve(candidate);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function resolveCmuxLaunchHarness(env) {
|
|
334
|
+
const launchKind = normalizeEnvValue(env.CMUX_AGENT_LAUNCH_KIND);
|
|
335
|
+
return launchKind ? HARNESS_COMMAND_MAPPING[launchKind] ?? null : null;
|
|
336
|
+
}
|
|
272
337
|
function resolveSignalProcessRef(signal, fallbackPid, fallbackInspection, inspector) {
|
|
273
338
|
if (signal.pidHint && signal.pidHint !== fallbackPid) {
|
|
274
339
|
const hintedInspection = inspector.inspect(signal.pidHint);
|
|
@@ -294,6 +359,10 @@ function parsePositiveInteger(value) {
|
|
|
294
359
|
function nonEmpty(value) {
|
|
295
360
|
return value && value.trim().length > 0 ? value : null;
|
|
296
361
|
}
|
|
362
|
+
function normalizeEnvValue(value) {
|
|
363
|
+
const nonBlank = nonEmpty(value);
|
|
364
|
+
return nonBlank ? nonBlank.toLowerCase() : null;
|
|
365
|
+
}
|
|
297
366
|
function deriveCommandLabel(command) {
|
|
298
367
|
if (!command) {
|
|
299
368
|
return "harness";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ export { ProtocolError, isProtocolError } from "./errors.js";
|
|
|
5
5
|
export { deriveHarnessCliIdentity, deriveHumanCliIdentity, deriveMcpHarnessIdentity } from "./identity.js";
|
|
6
6
|
export { ancestorPaths, canonicalizeContextPath, resolveContextPath, resolveWorkspaceRoot } from "./path-resolution.js";
|
|
7
7
|
export { DEFAULT_MAX_INSTRUCTION_FILE_BYTES, DEFAULT_INSTRUCTIONS_MARKDOWN, editInstructions, extractHarnessInstructions, normalizeInstructionHarness, parseInstructionScope, resetInstructions, resolveInstructionHarness, resolveInstructionPaths, showInstructions } from "./instructions.js";
|
|
8
|
-
export { SUPPORTED_HARNESSES, MissingHarnessError, detectHarness, parseHarnessList, planUninstall, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
|
|
8
|
+
export { SUPPORTED_HARNESSES, buildGrokSessionHookConfig, DEFAULT_GROK_SESSION_HOOK_COMMAND, GROK_SESSION_HOOK_EVENTS, GROK_SESSION_HOOK_FILE, MissingHarnessError, detectHarness, parseHarnessList, planGrokSessionHookInstall, planGrokSessionHookUninstall, planUninstall, resolveGrokSessionHookPath, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
|
|
9
|
+
export { DEFAULT_GROK_SESSION_RECORD_MAX_AGE_MS, appendGrokSessionRecord, findGrokSessionRecord, isGrokSessionEndEvent, readGrokSessionRecords, resolveGrokSessionLogPath } from "./grok-session-store.js";
|
|
9
10
|
export { DEFAULT_SKILL_NAME, planSkillInstall, planSkillUninstall, resolveBundledSkillPath, resolveSkillTargetPath, syncInstalledSkills } from "./skill-install.js";
|
|
10
11
|
export { readPackageVersion, readUpdateMigrationState, resolveUpdateMigrationStatePath, runFirstRunMcpMigration, runStaleMcpCleanup, writeUpdateMigrationState } from "./update-migration.js";
|
|
11
12
|
export { createSystemProcessInspector, terminateKnownProcess } from "./process-utils.js";
|
package/dist/install.js
CHANGED
|
@@ -2,9 +2,17 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
export const SUPPORTED_HARNESSES = ["claude-code", "codex", "gemini", "opencode"];
|
|
5
|
+
export const SUPPORTED_HARNESSES = ["claude-code", "codex", "gemini", "grok", "opencode"];
|
|
6
6
|
export const DEFAULT_SERVER_NAME = "talking-stick";
|
|
7
7
|
export const DEFAULT_SERVER_COMMAND = ["tt", "mcp"];
|
|
8
|
+
export const GROK_SESSION_HOOK_FILE = "talking-stick-session.json";
|
|
9
|
+
export const DEFAULT_GROK_SESSION_HOOK_COMMAND = ": talking-stick-grok-session-hook; if command -v tt >/dev/null 2>&1; then tt grok-session-hook >/dev/null 2>/dev/null || true; fi";
|
|
10
|
+
export const GROK_SESSION_HOOK_EVENTS = [
|
|
11
|
+
"SessionStart",
|
|
12
|
+
"UserPromptSubmit",
|
|
13
|
+
"PreToolUse",
|
|
14
|
+
"SessionEnd"
|
|
15
|
+
];
|
|
8
16
|
export class MissingHarnessError extends Error {
|
|
9
17
|
constructor(message) {
|
|
10
18
|
super(message);
|
|
@@ -96,11 +104,21 @@ export function resolveHarnessConfigDir(harness, options = {}) {
|
|
|
96
104
|
const resolved = resolveOptions(options);
|
|
97
105
|
return resolveHarnessConfigDirFromResolved(harness, resolved);
|
|
98
106
|
}
|
|
107
|
+
export function resolveGrokSessionHookPath(options = {}) {
|
|
108
|
+
const resolved = resolveOptions(options);
|
|
109
|
+
return path.join(resolveGrokConfigDirFromResolved(resolved), "hooks", GROK_SESSION_HOOK_FILE);
|
|
110
|
+
}
|
|
99
111
|
function resolveOpencodeConfigDirFromResolved(resolved) {
|
|
100
112
|
const xdg = resolved.env.XDG_CONFIG_HOME?.trim();
|
|
101
113
|
const base = xdg && xdg.length > 0 ? xdg : path.join(resolved.homeDir, ".config");
|
|
102
114
|
return path.join(base, "opencode");
|
|
103
115
|
}
|
|
116
|
+
function resolveGrokConfigDirFromResolved(resolved) {
|
|
117
|
+
const grokHome = resolved.env.GROK_HOME?.trim();
|
|
118
|
+
return grokHome && grokHome.length > 0
|
|
119
|
+
? grokHome
|
|
120
|
+
: path.join(resolved.homeDir, ".grok");
|
|
121
|
+
}
|
|
104
122
|
function resolveHarnessConfigDirFromResolved(harness, resolved) {
|
|
105
123
|
switch (harness) {
|
|
106
124
|
case "claude-code":
|
|
@@ -109,6 +127,8 @@ function resolveHarnessConfigDirFromResolved(harness, resolved) {
|
|
|
109
127
|
return path.join(resolved.homeDir, ".codex");
|
|
110
128
|
case "gemini":
|
|
111
129
|
return path.join(resolved.homeDir, ".gemini");
|
|
130
|
+
case "grok":
|
|
131
|
+
return resolveGrokConfigDirFromResolved(resolved);
|
|
112
132
|
case "opencode":
|
|
113
133
|
return resolveOpencodeConfigDirFromResolved(resolved);
|
|
114
134
|
default:
|
|
@@ -157,6 +177,8 @@ export function planUninstall(harness, options = {}) {
|
|
|
157
177
|
operation: "uninstall",
|
|
158
178
|
serverName: resolved.serverName
|
|
159
179
|
};
|
|
180
|
+
case "grok":
|
|
181
|
+
return skipAction(harness, "legacy Talking Stick cleanup is not applicable for grok");
|
|
160
182
|
case "opencode": {
|
|
161
183
|
const filePath = resolveOpencodeConfigPath(options);
|
|
162
184
|
const configDir = path.dirname(filePath);
|
|
@@ -189,6 +211,76 @@ export function skipAction(harness, message) {
|
|
|
189
211
|
message
|
|
190
212
|
};
|
|
191
213
|
}
|
|
214
|
+
export function planGrokSessionHookInstall(options = {}) {
|
|
215
|
+
const resolved = resolveOptions(options);
|
|
216
|
+
const grokConfigDir = resolveGrokConfigDirFromResolved(resolved);
|
|
217
|
+
const filePath = resolveGrokSessionHookPath(options);
|
|
218
|
+
if (resolved.skipMissing && !resolved.hooks.pathExists(grokConfigDir)) {
|
|
219
|
+
return skipAction("grok", `grok config directory not found: ${grokConfigDir}`);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
kind: "file-patch",
|
|
223
|
+
harness: "grok",
|
|
224
|
+
filePath,
|
|
225
|
+
description: `write Grok session hook ${filePath}`,
|
|
226
|
+
inspect: () => inspectGrokSessionHook(filePath, resolved),
|
|
227
|
+
apply: () => writeGrokSessionHook(filePath, resolved)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function planGrokSessionHookUninstall(options = {}) {
|
|
231
|
+
const resolved = resolveOptions(options);
|
|
232
|
+
const grokConfigDir = resolveGrokConfigDirFromResolved(resolved);
|
|
233
|
+
const filePath = resolveGrokSessionHookPath(options);
|
|
234
|
+
if (resolved.skipMissing && !resolved.hooks.pathExists(grokConfigDir)) {
|
|
235
|
+
return skipAction("grok", `grok config directory not found: ${grokConfigDir}`);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
kind: "file-patch",
|
|
239
|
+
harness: "grok",
|
|
240
|
+
filePath,
|
|
241
|
+
description: `remove Grok session hook ${filePath}`,
|
|
242
|
+
inspect: () => resolved.hooks.readFile(filePath) === null ? "absent" : "present",
|
|
243
|
+
apply: () => removeGrokSessionHook(filePath, resolved)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
export function buildGrokSessionHookConfig() {
|
|
247
|
+
const hook = {
|
|
248
|
+
type: "command",
|
|
249
|
+
command: DEFAULT_GROK_SESSION_HOOK_COMMAND,
|
|
250
|
+
timeout: 5
|
|
251
|
+
};
|
|
252
|
+
const hooks = Object.fromEntries(GROK_SESSION_HOOK_EVENTS.map((event) => [
|
|
253
|
+
event,
|
|
254
|
+
[
|
|
255
|
+
{
|
|
256
|
+
hooks: [hook]
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
]));
|
|
260
|
+
return JSON.stringify({ hooks }, null, 2) + "\n";
|
|
261
|
+
}
|
|
262
|
+
function inspectGrokSessionHook(filePath, resolved) {
|
|
263
|
+
const existing = resolved.hooks.readFile(filePath);
|
|
264
|
+
if (existing === null)
|
|
265
|
+
return "absent";
|
|
266
|
+
return existing === buildGrokSessionHookConfig() ? "present" : "different";
|
|
267
|
+
}
|
|
268
|
+
function writeGrokSessionHook(filePath, resolved) {
|
|
269
|
+
resolved.hooks.ensureDir(path.dirname(filePath));
|
|
270
|
+
resolved.hooks.writeFile(filePath, buildGrokSessionHookConfig());
|
|
271
|
+
}
|
|
272
|
+
function removeGrokSessionHook(filePath, resolved) {
|
|
273
|
+
void resolved;
|
|
274
|
+
try {
|
|
275
|
+
fs.rmSync(filePath, { force: true });
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error.code === "ENOENT") {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
192
284
|
function patchOpencodeConfig(filePath, resolved, mode) {
|
|
193
285
|
const existing = resolved.hooks.readFile(filePath);
|
|
194
286
|
if (resolved.skipMissing) {
|
|
@@ -305,6 +397,15 @@ export function detectHarness(harness, options = {}) {
|
|
|
305
397
|
return { harness, detected: true, evidence: configDir };
|
|
306
398
|
return { harness, detected: false, evidence: "gemini not on PATH and no config directory" };
|
|
307
399
|
}
|
|
400
|
+
case "grok": {
|
|
401
|
+
const bin = resolved.hooks.which("grok");
|
|
402
|
+
if (bin)
|
|
403
|
+
return { harness, detected: true, evidence: bin };
|
|
404
|
+
const configDir = resolveHarnessConfigDirFromResolved(harness, resolved);
|
|
405
|
+
if (resolved.hooks.pathExists(configDir))
|
|
406
|
+
return { harness, detected: true, evidence: configDir };
|
|
407
|
+
return { harness, detected: false, evidence: "grok not on PATH and no config directory" };
|
|
408
|
+
}
|
|
308
409
|
case "opencode": {
|
|
309
410
|
const bin = resolved.hooks.which("opencode");
|
|
310
411
|
if (bin)
|
|
@@ -534,6 +635,8 @@ function mcpConfigLocation(action) {
|
|
|
534
635
|
return "Codex global config";
|
|
535
636
|
case "gemini":
|
|
536
637
|
return "Gemini user config";
|
|
638
|
+
case "grok":
|
|
639
|
+
return "Grok config";
|
|
537
640
|
case "opencode":
|
|
538
641
|
return "OpenCode config";
|
|
539
642
|
default:
|
package/dist/instructions.js
CHANGED
|
@@ -30,6 +30,10 @@ Lean into adversarial review, convergence, precise implementation, edge-case swe
|
|
|
30
30
|
|
|
31
31
|
Use broad context review and exploration conservatively until the project has stronger Gemini-specific dogfood. Keep handoffs concrete and do not assume responsibility that the operator assigned to another harness.
|
|
32
32
|
|
|
33
|
+
## Grok
|
|
34
|
+
|
|
35
|
+
Use Grok Build as a first-class local coding harness. Keep coordination safety ahead of speed, rely on the native Grok skill and session hook when installed, and keep handoffs concrete when another harness is better positioned to implement or review.
|
|
36
|
+
|
|
33
37
|
## OpenCode
|
|
34
38
|
|
|
35
39
|
Use terminal-native local exploration and implementation conservatively until the project has stronger OpenCode-specific dogfood. Keep coordination safety ahead of speed.
|
|
@@ -41,6 +45,8 @@ const HARNESS_ALIASES = {
|
|
|
41
45
|
"claude-code": "claude",
|
|
42
46
|
codex: "codex",
|
|
43
47
|
gemini: "gemini",
|
|
48
|
+
grok: "grok",
|
|
49
|
+
"grok-build": "grok",
|
|
44
50
|
opencode: "opencode"
|
|
45
51
|
};
|
|
46
52
|
export function resolveInstructionPaths(options = {}) {
|
|
@@ -108,7 +114,7 @@ export function resolveInstructionHarness(explicitHarness, identity) {
|
|
|
108
114
|
export function normalizeInstructionHarness(value) {
|
|
109
115
|
const normalized = HARNESS_ALIASES[normalizeKey(value)];
|
|
110
116
|
if (!normalized) {
|
|
111
|
-
throw new Error(`--harness must be one of claude, codex, gemini, opencode, all (got ${value}).`);
|
|
117
|
+
throw new Error(`--harness must be one of claude, codex, gemini, grok, opencode, all (got ${value}).`);
|
|
112
118
|
}
|
|
113
119
|
return normalized;
|
|
114
120
|
}
|
|
@@ -208,6 +214,8 @@ function parseHarnessHeader(line) {
|
|
|
208
214
|
return "codex";
|
|
209
215
|
if (key.startsWith("gemini"))
|
|
210
216
|
return "gemini";
|
|
217
|
+
if (key.startsWith("grok"))
|
|
218
|
+
return "grok";
|
|
211
219
|
if (key.startsWith("opencode"))
|
|
212
220
|
return "opencode";
|
|
213
221
|
return null;
|
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,6 +15,8 @@ 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
21
|
return path.join(homeDir, ".opencode", "skills", options.skillName ?? DEFAULT_SKILL_NAME);
|
|
20
22
|
default:
|
|
@@ -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
|
+
```
|
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
|
|