talking-stick 0.4.12 → 0.4.13

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 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, 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`.
5
+ **Version:** 0.4.12. Multi-process-safe (SQLite WAL), liveness-aware, no daemon. Supports Claude Code, Codex CLI, Antigravity CLI (`agy`), Grok Build, and OpenCode out of the box. Gemini CLI identity is retained for existing sessions, but Gemini skill installation is deprecated in favor of Antigravity and the shared agents skill directory. 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
 
@@ -84,6 +84,8 @@ tt self-update
84
84
  tt uninstall --all
85
85
  ```
86
86
 
87
+ Single-harness uninstalls for shared-reading harnesses leave `~/.agents/skills/talking-stick` in place because Codex, Antigravity, Grok, and OpenCode share that one skill location. Use `tt uninstall agents` or `tt uninstall --shared` to remove only the shared skill target.
88
+
87
89
  ## What it gives your agent
88
90
 
89
91
  Once installed, each agent harness has a skill that tells it to coordinate through the `tt` CLI:
@@ -163,17 +165,18 @@ For harnesses that only notice completed subprocesses, run `tt events --wait --a
163
165
 
164
166
  `tt install` installs or refreshes the bundled `talking-stick` skill. It does not add MCP servers. During install, uninstall, package update, and first run after an installed package version changes, `tt` removes stale MCP registrations written by older Talking Stick releases.
165
167
 
166
- - Claude Code: copied or linked into `~/.claude/skills/talking-stick`
167
- - Codex: copied or linked into `~/.codex/skills/talking-stick`
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`
170
- - OpenCode: copied or linked into the resolved OpenCode config directory, normally `~/.config/opencode/skills/talking-stick` and honoring `XDG_CONFIG_HOME`
168
+ - Claude Code: copied or linked into `~/.claude/skills/talking-stick` because Claude Code does not read `~/.agents/skills`
169
+ - Codex, Antigravity (`agy`), Grok Build, and OpenCode: copied or linked once into the shared `~/.agents/skills/talking-stick`
170
+ - Grok Build: also installs a trusted global session hook at `~/.grok/hooks/talking-stick-session.json`
171
+ - Gemini CLI: deprecated for skill installation; `tt install gemini` prints a deprecation notice and runs cleanup only
172
+
173
+ By default, `tt install` links the bundled skill so local updates are picked up immediately. Pass `--copy` if you want a standalone snapshot.
171
174
 
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.
175
+ For harnesses that previously had proprietary skill copies, `tt` prunes duplicate `talking-stick` entries conservatively: it removes only symlinks that resolve to the bundled Talking Stick skill, and preserves copied directories, foreign symlinks, or hand-authored entries. OpenCode cleanup checks both `~/.config/opencode/skills/talking-stick` (honoring `XDG_CONFIG_HOME`) and the older `~/.opencode/skills/talking-stick` location.
173
176
 
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`.
177
+ 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 and Antigravity have no Talking Stick MCP registration path; install is native skill plus hook/shared skill only. Every cleanup run appends JSONL audit entries to `${TALKING_STICK_DATA_DIR}/update-migrations.log`.
175
178
 
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.
179
+ Human CLI invocations also perform a silent best-effort sync for already-installed file-based skills in Claude Code and the shared `~/.agents/skills/talking-stick` target. If the installed skill is a copy, it is refreshed from the bundled skill; if it is a stale symlink, it is relinked. Missing skill installs are skipped. Gemini skill sync is deprecated; use Antigravity/shared install instead.
177
180
 
178
181
  ## Human CLI
179
182
 
@@ -190,7 +193,7 @@ tt state [path] # full room state
190
193
  tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent] # room event log; --wait/--follow long-polls
191
194
  tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR] # send an OOB message
192
195
  tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR] # receive OOB messages
193
- tt instructions show [path] [--harness claude|codex|gemini|grok|opencode|all] [--scope effective|bundled|user|project] # show collaboration prompt
196
+ tt instructions show [path] [--harness claude|codex|antigravity|gemini|grok|opencode|all] [--scope effective|bundled|user|project] # show collaboration prompt
194
197
  tt instructions edit [path] [--user|--project] # edit user or project prompt
195
198
  tt instructions reset [path] (--user|--project) # delete a user or project prompt
196
199
  tt release [path] --status TEXT --next-action TEXT # normal handoff
@@ -201,7 +204,7 @@ tt takeover [path] [--reason TEXT] # alias for take
201
204
  tt notes add <body> [--turn N] [--path DIR] [--stdin] # leave an async note
202
205
  tt notes list [--all] [--after ID] [--limit N] [--path DIR] # read notes
203
206
  tt install <harness...> | --all [--print] [--copy] [--link] # install skill and clean stale MCP entries
204
- tt uninstall <harness...> | --all [--print] # remove skill and stale MCP entries
207
+ tt uninstall <harness...|agents> | --all | --shared [--print] # remove skill and stale MCP entries
205
208
  tt self-update [--print] [--manager npm|pnpm|yarn|bun] # update to the latest published tt
206
209
  ```
207
210
 
@@ -217,7 +220,7 @@ By default, `tt` behaves like a human CLI and resolves to `human:<username>` onl
217
220
 
218
221
  Harness-aware CLI identity is resolved before the human fallback:
219
222
 
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.
223
+ - Known harness environment markers such as `CLAUDECODE=1`, `CODEX_THREAD_ID`, `ANTIGRAVITY_AGENT=1`, `ANTIGRAVITY_CONVERSATION_ID`, `ANTIGRAVITY_TRAJECTORY_ID`, `GEMINI_CLI=1`, `CMUX_AGENT_LAUNCH_KIND=grok`, or `OPENCODE=1` make `tt` derive a harness-style identity automatically. Antigravity uses `ANTIGRAVITY_CONVERSATION_ID` as the preferred session anchor, falling back to `ANTIGRAVITY_TRAJECTORY_ID` and then `agy` process ancestry. The cmux Grok marker is optional; Grok Build also works without cmux by walking process ancestry for a `grok` root process.
221
224
  - 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.
222
225
  - Set `TT_HARNESS_AGENT_ID=<agent-id>` if the harness wants to export the exact agent id directly.
223
226
  - Set `TT_HARNESS_EXPORT=1` only when you need ancestry-based harness detection without a known harness environment marker.
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
- import { SUPPORTED_HARNESSES, detectHarness, parseHarnessList, planGrokSessionHookInstall, planGrokSessionHookUninstall, planUninstall, runAction } from "../install.js";
3
- import { planSkillInstall, planSkillUninstall } from "../skill-install.js";
2
+ import { SUPPORTED_HARNESSES, detectHarness, isDeprecatedHarness, parseHarnessList, planGrokSessionHookInstall, planGrokSessionHookUninstall, planUninstall, runAction } from "../install.js";
3
+ import { removeDuplicateSkillInstalls, planSkillInstall, planSkillUninstall, planSharedSkillUninstall, resolveDuplicateSkillTargetPaths } from "../skill-install.js";
4
4
  import { resolveDataDir } from "../config.js";
5
5
  import { FileAuditLog, defaultAuditLogPath } from "../install-audit.js";
6
6
  import { removeStaleMcpRegistrations } from "../install-migration.js";
@@ -15,32 +15,44 @@ export async function runInstallCommand(parsed) {
15
15
  skipMissing: true
16
16
  };
17
17
  if (dryRun) {
18
- for (const action of planInstallActions(harnesses, installOptions)) {
18
+ printDeprecatedHarnessNotices(harnesses);
19
+ for (const action of dedupeInstallActions(planInstallActions(harnesses, installOptions))) {
19
20
  printActionPlan(action);
20
21
  }
21
22
  for (const action of planCleanupActions(harnesses, installOptions)) {
22
23
  printActionPlan(action);
23
24
  }
25
+ printDuplicateSkillCleanupPlan(harnesses, installOptions);
24
26
  return;
25
27
  }
26
- const results = (await Promise.all(harnesses.map((harness) => runSkillInstall(harness, installOptions)))).flat();
28
+ printDeprecatedHarnessNotices(harnesses);
29
+ const results = await runSkillInstallActions(dedupeInstallActions(planInstallActions(harnesses, installOptions)), installOptions);
27
30
  reportInstallResults(results, "install");
28
31
  reportCleanupResults(await runCleanup(harnesses, "manual", installOptions), "install");
29
32
  printInstructionHint(results);
30
33
  }
31
34
  export async function runUninstallCommand(parsed) {
32
- const harnesses = selectHarnesses(parsed);
35
+ const { harnesses, removeShared } = selectUninstallTargets(parsed);
33
36
  const dryRun = hasOption(parsed, "print");
34
37
  const installOptions = { skipMissing: true };
35
- const actions = planUninstallActions(harnesses, installOptions);
38
+ const actions = [
39
+ ...planUninstallActions(harnesses, installOptions),
40
+ ...(removeShared ? [planSharedSkillUninstall(installOptions)] : [])
41
+ ];
36
42
  if (dryRun) {
43
+ printDeprecatedHarnessNotices(harnesses);
37
44
  for (const action of actions) {
38
45
  printActionPlan(action);
39
46
  }
47
+ printSharedSkillLeftHint(harnesses, removeShared);
40
48
  return;
41
49
  }
42
50
  const results = (await Promise.all(harnesses.map((harness) => runSkillUninstall(harness, installOptions)))).flat();
51
+ if (removeShared) {
52
+ results.push(await runAction(planSharedSkillUninstall(installOptions), installOptions));
53
+ }
43
54
  reportInstallResults(results, "uninstall");
55
+ printSharedSkillLeftHint(harnesses, removeShared);
44
56
  reportCleanupResults(await runCleanup(harnesses, "uninstall", installOptions), "uninstall");
45
57
  }
46
58
  export async function runInstallSkillCommand(parsed) {
@@ -48,21 +60,25 @@ export async function runInstallSkillCommand(parsed) {
48
60
  const dryRun = hasOption(parsed, "print");
49
61
  const link = resolveSkillInstallLinkMode(parsed);
50
62
  const installOptions = { link, skipMissing: true };
51
- const actions = harnesses.map((harness) => planSkillInstall(harness, installOptions));
63
+ const actions = dedupeInstallActions(harnesses.map((harness) => planSkillInstall(harness, installOptions)));
52
64
  if (dryRun) {
53
65
  for (const action of actions) {
54
66
  printActionPlan(action);
55
67
  }
56
68
  return;
57
69
  }
70
+ printDeprecatedHarnessNotices(harnesses);
58
71
  const results = await Promise.all(actions.map((action) => runAction(action, installOptions)));
59
72
  reportInstallResults(results, "install");
60
73
  }
61
74
  export async function runUninstallSkillCommand(parsed) {
62
- const harnesses = selectHarnesses(parsed);
75
+ const { harnesses, removeShared } = selectUninstallTargets(parsed);
63
76
  const dryRun = hasOption(parsed, "print");
64
77
  const installOptions = { skipMissing: true };
65
- const actions = harnesses.map((harness) => planSkillUninstall(harness, installOptions));
78
+ const actions = [
79
+ ...harnesses.map((harness) => planSkillUninstall(harness, installOptions)),
80
+ ...(removeShared ? [planSharedSkillUninstall(installOptions)] : [])
81
+ ];
66
82
  if (dryRun) {
67
83
  for (const action of actions) {
68
84
  printActionPlan(action);
@@ -71,6 +87,7 @@ export async function runUninstallSkillCommand(parsed) {
71
87
  }
72
88
  const results = await Promise.all(actions.map((action) => runAction(action, installOptions)));
73
89
  reportInstallResults(results, "uninstall");
90
+ printSharedSkillLeftHint(harnesses, removeShared);
74
91
  }
75
92
  export async function runSelfUpdateCommand(parsed, cliEntryUrl) {
76
93
  const dryRun = hasOption(parsed, "print");
@@ -137,6 +154,27 @@ function resolveSkillInstallLinkMode(parsed) {
137
154
  function planInstallActions(harnesses, installOptions) {
138
155
  return harnesses.flatMap((harness) => planInstallActionsForHarness(harness, installOptions));
139
156
  }
157
+ function dedupeInstallActions(actions) {
158
+ const seen = new Set();
159
+ const result = [];
160
+ for (const action of actions) {
161
+ const key = installActionDedupeKey(action);
162
+ if (seen.has(key))
163
+ continue;
164
+ seen.add(key);
165
+ result.push(action);
166
+ }
167
+ return result;
168
+ }
169
+ function installActionDedupeKey(action) {
170
+ if (action.kind === "file-patch") {
171
+ return `${action.kind}:${action.operation ?? "op"}:${action.filePath}`;
172
+ }
173
+ if (action.kind === "exec") {
174
+ return `${action.kind}:${action.operation ?? "op"}:${action.command}:${action.args.join("\0")}`;
175
+ }
176
+ return `${action.kind}:${action.harness}:${action.message}`;
177
+ }
140
178
  function planUninstallActions(harnesses, installOptions) {
141
179
  return harnesses.flatMap((harness) => [
142
180
  planSkillUninstall(harness, {
@@ -157,8 +195,7 @@ function planUninstallActions(harnesses, installOptions) {
157
195
  function planCleanupActions(harnesses, installOptions) {
158
196
  return harnesses.map((harness) => planUninstall(harness, installOptions));
159
197
  }
160
- async function runSkillInstall(harness, installOptions) {
161
- const actions = planInstallActionsForHarness(harness, installOptions);
198
+ async function runSkillInstallActions(actions, installOptions) {
162
199
  return Promise.all(actions.map((action) => runAction(action, installOptions)));
163
200
  }
164
201
  async function runSkillUninstall(harness, installOptions) {
@@ -186,16 +223,24 @@ function planInstallActionsForHarness(harness, installOptions) {
186
223
  }
187
224
  async function runCleanup(harnesses, reason, installOptions) {
188
225
  const dataDir = resolveDataDir();
189
- return removeStaleMcpRegistrations({
226
+ const audit = new FileAuditLog(defaultAuditLogPath(dataDir));
227
+ const mcpResults = await removeStaleMcpRegistrations({
190
228
  harnesses,
191
229
  reason,
192
- audit: new FileAuditLog(defaultAuditLogPath(dataDir)),
230
+ audit,
193
231
  installOptions
194
232
  });
233
+ const skillResults = removeDuplicateSkillInstalls({
234
+ harnesses,
235
+ reason,
236
+ audit,
237
+ ...installOptions
238
+ });
239
+ return [...mcpResults, ...skillResults];
195
240
  }
196
241
  function selectHarnesses(parsed) {
197
242
  if (hasOption(parsed, "all")) {
198
- const detected = SUPPORTED_HARNESSES.filter((harness) => detectHarness(harness).detected);
243
+ const detected = SUPPORTED_HARNESSES.filter((harness) => !isDeprecatedHarness(harness) && detectHarness(harness).detected);
199
244
  return [...detected];
200
245
  }
201
246
  if (parsed.positionals.length === 0) {
@@ -203,6 +248,38 @@ function selectHarnesses(parsed) {
203
248
  }
204
249
  return parseHarnessList(parsed.positionals);
205
250
  }
251
+ function selectUninstallTargets(parsed) {
252
+ if (hasOption(parsed, "all")) {
253
+ return { harnesses: [...SUPPORTED_HARNESSES], removeShared: true };
254
+ }
255
+ const removeShared = hasOption(parsed, "shared") || parsed.positionals.includes("agents");
256
+ const harnessTokens = parsed.positionals.filter((value) => value !== "agents");
257
+ if (harnessTokens.length === 0) {
258
+ if (removeShared)
259
+ return { harnesses: [], removeShared: true };
260
+ throw new Error(`Specify at least one harness (${SUPPORTED_HARNESSES.join(", ")}, agents) or pass --all to target every installed one.`);
261
+ }
262
+ return {
263
+ harnesses: parseHarnessList(harnessTokens),
264
+ removeShared
265
+ };
266
+ }
267
+ function printDeprecatedHarnessNotices(harnesses) {
268
+ if (!harnesses.includes("gemini"))
269
+ return;
270
+ process.stdout.write("[gemini] deprecated: Gemini CLI skill install is deprecated; use `tt install antigravity`.\n");
271
+ }
272
+ function printSharedSkillLeftHint(harnesses, removeShared) {
273
+ if (removeShared)
274
+ return;
275
+ if (!harnesses.some((harness) => harness === "codex" ||
276
+ harness === "grok" ||
277
+ harness === "opencode" ||
278
+ harness === "antigravity")) {
279
+ return;
280
+ }
281
+ process.stdout.write("Left ~/.agents/skills/talking-stick (shared with other agents). Run `tt uninstall --all` or `tt uninstall agents` to remove the shared skill.\n");
282
+ }
206
283
  function printActionPlan(action) {
207
284
  if (action.kind === "skip") {
208
285
  return;
@@ -213,6 +290,13 @@ function printActionPlan(action) {
213
290
  }
214
291
  process.stdout.write(`[${action.harness}] ${action.description}\n`);
215
292
  }
293
+ function printDuplicateSkillCleanupPlan(harnesses, installOptions) {
294
+ for (const harness of harnesses) {
295
+ for (const targetPath of resolveDuplicateSkillTargetPaths(harness, installOptions)) {
296
+ process.stdout.write(`[${harness}] remove duplicate skill symlink ${targetPath} if it points at bundled skill\n`);
297
+ }
298
+ }
299
+ }
216
300
  function runInheritIo(command, args) {
217
301
  return new Promise((resolve, reject) => {
218
302
  const child = spawn(command, args, { stdio: "inherit", shell: false });
@@ -251,12 +335,13 @@ export function reportCleanupResults(results, mode) {
251
335
  if (result.action === "absent" || result.action === "skipped") {
252
336
  continue;
253
337
  }
254
- process.stdout.write(`[${result.harness}] mcp-cleanup ${result.action}: ${result.message}\n`);
338
+ const label = result.target_type === "skill" ? "skill-cleanup" : "mcp-cleanup";
339
+ process.stdout.write(`[${result.harness}] ${label} ${result.action}: ${result.message}\n`);
255
340
  if (result.action === "failed")
256
341
  anyFailed = true;
257
342
  }
258
343
  if (anyFailed) {
259
- throw new Error(`${mode} completed with MCP cleanup failures.`);
344
+ throw new Error(`${mode} completed with cleanup failures.`);
260
345
  }
261
346
  }
262
347
  function formatInstallStatus(status) {
@@ -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|grok|opencode|all] [--scope effective|bundled|user|project]
167
+ tt instructions show [path] [--harness claude|codex|antigravity|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)
@@ -175,7 +175,7 @@ Commands:
175
175
  tt notes add <body> [--turn N] [--path DIR] [--stdin]
176
176
  tt notes list [--all] [--after NOTE_ID] [--limit N] [--path DIR]
177
177
  tt install <harness...> | --all [--print] [--copy] [--link]
178
- tt uninstall <harness...> | --all [--print]
178
+ tt uninstall <harness...|agents> | --all | --shared [--print]
179
179
  tt self-update [--print] [--manager npm|pnpm|yarn|bun]
180
180
 
181
181
  Harnesses: ${SUPPORTED_HARNESSES.join(", ")}
@@ -16,6 +16,7 @@ const BOOLEAN_FLAGS = new Set([
16
16
  "project",
17
17
  "quiet",
18
18
  "room",
19
+ "shared",
19
20
  "stdin",
20
21
  "text",
21
22
  "user",
@@ -39,7 +39,7 @@ export const COMMAND_REGISTRY = [
39
39
  needsRuntime: false,
40
40
  startupMaintenance: false,
41
41
  internal: false,
42
- usage: "tt uninstall <harness...> | --all [--print]",
42
+ usage: "tt uninstall <harness...|agents> | --all | --shared [--print]",
43
43
  description: "Remove the Talking Stick skill and stale MCP registrations.",
44
44
  handler: ({ parsed }) => runUninstallCommand(parsed)
45
45
  },
@@ -0,0 +1,44 @@
1
+ export const HARNESS_SKILL_MODELS = {
2
+ "claude-code": {
3
+ identity: "claude",
4
+ commandLabels: ["claude", "claude-code"],
5
+ skillLoadingModel: "proprietary"
6
+ },
7
+ codex: {
8
+ identity: "codex",
9
+ commandLabels: ["codex"],
10
+ skillLoadingModel: "shared+proprietary"
11
+ },
12
+ antigravity: {
13
+ identity: "antigravity",
14
+ commandLabels: ["agy", "antigravity"],
15
+ skillLoadingModel: "shared"
16
+ },
17
+ grok: {
18
+ identity: "grok",
19
+ commandLabels: ["grok"],
20
+ skillLoadingModel: "shared+proprietary"
21
+ },
22
+ opencode: {
23
+ identity: "opencode",
24
+ commandLabels: ["opencode"],
25
+ skillLoadingModel: "shared+proprietary"
26
+ },
27
+ gemini: {
28
+ identity: "gemini",
29
+ commandLabels: ["gemini"],
30
+ skillLoadingModel: "deprecated",
31
+ deprecated: true
32
+ }
33
+ };
34
+ export const SUPPORTED_HARNESSES = Object.keys(HARNESS_SKILL_MODELS);
35
+ export const FILE_SKILL_HARNESSES = SUPPORTED_HARNESSES.filter((harness) => HARNESS_SKILL_MODELS[harness].skillLoadingModel !== "deprecated");
36
+ export const HARNESS_CLI_HARNESSES = Array.from(new Set(SUPPORTED_HARNESSES.map((harness) => HARNESS_SKILL_MODELS[harness].identity)));
37
+ export const HARNESS_COMMAND_MAPPING = Object.fromEntries(SUPPORTED_HARNESSES.flatMap((harness) => HARNESS_SKILL_MODELS[harness].commandLabels.map((label) => [
38
+ label,
39
+ HARNESS_SKILL_MODELS[harness].identity
40
+ ])));
41
+ export function isDeprecatedHarness(harness) {
42
+ const model = HARNESS_SKILL_MODELS[harness];
43
+ return model.deprecated === true;
44
+ }
package/dist/identity.js CHANGED
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { findGrokSessionRecord, resolveGrokSessionLogPath } from "./grok-session-store.js";
5
+ import { HARNESS_COMMAND_MAPPING } from "./harness-model.js";
5
6
  import { resolveContextPath } from "./path-resolution.js";
6
7
  import { createSystemProcessInspector } from "./process-utils.js";
7
8
  const HARNESS_CLI_EXPORT_ENV = "TT_HARNESS_EXPORT";
@@ -233,14 +234,6 @@ function deriveHarnessDisplayName(agentId) {
233
234
  const prefix = agentId.split(":")[0]?.trim();
234
235
  return prefix && prefix.length > 0 ? prefix : "harness";
235
236
  }
236
- const HARNESS_COMMAND_MAPPING = {
237
- claude: "claude",
238
- "claude-code": "claude",
239
- codex: "codex",
240
- gemini: "gemini",
241
- grok: "grok",
242
- opencode: "opencode"
243
- };
244
237
  function detectHarnessViaAncestry(pid, inspector, maxDepth = 10) {
245
238
  let currentPid = pid;
246
239
  for (let i = 0; i < maxDepth; i++) {
@@ -279,6 +272,16 @@ function detectHarnessSignal(env) {
279
272
  pidHint: null
280
273
  };
281
274
  }
275
+ if (env.ANTIGRAVITY_AGENT === "1" ||
276
+ nonEmpty(env.ANTIGRAVITY_CONVERSATION_ID) ||
277
+ nonEmpty(env.ANTIGRAVITY_TRAJECTORY_ID)) {
278
+ return {
279
+ harness: "antigravity",
280
+ sessionId: nonEmpty(env.ANTIGRAVITY_CONVERSATION_ID) ??
281
+ nonEmpty(env.ANTIGRAVITY_TRAJECTORY_ID),
282
+ pidHint: null
283
+ };
284
+ }
282
285
  if (env.GEMINI_CLI === "1") {
283
286
  return { harness: "gemini", sessionId: null, pidHint: null };
284
287
  }
package/dist/index.js CHANGED
@@ -4,10 +4,11 @@ export { applyPragmas, assertLocalFilesystem, detectFilesystemType, migrate, ope
4
4
  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
- export { DEFAULT_MAX_INSTRUCTION_FILE_BYTES, DEFAULT_INSTRUCTIONS_MARKDOWN, editInstructions, extractHarnessInstructions, normalizeInstructionHarness, parseInstructionScope, resetInstructions, resolveInstructionHarness, resolveInstructionPaths, showInstructions } from "./instructions.js";
7
+ export { DEFAULT_MAX_INSTRUCTION_FILE_BYTES, DEFAULT_INSTRUCTIONS_MARKDOWN, HARNESS_ALIASES, INSTRUCTION_HARNESSES, editInstructions, extractHarnessInstructions, normalizeInstructionHarness, parseInstructionScope, resetInstructions, resolveInstructionHarness, resolveInstructionPaths, showInstructions } from "./instructions.js";
8
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
9
  export { DEFAULT_GROK_SESSION_RECORD_MAX_AGE_MS, appendGrokSessionRecord, findGrokSessionRecord, isGrokSessionEndEvent, readGrokSessionRecords, resolveGrokSessionLogPath } from "./grok-session-store.js";
10
- export { DEFAULT_SKILL_NAME, planSkillInstall, planSkillUninstall, resolveBundledSkillPath, resolveSkillTargetPath, syncInstalledSkills } from "./skill-install.js";
10
+ export { FILE_SKILL_HARNESSES, DEFAULT_SKILL_NAME, removeDuplicateSkillInstalls, planSkillInstall, planSkillUninstall, planSharedSkillUninstall, resolveBundledSkillPath, resolveDuplicateSkillTargetPaths, resolveLegacyOpencodeSkillTargetPath, resolvePrimarySkillTargetPath, resolveSharedAgentsSkillsDir, resolveSharedSkillTargetPath, resolveSkillTargetPath, skillLoadingModel, syncInstalledSkills } from "./skill-install.js";
11
+ export { HARNESS_CLI_HARNESSES, HARNESS_COMMAND_MAPPING, HARNESS_SKILL_MODELS, isDeprecatedHarness } from "./harness-model.js";
11
12
  export { readPackageVersion, readUpdateMigrationState, resolveUpdateMigrationStatePath, runFirstRunMcpMigration, runStaleMcpCleanup, writeUpdateMigrationState } from "./update-migration.js";
12
13
  export { createSystemProcessInspector, terminateKnownProcess } from "./process-utils.js";
13
14
  export { clearCliSessionLease, findCliSessionByRoom, findCliSessionForContextPath, readCliSessions, removeCliSession, removeCliSessionsForRoom, resolveCliSessionPath, upsertCliSession, upsertJoinedCliSession, writeCliSessions } from "./session-store.js";
@@ -20,12 +20,18 @@ export async function removeStaleMcpRegistrations(options) {
20
20
  package_version_to: options.packageVersionTo,
21
21
  harness,
22
22
  config_path: result.configPath,
23
+ target_type: "mcp",
23
24
  action: result.action,
24
25
  server_name: result.serverName,
25
26
  detail: result.message
26
27
  });
27
28
  }
28
- return results.map(({ harness, action, message }) => ({ harness, action, message }));
29
+ return results.map(({ harness, action, message }) => ({
30
+ harness,
31
+ action,
32
+ message,
33
+ target_type: "mcp"
34
+ }));
29
35
  }
30
36
  async function removeOneHarness(harness, installOptions, strict) {
31
37
  const action = planUninstall(harness, installOptions);
package/dist/install.js CHANGED
@@ -3,7 +3,8 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { writeFileAtomic } from "./atomic-write.js";
6
- export const SUPPORTED_HARNESSES = ["claude-code", "codex", "gemini", "grok", "opencode"];
6
+ import { SUPPORTED_HARNESSES } from "./harness-model.js";
7
+ export { SUPPORTED_HARNESSES, isDeprecatedHarness } from "./harness-model.js";
7
8
  export const DEFAULT_SERVER_NAME = "talking-stick";
8
9
  // Legacy MCP command retained only to identify stale config entries for removal.
9
10
  export const DEFAULT_SERVER_COMMAND = ["tt", "mcp"];
@@ -127,6 +128,8 @@ function resolveHarnessConfigDirFromResolved(harness, resolved) {
127
128
  return path.join(resolved.homeDir, ".claude");
128
129
  case "codex":
129
130
  return path.join(resolved.homeDir, ".codex");
131
+ case "antigravity":
132
+ return path.join(resolved.homeDir, ".agents");
130
133
  case "gemini":
131
134
  return path.join(resolved.homeDir, ".gemini");
132
135
  case "grok":
@@ -166,6 +169,8 @@ export function planUninstall(harness, options = {}) {
166
169
  operation: "uninstall",
167
170
  serverName: resolved.serverName
168
171
  };
172
+ case "antigravity":
173
+ return skipAction(harness, "legacy Talking Stick cleanup is not applicable for antigravity");
169
174
  case "gemini":
170
175
  if (resolved.skipMissing && !resolved.hooks.which("gemini")) {
171
176
  return skipAction(harness, "gemini not on PATH");
@@ -382,6 +387,12 @@ export function detectHarness(harness, options = {}) {
382
387
  return { harness, detected: true, evidence: configDir };
383
388
  return { harness, detected: false, evidence: "codex not on PATH and no config directory" };
384
389
  }
390
+ case "antigravity": {
391
+ const bin = resolved.hooks.which("agy");
392
+ if (bin)
393
+ return { harness, detected: true, evidence: bin };
394
+ return { harness, detected: false, evidence: "agy not on PATH" };
395
+ }
385
396
  case "gemini": {
386
397
  const bin = resolved.hooks.which("gemini");
387
398
  if (bin)
@@ -648,6 +659,8 @@ function mcpConfigLocation(action) {
648
659
  return "Claude Code user config";
649
660
  case "codex":
650
661
  return "Codex global config";
662
+ case "antigravity":
663
+ return "Antigravity shared config";
651
664
  case "gemini":
652
665
  return "Gemini user config";
653
666
  case "grok":
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { resolveDataDir } from "./config.js";
5
+ import { HARNESS_CLI_HARNESSES } from "./harness-model.js";
5
6
  import { resolveContextPath } from "./path-resolution.js";
6
7
  export const DEFAULT_MAX_INSTRUCTION_FILE_BYTES = 256 * 1024;
7
8
  export const DEFAULT_INSTRUCTIONS_MARKDOWN = `# Talking Stick collaboration instructions
@@ -12,7 +13,7 @@ On freshly invoked multi-agent tasks, give peers a short window to join before d
12
13
 
13
14
  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.
14
15
 
15
- Claude and Codex are peers of comparable capability; neither outranks the other. Split work evenly between them rather than routing by stereotype, and have all models plan, implement, and evaluate together: any harness can draft, review, converge, implement, or release. Gemini and OpenCode start with conservative local guidance until project dogfood says otherwise.
16
+ Claude and Codex are peers of comparable capability; neither outranks the other. Split work evenly between them rather than routing by stereotype, and have all models plan, implement, and evaluate together: any harness can draft, review, converge, implement, or release. Antigravity and OpenCode start with conservative local guidance until project dogfood says otherwise.
16
17
 
17
18
  For multi-agent design work, prefer independent read-only drafts first, then adversarial review and convergence. Do not impose a draft file structure on the workspace by default. If scratch draft files are useful, delete superseded pre-convergence drafts after the converged plan exists unless the operator asks to keep them.
18
19
 
@@ -26,9 +27,9 @@ Take a full, even share of planning, implementation, and evaluation. Watch for s
26
27
 
27
28
  Take a full, even share of planning, implementation, and evaluation. Watch for over-indexing on mechanics when the operator still needs to decide direction. Make the next phase explicit in the handoff.
28
29
 
29
- ## Gemini
30
+ ## Antigravity
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
+ Use broad context review and exploration conservatively until the project has stronger Antigravity-specific dogfood. Keep handoffs concrete and do not assume responsibility that the operator assigned to another harness.
32
33
 
33
34
  ## Grok
34
35
 
@@ -38,11 +39,17 @@ Use Grok Build as a first-class local coding harness. Keep coordination safety a
38
39
 
39
40
  Use terminal-native local exploration and implementation conservatively until the project has stronger OpenCode-specific dogfood. Keep coordination safety ahead of speed.
40
41
  `;
41
- const HARNESS_ALIASES = {
42
+ export const INSTRUCTION_HARNESSES = [
43
+ ...HARNESS_CLI_HARNESSES,
44
+ "all"
45
+ ];
46
+ export const HARNESS_ALIASES = {
42
47
  all: "all",
43
48
  base: "all",
44
49
  claude: "claude",
45
50
  "claude-code": "claude",
51
+ antigravity: "antigravity",
52
+ agy: "antigravity",
46
53
  codex: "codex",
47
54
  gemini: "gemini",
48
55
  grok: "grok",
@@ -114,7 +121,7 @@ export function resolveInstructionHarness(explicitHarness, identity) {
114
121
  export function normalizeInstructionHarness(value) {
115
122
  const normalized = HARNESS_ALIASES[normalizeKey(value)];
116
123
  if (!normalized) {
117
- throw new Error(`--harness must be one of claude, codex, gemini, grok, opencode, all (got ${value}).`);
124
+ throw new Error(`--harness must be one of claude, codex, antigravity, gemini, grok, opencode, all (got ${value}).`);
118
125
  }
119
126
  return normalized;
120
127
  }
@@ -219,6 +226,8 @@ function parseHarnessHeader(line) {
219
226
  return "claude";
220
227
  if (key.startsWith("codex"))
221
228
  return "codex";
229
+ if (key.startsWith("antigravity") || key === "agy")
230
+ return "antigravity";
222
231
  if (key.startsWith("gemini"))
223
232
  return "gemini";
224
233
  if (key.startsWith("grok"))