talking-stick 0.3.0 → 0.4.0

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.3.0. 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.0. Multi-process-safe (SQLite WAL), liveness-aware, no daemon. Supports Claude Code, Codex CLI, Gemini CLI, and OpenCode out of the box. Two agents in the same room can also chat out-of-band — without passing the stick — via `tt msg send/recv`.
6
6
 
7
7
  ## Quickstart
8
8
 
@@ -20,7 +20,7 @@ npm i -g talking-stick
20
20
  tt install --all
21
21
  ```
22
22
 
23
- Restart any harness that was already running so it loads the updated skill. The skill teaches agents to coordinate by running `tt` CLI commands from the workspace.
23
+ Restart any harness that was already running so it loads the updated skill. The skill teaches agents to coordinate by running `tt` CLI commands from the workspace. To tune the default collaboration prompt without editing installed package files, run `tt instructions edit`.
24
24
 
25
25
  ### 3. Try it: two agents, one repo
26
26
 
@@ -46,7 +46,7 @@ That's the whole workflow. They negotiate turns automatically, hand off structur
46
46
 
47
47
  | Method | Command | Notes |
48
48
  |---|---|---|
49
- | **From npm** | `npm i -g talking-stick` | Published as `0.3.0`. Requires Node ≥ 22. |
49
+ | **From npm** | `npm i -g talking-stick` | Published as `0.4.0`. Requires Node ≥ 22. |
50
50
  | **From GitHub** | `npm i -g github:mostlydev/talking-stick` | Tracks the `master` branch; builds on install via the `prepare` hook. |
51
51
  | **From source** | `git clone … && npm install && npm link` | For contributors. |
52
52
 
@@ -101,12 +101,27 @@ tt state — authoritative state projection
101
101
  tt events — audit log and long-poll stream of turn transitions/messages
102
102
  tt notes add/list — durable async observations for the room
103
103
  tt msg send/recv — out-of-band chat into the room event log
104
+ tt instructions — editable collaboration prompt loaded by the skill
104
105
  ```
105
106
 
106
107
  A workspace maps to a room — usually the `git` root or nearest project marker — so two agents `cd`'d anywhere under the same repo join the same room automatically.
107
108
 
108
109
  The global skill tells the model when to join, wait, verify its guardian, take over, leave notes, send messages, and hand off.
109
110
 
111
+ ## Editable collaboration instructions
112
+
113
+ The bundled skill is the safety floor. It is intentionally small and package-managed. Local collaboration preferences live in editable Markdown files that `tt instructions` shows to agents after they join.
114
+
115
+ ```bash
116
+ tt instructions show # effective prompt for the detected harness
117
+ tt instructions show --harness codex # view one harness's effective prompt
118
+ tt instructions edit # edit user defaults
119
+ tt instructions edit --project # edit this repo's overrides
120
+ tt instructions reset --project # remove this repo's override
121
+ ```
122
+
123
+ Effective instructions are layered in this order: bundled defaults, user defaults at `${TALKING_STICK_DATA_DIR}/instructions.md` (normally `~/.local/share/talking-stick/instructions.md`), then project overrides at `.talking-stick/instructions.md` in the workspace root. User and project files are created lazily on first edit, so installing `tt` does not litter repositories or harness config directories.
124
+
110
125
  ## Non-owner notes
111
126
 
112
127
  While you wait your turn you may still need to flag something to the current owner: a subtle invariant, a related bug, a pointer to a doc. Non-owner notes give you a durable channel without interrupting the turn.
@@ -173,6 +188,9 @@ tt state [path] # full room state
173
188
  tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent] # room event log; --wait/--follow long-polls
174
189
  tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR] # send an OOB message
175
190
  tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR] # receive OOB messages
191
+ tt instructions show [path] [--harness claude|codex|gemini|opencode|all] [--scope effective|bundled|user|project] # show collaboration prompt
192
+ tt instructions edit [path] [--user|--project] # edit user or project prompt
193
+ tt instructions reset [path] (--user|--project) # delete a user or project prompt
176
194
  tt release [path] --status TEXT --next-action TEXT # normal handoff
177
195
  tt pass [path] --status TEXT --next-action TEXT # pass/end your turn
178
196
  tt assign <target|next> [path] --status TEXT --next-action TEXT # explicit handoff
@@ -29,6 +29,7 @@ export async function runInstallCommand(parsed) {
29
29
  const results = (await Promise.all(harnesses.map((harness) => runSkillInstall(harness, installOptions)))).flat();
30
30
  reportInstallResults(results, "install");
31
31
  reportCleanupResults(await runCleanup(harnesses, "manual", installOptions), "install");
32
+ printInstructionHint(results);
32
33
  }
33
34
  export async function runUninstallCommand(parsed) {
34
35
  normalizeBooleanFlag(parsed, "print");
@@ -222,6 +223,12 @@ function reportInstallResults(results, mode) {
222
223
  throw new Error(`${mode} completed with failures.`);
223
224
  }
224
225
  }
226
+ function printInstructionHint(results) {
227
+ if (!results.some((result) => result.ok && !result.skipped)) {
228
+ return;
229
+ }
230
+ process.stdout.write("Customize collaboration instructions with: tt instructions edit\n");
231
+ }
225
232
  function reportCleanupResults(results, mode) {
226
233
  let anyFailed = false;
227
234
  for (const result of results) {
@@ -0,0 +1,113 @@
1
+ import { editInstructions, parseInstructionScope, resetInstructions, showInstructions } from "../instructions.js";
2
+ import { deriveCliIdentity } from "./identity.js";
3
+ import { getStringOption, hasOption } from "./parser.js";
4
+ import { printResult } from "./output.js";
5
+ export async function handleInstructionsCommand(parsed) {
6
+ const [subcommand = "show", ...rest] = parsed.positionals;
7
+ const subParsed = {
8
+ name: `instructions ${subcommand}`,
9
+ positionals: rest,
10
+ options: parsed.options
11
+ };
12
+ switch (subcommand) {
13
+ case "show":
14
+ handleInstructionsShowCommand(subParsed);
15
+ return;
16
+ case "edit":
17
+ await handleInstructionsEditCommand(subParsed);
18
+ return;
19
+ case "reset":
20
+ handleInstructionsResetCommand(subParsed);
21
+ return;
22
+ default:
23
+ throw new Error(`Unknown instructions subcommand: ${subcommand}`);
24
+ }
25
+ }
26
+ function handleInstructionsShowCommand(parsed) {
27
+ repairBooleanFlag(parsed, "json", 0);
28
+ repairBooleanFlag(parsed, "text", 0);
29
+ const contextPath = resolveContextPathArg(parsed);
30
+ const scope = parseInstructionScope(getStringOption(parsed, "scope"));
31
+ const identity = deriveCliIdentity(parsed);
32
+ const result = showInstructions({
33
+ harness: getStringOption(parsed, "harness"),
34
+ scope,
35
+ options: {
36
+ contextPath,
37
+ identity
38
+ }
39
+ });
40
+ printResult(parsed, result, () => {
41
+ if (result.text.trim().length > 0) {
42
+ return result.text;
43
+ }
44
+ return `No ${result.scope} instructions found for ${result.harness}.`;
45
+ });
46
+ }
47
+ async function handleInstructionsEditCommand(parsed) {
48
+ repairBooleanFlag(parsed, "json", 0);
49
+ repairBooleanFlag(parsed, "text", 0);
50
+ repairBooleanFlag(parsed, "user", 0);
51
+ repairBooleanFlag(parsed, "project", 0);
52
+ const contextPath = resolveContextPathArg(parsed);
53
+ const scope = resolveEditableScope(parsed, false);
54
+ const result = await editInstructions({
55
+ scope,
56
+ options: { contextPath }
57
+ });
58
+ printResult(parsed, result, () => {
59
+ if (result.opened) {
60
+ return `Opened ${result.scope} instructions: ${result.path}`;
61
+ }
62
+ return [
63
+ `Instructions file ready: ${result.path}`,
64
+ "Set $VISUAL or $EDITOR to edit it from this command."
65
+ ].join("\n");
66
+ });
67
+ }
68
+ function handleInstructionsResetCommand(parsed) {
69
+ repairBooleanFlag(parsed, "json", 0);
70
+ repairBooleanFlag(parsed, "text", 0);
71
+ repairBooleanFlag(parsed, "user", 0);
72
+ repairBooleanFlag(parsed, "project", 0);
73
+ const contextPath = resolveContextPathArg(parsed);
74
+ const scope = resolveEditableScope(parsed, true);
75
+ const result = resetInstructions({
76
+ scope,
77
+ options: { contextPath }
78
+ });
79
+ printResult(parsed, result, () => {
80
+ if (result.removed) {
81
+ return `Removed ${result.scope} instructions: ${result.path}`;
82
+ }
83
+ return `No ${result.scope} instructions file at ${result.path}`;
84
+ });
85
+ }
86
+ function resolveEditableScope(parsed, requireExplicit) {
87
+ const wantsUser = hasOption(parsed, "user");
88
+ const wantsProject = hasOption(parsed, "project");
89
+ if (wantsUser && wantsProject) {
90
+ throw new Error("Pass only one of --user or --project.");
91
+ }
92
+ if (wantsProject) {
93
+ return "project";
94
+ }
95
+ if (wantsUser || !requireExplicit) {
96
+ return "user";
97
+ }
98
+ throw new Error("Pass --user or --project to choose which instructions to reset.");
99
+ }
100
+ function resolveContextPathArg(parsed) {
101
+ const pathOption = parsed.options.get("path");
102
+ if (pathOption === true) {
103
+ throw new Error("--path requires a value.");
104
+ }
105
+ return pathOption ?? parsed.positionals[0] ?? process.cwd();
106
+ }
107
+ function repairBooleanFlag(parsed, key, insertAt) {
108
+ const value = parsed.options.get(key);
109
+ if (typeof value === "string") {
110
+ parsed.positionals.splice(insertAt, 0, value);
111
+ parsed.options.set(key, true);
112
+ }
113
+ }
@@ -129,6 +129,9 @@ Commands:
129
129
  tt events [path] [--after N] [--limit N] [--wait|--follow] [--event TYPE[,TYPE]] [--target self|any|agent]
130
130
  tt msg send <recipient|room> <body...> [--interrupt] [--stdin] [--path DIR]
131
131
  tt msg recv [--wait|--follow] [--from agent] [--after N] [--target self|any|agent] [--path DIR]
132
+ tt instructions show [path] [--harness claude|codex|gemini|opencode|all] [--scope effective|bundled|user|project]
133
+ tt instructions edit [path] [--user|--project]
134
+ tt instructions reset [path] (--user|--project)
132
135
  tt release [path] (--status TEXT --next-action TEXT | --stdin)
133
136
  tt pass [path] (--status TEXT --next-action TEXT | --stdin)
134
137
  tt assign <target|next> [path] (--status TEXT --next-action TEXT | --stdin)
@@ -1,5 +1,6 @@
1
1
  import { runGuardCommand } from "./guardian.js";
2
2
  import { runInstallCommand, runMcpMigrationCommand, runSelfUpdateCommand, runUninstallCommand } from "./install-commands.js";
3
+ import { handleInstructionsCommand } from "./instructions-commands.js";
3
4
  import { handleMsgCommand } from "./msg-commands.js";
4
5
  import { handleNotesCommand } from "./notes-commands.js";
5
6
  import { handleEventsCommand, handleJoinCommand, handleKickCommand, handleLeaveCommand, handleListCommand, handleStateCommand, handleWhoAmICommand } from "./room-commands.js";
@@ -50,6 +51,15 @@ export const COMMAND_REGISTRY = [
50
51
  description: "Remove stale Talking Stick MCP registrations.",
51
52
  handler: ({ parsed }) => runMcpMigrationCommand(parsed)
52
53
  },
54
+ {
55
+ name: "instructions",
56
+ needsRuntime: false,
57
+ startupMaintenance: true,
58
+ internal: false,
59
+ usage: "tt instructions [show|edit|reset] [--harness NAME] [--scope SCOPE]",
60
+ description: "Show, edit, or reset editable collaboration instructions.",
61
+ handler: ({ parsed }) => handleInstructionsCommand(parsed)
62
+ },
53
63
  {
54
64
  name: "whoami",
55
65
  needsRuntime: false,
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ 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
8
  export { SUPPORTED_HARNESSES, MissingHarnessError, detectHarness, parseHarnessList, planUninstall, resolveHarnessConfigDir, resolveOpencodeConfigDir, resolveOpencodeConfigPath, runAction, skipAction } from "./install.js";
8
9
  export { DEFAULT_SKILL_NAME, planSkillInstall, planSkillUninstall, resolveBundledSkillPath, resolveSkillTargetPath, syncInstalledSkills } from "./skill-install.js";
9
10
  export { readPackageVersion, readUpdateMigrationState, resolveUpdateMigrationStatePath, runFirstRunMcpMigration, runStaleMcpCleanup, writeUpdateMigrationState } from "./update-migration.js";
@@ -0,0 +1,256 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { resolveDataDir } from "./config.js";
5
+ import { resolveContextPath } from "./path-resolution.js";
6
+ export const DEFAULT_MAX_INSTRUCTION_FILE_BYTES = 256 * 1024;
7
+ export const DEFAULT_INSTRUCTIONS_MARKDOWN = `# Talking Stick collaboration instructions
8
+
9
+ Keep using Talking Stick until the shared task is done. After releasing or handing off, re-enter the wait loop by default. Prefer continued action unless the task is complete or the operator explicitly redirects or stops the room.
10
+
11
+ Use phase names in handoffs when they clarify the work: draft, adversarial review, convergence, implementation, implementation review, test review, and release. These phases are vocabulary, not protocol state.
12
+
13
+ Typical fits are advisory. Claude is usually strong at prose, first-pass synthesis, tool-running, implementation review, and test review. Codex is usually strong at adversarial review, convergence, implementation, edge cases, and release mechanics after operator approval. Gemini and OpenCode start with conservative local guidance until project dogfood says otherwise.
14
+
15
+ 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.
16
+
17
+ Default to normal release handoffs. Use named assignment only when a specific member must go next because of unique context, credentials, capability, or direct operator routing.
18
+
19
+ ## Claude
20
+
21
+ Lean into drafting, synthesis, tool-running, implementation review, and test review. Watch for scope creep and messy first-pass artifacts. When implementation belongs elsewhere, make the next phase explicit in the handoff.
22
+
23
+ ## Codex
24
+
25
+ Lean into adversarial review, convergence, precise implementation, edge-case sweeps, and release mechanics after operator approval. Watch for over-indexing on mechanics when the operator still needs to decide direction.
26
+
27
+ ## Gemini
28
+
29
+ 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.
30
+
31
+ ## OpenCode
32
+
33
+ Use terminal-native local exploration and implementation conservatively until the project has stronger OpenCode-specific dogfood. Keep coordination safety ahead of speed.
34
+ `;
35
+ const HARNESS_ALIASES = {
36
+ all: "all",
37
+ base: "all",
38
+ claude: "claude",
39
+ "claude-code": "claude",
40
+ codex: "codex",
41
+ gemini: "gemini",
42
+ opencode: "opencode"
43
+ };
44
+ export function resolveInstructionPaths(options = {}) {
45
+ const contextPath = options.contextPath ?? process.cwd();
46
+ const workspaceRoot = resolveContextPath(contextPath).workspace_root;
47
+ return {
48
+ user: path.join(resolveDataDir(options), "instructions.md"),
49
+ project: path.join(workspaceRoot, ".talking-stick", "instructions.md")
50
+ };
51
+ }
52
+ export function showInstructions(input = {}) {
53
+ const options = input.options ?? {};
54
+ const harness = resolveInstructionHarness(input.harness, options.identity);
55
+ const scope = input.scope ?? "effective";
56
+ const paths = resolveInstructionPaths(options);
57
+ const layers = readInstructionLayers(paths, options.maxInstructionFileBytes ?? DEFAULT_MAX_INSTRUCTION_FILE_BYTES);
58
+ const selectedLayers = selectLayers(scope, layers);
59
+ const text = joinInstructionTexts(selectedLayers.map((layer) => extractHarnessInstructions(layer.text, harness)));
60
+ return {
61
+ harness,
62
+ scope,
63
+ text,
64
+ sources: selectedLayers.map((layer) => ({
65
+ scope: layer.scope,
66
+ path: layer.path
67
+ })),
68
+ paths
69
+ };
70
+ }
71
+ export async function editInstructions(input = {}) {
72
+ const scope = input.scope ?? "user";
73
+ const options = input.options ?? {};
74
+ const paths = resolveInstructionPaths(options);
75
+ const filePath = paths[scope];
76
+ const created = ensureInstructionFile(filePath);
77
+ const editor = chooseEditor(options);
78
+ if (!editor) {
79
+ return { scope, path: filePath, created, opened: false, editor: null };
80
+ }
81
+ await runEditor(editor, filePath);
82
+ return { scope, path: filePath, created, opened: true, editor };
83
+ }
84
+ export function resetInstructions(input) {
85
+ const paths = resolveInstructionPaths(input.options ?? {});
86
+ const filePath = paths[input.scope];
87
+ const removed = fs.existsSync(filePath);
88
+ if (removed) {
89
+ fs.rmSync(filePath, { force: true });
90
+ }
91
+ return { scope: input.scope, path: filePath, removed };
92
+ }
93
+ export function resolveInstructionHarness(explicitHarness, identity) {
94
+ if (explicitHarness) {
95
+ return normalizeInstructionHarness(explicitHarness);
96
+ }
97
+ const displayName = identity?.process_metadata.display_name ?? undefined;
98
+ const fromDisplay = displayName ? HARNESS_ALIASES[normalizeKey(displayName)] : undefined;
99
+ if (fromDisplay) {
100
+ return fromDisplay;
101
+ }
102
+ const prefix = identity?.agent_id.split(":")[0];
103
+ const fromPrefix = prefix ? HARNESS_ALIASES[normalizeKey(prefix)] : undefined;
104
+ return fromPrefix ?? "all";
105
+ }
106
+ export function normalizeInstructionHarness(value) {
107
+ const normalized = HARNESS_ALIASES[normalizeKey(value)];
108
+ if (!normalized) {
109
+ throw new Error(`--harness must be one of claude, codex, gemini, opencode, all (got ${value}).`);
110
+ }
111
+ return normalized;
112
+ }
113
+ export function parseInstructionScope(value) {
114
+ if (!value) {
115
+ return "effective";
116
+ }
117
+ if (value === "effective" ||
118
+ value === "bundled" ||
119
+ value === "user" ||
120
+ value === "project") {
121
+ return value;
122
+ }
123
+ throw new Error(`--scope must be one of effective, bundled, user, project (got ${value}).`);
124
+ }
125
+ export function extractHarnessInstructions(markdown, harness) {
126
+ const trimmed = markdown.trim();
127
+ if (!trimmed || harness === "all") {
128
+ return trimmed;
129
+ }
130
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
131
+ const shared = [];
132
+ const sections = new Map();
133
+ let current = null;
134
+ let sawSection = false;
135
+ for (const line of lines) {
136
+ const header = parseHarnessHeader(line);
137
+ if (header) {
138
+ sawSection = true;
139
+ current = header;
140
+ if (!sections.has(current)) {
141
+ sections.set(current, []);
142
+ }
143
+ sections.get(current)?.push(line);
144
+ continue;
145
+ }
146
+ if (!sawSection) {
147
+ shared.push(line);
148
+ continue;
149
+ }
150
+ if (current) {
151
+ sections.get(current)?.push(line);
152
+ }
153
+ }
154
+ return joinInstructionTexts([
155
+ shared.join("\n").trim(),
156
+ sections.get(harness)?.join("\n").trim() ?? ""
157
+ ]);
158
+ }
159
+ function readInstructionLayers(paths, maxInstructionFileBytes) {
160
+ const layers = [
161
+ { scope: "bundled", path: null, text: DEFAULT_INSTRUCTIONS_MARKDOWN }
162
+ ];
163
+ for (const scope of ["user", "project"]) {
164
+ const filePath = paths[scope];
165
+ if (!fs.existsSync(filePath)) {
166
+ continue;
167
+ }
168
+ const stat = fs.statSync(filePath);
169
+ if (stat.size > maxInstructionFileBytes) {
170
+ throw new Error(`${scope} instructions file is too large (${stat.size} bytes, max ${maxInstructionFileBytes}): ${filePath}`);
171
+ }
172
+ const text = fs.readFileSync(filePath, "utf8");
173
+ layers.push({ scope, path: filePath, text });
174
+ }
175
+ return layers;
176
+ }
177
+ function selectLayers(scope, layers) {
178
+ if (scope === "effective") {
179
+ return layers;
180
+ }
181
+ return layers.filter((layer) => layer.scope === scope);
182
+ }
183
+ function joinInstructionTexts(parts) {
184
+ return parts
185
+ .map((part) => part.trim())
186
+ .filter((part) => part.length > 0)
187
+ .join("\n\n");
188
+ }
189
+ function ensureInstructionFile(filePath) {
190
+ if (fs.existsSync(filePath)) {
191
+ return false;
192
+ }
193
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
194
+ fs.writeFileSync(filePath, DEFAULT_INSTRUCTIONS_MARKDOWN);
195
+ return true;
196
+ }
197
+ function parseHarnessHeader(line) {
198
+ const match = line.match(/^##\s+(.+?)\s*$/);
199
+ if (!match) {
200
+ return null;
201
+ }
202
+ const key = normalizeKey(match[1]);
203
+ if (key.startsWith("claude"))
204
+ return "claude";
205
+ if (key.startsWith("codex"))
206
+ return "codex";
207
+ if (key.startsWith("gemini"))
208
+ return "gemini";
209
+ if (key.startsWith("opencode"))
210
+ return "opencode";
211
+ return null;
212
+ }
213
+ function chooseEditor(options) {
214
+ const env = options.env ?? process.env;
215
+ const explicit = env.VISUAL?.trim() || env.EDITOR?.trim();
216
+ if (explicit) {
217
+ return explicit;
218
+ }
219
+ switch (options.platform ?? process.platform) {
220
+ case "darwin":
221
+ return "open -t";
222
+ case "win32":
223
+ return "notepad.exe";
224
+ default:
225
+ if (env.DISPLAY || env.WAYLAND_DISPLAY) {
226
+ return "xdg-open";
227
+ }
228
+ return null;
229
+ }
230
+ }
231
+ function runEditor(editor, filePath) {
232
+ return new Promise((resolve, reject) => {
233
+ const child = spawn(`${editor} ${shellQuote(filePath)}`, {
234
+ stdio: "inherit",
235
+ shell: true
236
+ });
237
+ child.on("error", reject);
238
+ child.on("close", (code) => {
239
+ if (code === 0) {
240
+ resolve();
241
+ return;
242
+ }
243
+ reject(new Error(`${editor} exited with code ${code}.`));
244
+ });
245
+ });
246
+ }
247
+ function shellQuote(value) {
248
+ return `'${value.replaceAll("'", "'\\''")}'`;
249
+ }
250
+ function normalizeKey(value) {
251
+ return value
252
+ .trim()
253
+ .toLowerCase()
254
+ .replace(/[^a-z0-9]+/g, "-")
255
+ .replace(/^-+|-+$/g, "");
256
+ }
@@ -0,0 +1,220 @@
1
+ # v6 converged design — harness instructions
2
+
3
+ > **Status:** converged claude + codex design.
4
+ >
5
+ > **Convergence path:** independent drafts written in parallel, then debated
6
+ > by cross-reference. Outcomes: claude conceded load-mechanism (separate
7
+ > `tt instructions show` call, not piggyback on `tt join`); codex conceded
8
+ > file-structure (single file per layer, not per-harness) and materialization
9
+ > (lazy on edit, never on install). Naming, CLI surface, project-file scope,
10
+ > and bundled-skill scope were already aligned.
11
+ >
12
+ > The pre-debate drafts (`v6-claude.md`, `v6-codex.md`) were deleted after
13
+ > convergence per Wojtek: "if they write something they need to delete the
14
+ > pre-debate versions after convergence."
15
+
16
+ ## Problem
17
+
18
+ v5 collapsed the phase guidance into bundled `SKILL.md`. Wojtek's pushback:
19
+ *"The skill ships with the binary, which means people won't be able to edit. I
20
+ want them to be able to edit, but I still want it to be simple."* `tt install`
21
+ links the bundled skill by default and startup sync can refresh copied
22
+ installs — there is no durable user-edit affordance.
23
+
24
+ v6 fixes this with editable Markdown instructions, seeded lazily, opened with
25
+ `$EDITOR`. Not JSON profiles. Not a config subsystem.
26
+
27
+ ## Shape
28
+
29
+ Two layers of concern, kept separate by design:
30
+
31
+ ### 1. Bundled skill floor (`skills/talking-stick/SKILL.md`)
32
+
33
+ Immutable, package-owned. Covers join, wait, guardian checks, release/handoff,
34
+ event-wake safety, takeover, notes, messages, and the "keep using the stick
35
+ until done" rule. Tells agents how to load the editable collaboration
36
+ instructions. **Does not include phase vocabulary or typical fits** — those
37
+ live in the editable layer with bundled defaults as fallback inside the
38
+ instructions code path, not duplicated into the safety contract.
39
+
40
+ ### 2. Editable collaboration instructions
41
+
42
+ Single Markdown file per layer, with optional per-harness sections inside:
43
+
44
+ ```
45
+ ~/.local/share/talking-stick/instructions.md # user override (optional, XDG-aware)
46
+ <repo>/.talking-stick/instructions.md # project override (optional)
47
+ ```
48
+
49
+ Bundled defaults live as text constants in `src/instructions.ts` — they are
50
+ the seed text and the always-available fallback if no editable file exists.
51
+
52
+ The file uses `## <Harness>` section headers. The effective text for a given
53
+ harness is: shared preamble (everything before the first `##`) + the matching
54
+ harness section.
55
+
56
+ ## Effective-text resolution
57
+
58
+ Additive concatenation in layer order: bundled → user → project. Later layers
59
+ add or override by plain language; the model weighs the concatenated text. No
60
+ merge semantics, no structured fields, no precedence rules beyond "later text
61
+ typically wins by recency." Bundled safety floor (the SKILL.md content)
62
+ remains higher than editable content always — operators cannot use
63
+ instructions to override the coordination safety contract.
64
+
65
+ ## Loading
66
+
67
+ Agents call `tt instructions show --json` once after `tt join` on the first
68
+ substantial task in a room. The command infers the caller's harness from
69
+ identity detection (no `--harness` flag needed). The JSON result includes the
70
+ effective Markdown text and the file paths that contributed to it.
71
+
72
+ If the command fails, agents continue with the bundled `SKILL.md` guidance
73
+ alone. Coordination must never depend on instructions loading successfully.
74
+
75
+ `tt join` returns membership/coordination state only; instructions are
76
+ deliberately separate.
77
+
78
+ ## CLI surface
79
+
80
+ ```
81
+ tt instructions show [--harness <name>] [--scope effective|user|project|bundled] [--json]
82
+ tt instructions edit [--user|--project]
83
+ tt instructions reset [--user|--project]
84
+ ```
85
+
86
+ - `show` defaults to `--scope effective` and the detected current harness.
87
+ - `edit` opens `$VISUAL`, then `$EDITOR`, then a platform default if available;
88
+ prints the file path if no editor is found. Defaults to `--user`.
89
+ - `edit` materializes the seed text on first use, then opens the editor.
90
+ Subsequent edits open the existing file in place.
91
+ - `reset` requires explicit scope (no implicit `--user`); deletes the file at
92
+ the chosen scope so the layer below wins again.
93
+
94
+ **Cut from both drafts:** `set`/`append`/`init`/`diff`. Editor-only for v1.
95
+ Agents holding the stick can write to `<repo>/.talking-stick/instructions.md`
96
+ directly via filesystem.
97
+
98
+ ## Materialization
99
+
100
+ Lazy on first `tt instructions edit`, never on `tt install`. Reasoning:
101
+ - Operators who never customize stay on shipped defaults forever — package
102
+ updates flow through automatically with no stale local copies.
103
+ - Operators who customize get a one-time materialization at the moment they
104
+ show intent (running `edit`).
105
+ - `tt install` stays fast and scriptable; no extra files written.
106
+
107
+ `tt install` final output gains one hint:
108
+
109
+ ```
110
+ Customize collaboration instructions with: tt instructions edit
111
+ ```
112
+
113
+ No interactive prompt.
114
+
115
+ ## Bundled defaults (content)
116
+
117
+ Text constants in `src/instructions.ts`. Short. Cover:
118
+
119
+ - the "keep using Talking Stick until the shared task is done" rule,
120
+ including "prefer continued action unless the task is complete or the
121
+ operator explicitly redirects/stops",
122
+ - the phase vocabulary: draft, adversarial review, convergence,
123
+ implementation, implementation review, test review, release,
124
+ - typical fits (advisory, not enforced):
125
+ - Claude: prose, first-pass synthesis, tool-running, implementation review,
126
+ test review.
127
+ - Codex: adversarial review, convergence, implementation, edge cases,
128
+ release mechanics after operator approval.
129
+ - Gemini/OpenCode: conservative starter guidance until dogfood gives
130
+ evidence.
131
+
132
+ These are defaults, not a scheduler. Talking Stick does not auto-route turns
133
+ based on them.
134
+
135
+ ## Parallel drafting without workspace clutter
136
+
137
+ Wojtek's target default is independent read-only planning first, then debate
138
+ until convergence. His concern was not the independent thinking; it was
139
+ workspace clutter and imposed file structure: *"I'm not sure I want to default
140
+ to the models writing drafts or imposing a file structure on the workspace. I
141
+ guess maybe if they write something they need to delete the pre-debate versions
142
+ after convergence."*
143
+
144
+ So the default is **read-only independent drafts**, not repo draft files.
145
+ Rules when this happens:
146
+
147
+ - **Drafts go in handoff `status` text or room notes by default**, not in
148
+ workspace files. Coordination channels are the natural place for read-only
149
+ position exchange.
150
+ - **If files are unavoidable** (drafts too long for handoffs/notes), use
151
+ files in `docs/plans/` with clear `*-<harness>-draft.md` naming, and
152
+ **delete them after convergence**. Only the converged artifact remains.
153
+ - Single-agent rooms and tiny tasks can skip parallel drafting.
154
+ - The default for larger multi-agent planning is: draft independently, debate,
155
+ converge, then write only the converged artifact if a workspace file is
156
+ useful.
157
+
158
+ This is still prompt guidance, not protocol. Talking Stick does not create
159
+ draft files, track phases, or auto-route turns based on this pattern.
160
+
161
+ ## What we explicitly do not build
162
+
163
+ - No JSON config, no schema, no validator beyond a soft size cap on read.
164
+ - No structured merge semantics. Plain Markdown concatenation.
165
+ - No `set`/`append`/`init`/`diff` commands.
166
+ - No materialization on install.
167
+ - No phase tracking in the protocol. No `tt phase set`. No `phase` field in
168
+ handoff types.
169
+ - No `tt state` rendering changes.
170
+ - No model overlays.
171
+ - No auto-routing by harness.
172
+ - No instruction-text override of the bundled safety floor.
173
+
174
+ ## Implementation order
175
+
176
+ 1. `src/instructions.ts` — bundled default text constants, layer resolver
177
+ (bundled → user → project, additive concatenation), XDG-aware path
178
+ resolution, `## <Harness>` section parser, soft size cap.
179
+ 2. Add `tt instructions show` (CLI + service plumbing).
180
+ 3. Add `tt instructions edit` with `$VISUAL` / `$EDITOR` / platform fallback.
181
+ Materializes seed on first use.
182
+ 4. Add `tt instructions reset`.
183
+ 5. Edit bundled `SKILL.md`: keep current safety contract, add one short line
184
+ pointing agents at `tt instructions show --json` after first `tt join`.
185
+ 6. Update `tt install` final output with the customize hint.
186
+ 7. Tests: `tests/instructions.test.ts` for layer resolution, missing files,
187
+ materialization-only-on-edit, section parsing, additive concatenation,
188
+ bundled-fallback when commands fail. Extend `tests/cli.test.ts` for the
189
+ new commands.
190
+ 8. README: replace `tt install-skill` references with `tt install`; add a
191
+ short paragraph on `tt instructions edit` for customization.
192
+ 9. Release notes entry.
193
+
194
+ ## Acceptance criteria
195
+
196
+ - A fresh user can install with `npm i -g talking-stick && tt install --all`
197
+ and get useful defaults without answering prompts.
198
+ - The same user can run `tt instructions edit` and customize collaboration
199
+ behavior in `$EDITOR`.
200
+ - A package update never overwrites the user's instruction file.
201
+ - An agent runs one command after join (`tt instructions show --json`) and
202
+ receives effective instruction text + source paths.
203
+ - If instruction loading fails, the bundled `SKILL.md` still gives enough
204
+ guidance to coordinate safely (agents will lack phase vocabulary but
205
+ coordination/safety remains intact).
206
+ - No new protocol state, no role enforcement, and no auto-routing are added.
207
+
208
+ ## Process notes (meta)
209
+
210
+ This convergence used parallel-drafting one-off. Each harness wrote an
211
+ independent v6 draft without reading the other's first. Both landed near
212
+ the same shape but differed on three points (file structure, load
213
+ mechanism, materialization). Reading both drafts side by side made the
214
+ real trade-offs visible quickly; concessions went both ways within one
215
+ round.
216
+
217
+ The pre-debate draft files were deleted after convergence per Wojtek. The
218
+ parallel-drafting workflow is captured above as an *optional* operator-
219
+ driven pattern, not a shipped default — to avoid imposing file structure
220
+ on workspaces by default.
@@ -0,0 +1,71 @@
1
+ # Talking Stick 0.4.0
2
+
3
+ Date: 2026-05-10
4
+
5
+ This release adds editable collaboration instructions for harness roles and
6
+ phase preferences while keeping the bundled Talking Stick skill as the
7
+ coordination safety floor.
8
+
9
+ ## Added
10
+
11
+ ### Editable collaboration instructions
12
+
13
+ New CLI surface:
14
+
15
+ ```bash
16
+ tt instructions show
17
+ tt instructions edit
18
+ tt instructions reset
19
+ ```
20
+
21
+ `show` returns the effective Markdown prompt for the detected harness. Effective
22
+ instructions are layered as bundled defaults, user defaults, and project
23
+ overrides. User instructions live under the Talking Stick data directory
24
+ (`instructions.md`); project instructions live at
25
+ `.talking-stick/instructions.md` in the workspace root.
26
+
27
+ `edit` lazily materializes the default Markdown on first use and opens
28
+ `$VISUAL`, `$EDITOR`, or a platform editor fallback. `reset` deletes the chosen
29
+ user or project file so lower layers apply again.
30
+
31
+ ### Bundled defaults
32
+
33
+ The shipped defaults describe the shared phase vocabulary:
34
+
35
+ - draft
36
+ - adversarial review
37
+ - convergence
38
+ - implementation
39
+ - implementation review
40
+ - test review
41
+ - release
42
+
43
+ They also include advisory harness fits for Claude, Codex, Gemini, and
44
+ OpenCode. These defaults are prompt guidance only; Talking Stick does not track
45
+ phase state or auto-route turns.
46
+
47
+ ### Skill loading
48
+
49
+ The bundled `talking-stick` skill now tells agents to run
50
+ `tt instructions show --json` after joining a room. If the command fails, agents
51
+ continue with the bundled skill. Editable instructions can add local
52
+ preferences, but they cannot override the core safety rules around joining,
53
+ waiting, guardian checks, handoffs, event wakes, or takeover.
54
+
55
+ ## Changed
56
+
57
+ - `tt install` prints a short customization hint after a successful skill
58
+ install: `Customize collaboration instructions with: tt instructions edit`.
59
+ - The skill's re-entry guidance now says to prefer continued action until the
60
+ task is complete or the operator explicitly redirects or stops the room.
61
+
62
+ ## Verification
63
+
64
+ ```bash
65
+ npm run typecheck
66
+ npm run build
67
+ npm test
68
+ git diff --check
69
+ node dist/cli.js instructions show --harness codex --scope bundled --json
70
+ npm pack --dry-run
71
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talking-stick",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI coordination tool for path-scoped agent handoffs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,6 +60,14 @@ tt join --json
60
60
 
61
61
  Keep the returned room id and canonical path in mind. The current working directory is the implicit path for normal commands; pass an explicit path only when coordinating a different directory or intentionally selecting a nested room.
62
62
 
63
+ After joining, load editable collaboration instructions once:
64
+
65
+ ```sh
66
+ tt instructions show --json
67
+ ```
68
+
69
+ If that command fails, continue with this bundled skill. Editable instructions can add local preferences, but they do not override the safety rules in this skill.
70
+
63
71
  Right after joining, start a background ambient receiver so direct messages and turn passes/reservations surface as soon as they happen instead of waiting for the next time you poll:
64
72
 
65
73
  ```sh
@@ -214,10 +222,9 @@ The default after `tt release` or `tt assign` is to re-enter the wait loop and k
214
222
  Exit the wait loop only when one of these is true:
215
223
 
216
224
  - the shared task is explicitly finished
217
- - you are the only active member and there is no one to hand off to
218
225
  - the operator gives a direct redirect or stop
219
226
 
220
- In every other case, after `tt release` or `tt assign`, go straight back into `tt wait --json`.
227
+ In every other case, after `tt release` or `tt assign`, go straight back into `tt wait --json`. Other-harness inactivity is not an exit signal; keep waiting or take the next useful action until the task is done.
221
228
 
222
229
  If the operator tells you to drop out of coordination, run `tt leave --json`. Rooms with no active members are deleted instead of kept as history, and long-idle rooms may be purged on later invocations.
223
230