@generativereality/cctabs 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cctabs",
3
3
  "description": "Claude Code tab manager. Terminal tabs as the UI, no tmux. Claude can orchestrate its own sibling sessions.",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "author": {
6
6
  "name": "generativereality",
7
7
  "url": "https://cctabs.com"
package/README.md CHANGED
@@ -55,9 +55,9 @@ npm install -g cctabs
55
55
  **Skill only** (if you already have the CLI installed via npm):
56
56
 
57
57
  ```bash
58
- mkdir -p .claude/skills/herd
59
- curl -fsSL https://raw.githubusercontent.com/generativereality/cctabs/main/skills/herd/SKILL.md \
60
- -o .claude/skills/herd/SKILL.md
58
+ mkdir -p .claude/skills/cctabs
59
+ curl -fsSL https://raw.githubusercontent.com/generativereality/cctabs/main/skills/cctabs/SKILL.md \
60
+ -o .claude/skills/cctabs/SKILL.md
61
61
  ```
62
62
 
63
63
  **Requirements:** [Wave Terminal](https://waveterm.dev) · macOS · Node.js 20+
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { consola } from "consola";
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
11
11
  //#region package.json
12
12
  var name = "@generativereality/cctabs";
13
- var version = "0.1.0";
13
+ var version = "0.1.2";
14
14
  var description = "Claude Code tab manager. Terminal tabs as the UI, no tmux.";
15
15
  var package_default = {
16
16
  name,
@@ -28,10 +28,10 @@ var package_default = {
28
28
  "build": "tsdown",
29
29
  "typecheck": "tsc --noEmit",
30
30
  "lint": "eslint src/",
31
- "check": "bun run typecheck && bun run build",
31
+ "check": "npm run typecheck && npm run build",
32
32
  "release": "bumpp && npm publish",
33
33
  "sync-plugin": "bash scripts/sync-plugin.sh",
34
- "prepack": "bash scripts/sync-plugin.sh --check && bun run build"
34
+ "prepack": "bash scripts/sync-plugin.sh --check && npm run build"
35
35
  },
36
36
  keywords: [
37
37
  "claude-code",
@@ -206,7 +206,9 @@ var WaveAdapter = class {
206
206
  const out = execFileSync("wsh", [
207
207
  "blocks",
208
208
  "list",
209
- "--json"
209
+ "--json",
210
+ "--timeout",
211
+ "15000"
210
212
  ], { encoding: "utf-8" });
211
213
  return JSON.parse(out);
212
214
  } catch {
@@ -222,6 +224,16 @@ var WaveAdapter = class {
222
224
  `-${lastN}`
223
225
  ], { encoding: "utf-8" }).stdout ?? "";
224
226
  }
227
+ /** Poll scrollback to confirm it really is empty (block has no live shell).
228
+ * A freshly-opened tab may briefly have empty scrollback before the prompt
229
+ * renders, so poll a few times before declaring the block dead. */
230
+ async confirmScrollbackEmpty(blockId, attempts = 3, intervalMs = 500) {
231
+ for (let i = 0; i < attempts; i++) {
232
+ if (this.scrollback(blockId, 10).trim()) return false;
233
+ if (i < attempts - 1) await sleep(intervalMs);
234
+ }
235
+ return true;
236
+ }
225
237
  /** Detect whether a Claude session is running in a terminal block */
226
238
  detectSessionStatus(blockId) {
227
239
  const tail = this.scrollback(blockId, 10);
@@ -261,7 +273,7 @@ var WaveAdapter = class {
261
273
  }
262
274
  return true;
263
275
  }
264
- async waitForNewBlock(beforeIds, timeoutMs = 5e3) {
276
+ async waitForNewBlock(beforeIds, timeoutMs = 15e3) {
265
277
  const deadline = Date.now() + timeoutMs;
266
278
  while (Date.now() < deadline) {
267
279
  await sleep(250);
@@ -343,9 +355,12 @@ var WaveAdapter = class {
343
355
  }
344
356
  resolveTab(query, tabsById, tabNames) {
345
357
  const q = query.toLowerCase();
346
- return [...tabsById.keys()].filter((tid) => {
358
+ const ids = [...tabsById.keys()];
359
+ const exact = ids.filter((tid) => (tabNames.get(tid) ?? "").toLowerCase() === q);
360
+ if (exact.length > 0) return exact;
361
+ return ids.filter((tid) => {
347
362
  const name = tabNames.get(tid) ?? "";
348
- return name.toLowerCase() === q || tid.startsWith(query) || name.toLowerCase().startsWith(q);
363
+ return tid.startsWith(query) || name.toLowerCase().startsWith(q);
349
364
  });
350
365
  }
351
366
  resolveBlock(query, blocks) {
@@ -353,10 +368,11 @@ var WaveAdapter = class {
353
368
  }
354
369
  resolveWorkspace(workspaces, query) {
355
370
  const q = query.toLowerCase();
356
- return workspaces.filter(({ workspacedata: wd }) => {
371
+ const exact = workspaces.filter(({ workspacedata: wd }) => (wd.name ?? "").toLowerCase() === q);
372
+ return (exact.length > 0 ? exact : workspaces.filter(({ workspacedata: wd }) => {
357
373
  const name = wd.name ?? "";
358
- return name.toLowerCase() === q || wd.oid.startsWith(query) || name.toLowerCase().startsWith(q);
359
- }).map((w) => ({
374
+ return wd.oid.startsWith(query) || name.toLowerCase().startsWith(q);
375
+ })).map((w) => ({
360
376
  data: w.workspacedata,
361
377
  windowId: w.windowid
362
378
  }));
@@ -633,7 +649,7 @@ const newCommand = define({
633
649
  }
634
650
  let initialPromptFile;
635
651
  if (promptText) {
636
- initialPromptFile = join(tmpdir(), `herd-prompt-${Date.now()}.txt`);
652
+ initialPromptFile = join(tmpdir(), `cctabs-prompt-${Date.now()}.txt`);
637
653
  writeFileSync(initialPromptFile, promptText);
638
654
  } else if (promptFile) initialPromptFile = promptFile;
639
655
  const tabId = await openSession({
@@ -649,9 +665,11 @@ const newCommand = define({
649
665
  });
650
666
  //#endregion
651
667
  //#region src/core/session.ts
652
- /** Convert an absolute path to Claude's project slug (/ and . → -) */
668
+ /** Convert an absolute path to Claude Code's project slug.
669
+ * Claude Code replaces any non-alphanumeric character (spaces, /, ., etc.) with '-'.
670
+ * Hyphens are preserved. Example: "/Users/me/Remember This" → "-Users-me-Remember-This". */
653
671
  function pathToProjectSlug(dir) {
654
- return resolve(dir).replace(/[/.]/g, "-");
672
+ return resolve(dir).replace(/[^A-Za-z0-9-]/g, "-");
655
673
  }
656
674
  /** Find the most recent .jsonl session file in a Claude project directory */
657
675
  function latestJsonlIn(projectDir) {
@@ -849,7 +867,8 @@ const resumeCommand = define({
849
867
  process.exit(1);
850
868
  }
851
869
  const tabId = matchingTabs[0];
852
- const termBlock = (tabsById.get(tabId) ?? []).find((b) => b.view === "term");
870
+ const blocks = tabsById.get(tabId) ?? [];
871
+ const termBlock = blocks.find((b) => b.view === "term");
853
872
  if (!termBlock) {
854
873
  consola.error(`No terminal block found in tab '${name}'`);
855
874
  process.exit(1);
@@ -860,7 +879,20 @@ const resumeCommand = define({
860
879
  consola.warn(`Claude is already running in tab "${name}" (${status}) — skipping resume`);
861
880
  process.exit(0);
862
881
  }
863
- if (status === "unknown") consola.warn(`Scrollback unavailable for tab "${name}" — cannot confirm shell is ready. Proceeding anyway.`);
882
+ if (status === "unknown") {
883
+ if (await adapter.confirmScrollbackEmpty(termBlock.blockid)) {
884
+ consola.info(`Tab "${name}" has no live shell (empty scrollback) — recreating`);
885
+ for (const b of blocks) adapter.deleteBlock(b.blockid);
886
+ adapter.closeSocket();
887
+ const newTabId = await openSession({
888
+ tabName: name,
889
+ dir,
890
+ claudeCmd: `claude --resume ${sessionId} --name ${JSON.stringify(name)}`
891
+ });
892
+ consola.success(`Tab "${name}" [${newTabId.slice(0, 8)}] → claude --resume ${sessionId.slice(0, 8)}… at ${dir} (recreated)`);
893
+ return;
894
+ }
895
+ }
864
896
  const extraFlags = loadConfig().claude.flags.join(" ");
865
897
  const cmd = `cd ${JSON.stringify(dir)} && claude${extraFlags ? " " + extraFlags : ""} --resume ${sessionId} --name ${JSON.stringify(name)}\r`;
866
898
  await adapter.sendInput(termBlock.blockid, cmd);
@@ -1024,7 +1056,7 @@ const renameCommand = define({
1024
1056
  const query = ctx.positionals[1];
1025
1057
  const newName = ctx.positionals[2];
1026
1058
  if (!query || !newName) {
1027
- consola.error("Usage: herd rename <tab> <new-name>");
1059
+ consola.error("Usage: cctabs rename <tab> <new-name>");
1028
1060
  process.exit(1);
1029
1061
  }
1030
1062
  const adapter = requireWaveAdapter();
@@ -1134,7 +1166,7 @@ const sendCommand = define({
1134
1166
  const filePath = ctx.values.file;
1135
1167
  const appendEnter = ctx.values.enter ?? true;
1136
1168
  if (!query) {
1137
- consola.error("Usage: herd send <tab-or-block> [text]");
1169
+ consola.error("Usage: cctabs send <tab-or-block> [text]");
1138
1170
  process.exit(1);
1139
1171
  }
1140
1172
  let rawText;
@@ -1243,6 +1275,7 @@ const restoreCommand = define({
1243
1275
  consola.info(`Found ${toResume.length} tab(s) to restore:`);
1244
1276
  const extraFlags = loadConfig().claude.flags.join(" ");
1245
1277
  const results = [];
1278
+ const toRecreate = [];
1246
1279
  for (const tab of toResume) {
1247
1280
  const sessions = findSessionsByName(dir, tab.name);
1248
1281
  if (sessions.length === 0) {
@@ -1254,7 +1287,7 @@ const restoreCommand = define({
1254
1287
  continue;
1255
1288
  }
1256
1289
  if (sessions.length > 1) {
1257
- consola.log(` ${tab.name} — multiple sessions found, skipping (use herd resume --session to pick one)`);
1290
+ consola.log(` ${tab.name} — multiple sessions found, skipping (use cctabs resume --session to pick one)`);
1258
1291
  results.push({
1259
1292
  name: tab.name,
1260
1293
  result: "ambiguous (multiple sessions)"
@@ -1263,14 +1296,31 @@ const restoreCommand = define({
1263
1296
  }
1264
1297
  const sessionId = sessions[0].id;
1265
1298
  if (dryRun) {
1266
- consola.log(` ${tab.name} would resume session ${sessionId.slice(0, 8)}…`);
1299
+ const mode = tab.status === "unknown" ? "recreate" : "send";
1300
+ consola.log(` ${tab.name} → would ${mode} session ${sessionId.slice(0, 8)}…`);
1267
1301
  results.push({
1268
1302
  name: tab.name,
1269
1303
  result: `dry run: ${sessionId.slice(0, 8)}…`
1270
1304
  });
1271
1305
  continue;
1272
1306
  }
1273
- consola.log(` ${tab.name} resuming session ${sessionId.slice(0, 8)}…`);
1307
+ if (tab.status === "unknown") {
1308
+ if (await adapter.confirmScrollbackEmpty(tab.blockId)) {
1309
+ const blockIds = (tabsById.get(tab.tabId) ?? []).map((b) => b.blockid);
1310
+ toRecreate.push({
1311
+ name: tab.name,
1312
+ sessionId,
1313
+ blockIds,
1314
+ tabId: tab.tabId
1315
+ });
1316
+ results.push({
1317
+ name: tab.name,
1318
+ result: "queued for recreate"
1319
+ });
1320
+ continue;
1321
+ }
1322
+ }
1323
+ consola.log(` ${tab.name} → resuming session ${sessionId.slice(0, 8)}… (send)`);
1274
1324
  const cmd = `cd ${JSON.stringify(dir)} && claude${extraFlags ? " " + extraFlags : ""} --resume ${sessionId} --name ${JSON.stringify(tab.name)}\r`;
1275
1325
  await adapter.sendInput(tab.blockId, cmd);
1276
1326
  await new Promise((r) => setTimeout(r, 500));
@@ -1293,7 +1343,23 @@ const restoreCommand = define({
1293
1343
  }
1294
1344
  }
1295
1345
  }
1296
- adapter.closeSocket();
1346
+ if (!dryRun && toRecreate.length) {
1347
+ for (const t of toRecreate) for (const bid of t.blockIds) adapter.deleteBlock(bid);
1348
+ adapter.closeSocket();
1349
+ consola.info(`Recreating ${toRecreate.length} dead tab(s)…`);
1350
+ for (const t of toRecreate) try {
1351
+ const newTabId = await openSession({
1352
+ tabName: t.name,
1353
+ dir,
1354
+ claudeCmd: `claude --resume ${t.sessionId} --name ${JSON.stringify(t.name)}`
1355
+ });
1356
+ const r = results.find((x) => x.name === t.name);
1357
+ r.result = `✔ recreated [${newTabId.slice(0, 8)}]`;
1358
+ } catch (err) {
1359
+ const r = results.find((x) => x.name === t.name);
1360
+ r.result = `✘ recreate failed: ${err.message}`;
1361
+ }
1362
+ } else adapter.closeSocket();
1297
1363
  console.log("\nRestore summary:");
1298
1364
  for (const r of results) console.log(` ${r.name}: ${r.result}`);
1299
1365
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@generativereality/cctabs",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Claude Code tab manager. Terminal tabs as the UI, no tmux.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,10 +16,10 @@
16
16
  "build": "tsdown",
17
17
  "typecheck": "tsc --noEmit",
18
18
  "lint": "eslint src/",
19
- "check": "bun run typecheck && bun run build",
19
+ "check": "npm run typecheck && npm run build",
20
20
  "release": "bumpp && npm publish",
21
21
  "sync-plugin": "bash scripts/sync-plugin.sh",
22
- "prepack": "bash scripts/sync-plugin.sh --check && bun run build"
22
+ "prepack": "bash scripts/sync-plugin.sh --check && npm run build"
23
23
  },
24
24
  "keywords": [
25
25
  "claude-code",
@@ -0,0 +1,297 @@
1
+ ---
2
+ name: cctabs
3
+ description: Manage Claude Code sessions across terminal tabs (NOT browser tabs) — list running sessions, open new ones, fork, close, inspect output, and send input. Use this when working with multiple parallel Claude Code sessions in terminal tabs.
4
+ ---
5
+
6
+ You are managing Claude Code sessions using the `cctabs` CLI.
7
+
8
+ **Important:** "tabs" here means **terminal tabs** (e.g. Wave Terminal tabs), NOT browser tabs. Each terminal tab runs its own Claude Code session. This skill is for managing those terminal-based Claude Code sessions — not for browser automation.
9
+
10
+ ## Before you spawn anything: is cctabs the right tool?
11
+
12
+ cctabs is excellent for:
13
+ - **Multiple human-driven sessions** on unrelated projects (check on a deploy here, draft a blog post there, monitor a long-running task somewhere else).
14
+ - **Genuinely orthogonal parallel work** where each tab touches a disjoint file set (e.g. each tab writes to its own new directory, or each tab works on a different repo).
15
+ - **Long-running background sessions** that the user wants to check on later (builds, scrapes, benchmarks).
16
+
17
+ cctabs is the WRONG tool for:
18
+ - **Interconnected parallel work within one session.** If you're orchestrating and farming out subtasks that all modify the same evolving codebase, tabs hide each other's commits from each other. By the time they're done, you have three diverged branches that need manual merge, and any intervening change on `main`/`next` can make the merge structurally painful. **Use the Agent tool instead** — subagents share your filesystem and git state, commit in place, and surface their result back to you.
19
+ - **Sequential dependencies.** If B depends on A's commits landing, don't parallelize — run A to completion first, then B.
20
+ - **Work that touches the same files as the current orchestrator session.** Commits race, branches diverge, conflicts multiply.
21
+
22
+ A good test: *"If both tabs finish successfully, will merging their output be trivial?"* If yes, cctabs is fine. If no (or you can't tell), do it sequentially or use subagents.
23
+
24
+ ## First: Ensure cctabs is available
25
+
26
+ ```bash
27
+ which cctabs || ls "$(npm prefix -g)/bin/cctabs" 2>/dev/null
28
+ ```
29
+
30
+ If found, use whichever path works. If `cctabs` is on PATH, use it directly. Otherwise use the full path from `npm prefix -g`.
31
+
32
+ If not found, ask the user: "cctabs isn't installed yet — want me to install it globally with npm?" If they agree, run:
33
+
34
+ ```bash
35
+ npm install -g @generativereality/cctabs
36
+ ```
37
+
38
+ Do not modify PATH or npm configuration beyond this.
39
+
40
+ ---
41
+
42
+ Each Claude Code session runs in its own **terminal tab**. `cctabs` lets you — and other Claude Code sessions — introspect and orchestrate the full session fleet.
43
+
44
+ ## When to Use Worktrees
45
+
46
+ **Use `--worktree` whenever a tab will edit code on a branch that differs from the main working tree.** This includes:
47
+ - Fixing CI on a PR (`cctabs new fix-1789 ~/Dev/myapp --worktree`)
48
+ - Working on a feature branch while the main checkout runs a dev server
49
+ - Any task where multiple tabs might checkout different branches
50
+
51
+ Without `--worktree`, all tabs share the same working directory. If two tabs checkout different branches, they stomp on each other's files — causing silent conflicts, lost changes, and broken dev servers.
52
+
53
+ **Rule of thumb:**
54
+ - **Read-only / docs / coordination** → no worktree needed (stays on current branch)
55
+ - **Editing code on a different branch** → always `--worktree`
56
+
57
+ ```bash
58
+ # ❌ WRONG — two tabs checking out different branches in the same directory
59
+ cctabs new fix-auth ~/Dev/myapp --prompt "checkout PR #101 and fix lint"
60
+ cctabs new fix-api ~/Dev/myapp --prompt "checkout PR #102 and fix tests"
61
+
62
+ # ✅ RIGHT — each gets its own isolated copy
63
+ cctabs new fix-auth ~/Dev/myapp --worktree --prompt "checkout PR #101 and fix lint"
64
+ cctabs new fix-api ~/Dev/myapp --worktree --prompt "checkout PR #102 and fix tests"
65
+ ```
66
+
67
+ ## Quick Reference
68
+
69
+ ```bash
70
+ cctabs sessions # list all tabs with session status
71
+ cctabs list # list all workspaces, tabs, and blocks
72
+ cctabs new <name> [dir] [-w workspace] [-p "prompt"] [-f file] # new tab + claude
73
+ cctabs resume <name> [dir] # resume last session (reuses tab or creates one)
74
+ cctabs fork <tab-name> [-n new-name] # fork session into new tab (--resume <id> --fork-session)
75
+ cctabs close <name-or-id> # close a tab
76
+ cctabs rename <name-or-id> <new-name> # rename a tab
77
+ cctabs scrollback <tab-or-block> [n] # read terminal output (default: 50 lines)
78
+ cctabs send <tab-or-block> [text] # send input — arg, --file, or stdin pipe
79
+ cctabs config # show config and path
80
+ ```
81
+
82
+ ## Workflow: Checking What's Running
83
+
84
+ Before starting new sessions, always check what's already active:
85
+
86
+ ```bash
87
+ cctabs sessions
88
+ ```
89
+
90
+ Output example:
91
+ ```
92
+ Sessions
93
+ ==================================================
94
+
95
+ Workspace: work (current)
96
+
97
+ [a1b2c3d4] "auth" ◄ ~/Dev/myapp
98
+ ● active
99
+ [e5f6a7b8] "api" ~/Dev/myapp
100
+ ○ idle
101
+ [c9d0e1f2] "infra" ~/Dev/myapp
102
+ terminal
103
+ last: $ git status
104
+ ```
105
+
106
+ ## Workflow: Opening a Session Batch
107
+
108
+ ```bash
109
+ cctabs new auth ~/Dev/myapp
110
+ cctabs new api ~/Dev/myapp
111
+ cctabs new infra ~/Dev/myapp
112
+ ```
113
+
114
+ Each tab is automatically named and the claude session name is synced to the tab title.
115
+
116
+ ## Workflow: Resuming a Session
117
+
118
+ `cctabs resume` finds the latest session ID for the directory and runs `claude --resume <id>`.
119
+ If the named tab still exists, it reuses it. If not, it creates a new tab.
120
+
121
+ ```bash
122
+ cctabs resume auth ~/Dev/myapp # reuses "auth" tab if it exists, otherwise creates one
123
+ cctabs resume api ~/Dev/myapp
124
+ ```
125
+
126
+ **Use `cctabs resume` instead of `cctabs new` when you want to continue a previous conversation.**
127
+ `cctabs new` always starts a fresh Claude session. `cctabs resume` picks up where the last session left off.
128
+
129
+ ## Workflow: Forking a Session
130
+
131
+ Use `fork` when you want to explore an alternative approach without disrupting the original.
132
+ `cctabs fork` finds the latest session ID for the source tab and opens a new tab with
133
+ `claude --resume <id> --fork-session`. The source tab is not modified.
134
+
135
+ ```bash
136
+ cctabs fork auth # creates "auth-fork" tab
137
+ cctabs fork auth -n "auth-v2" # creates "auth-v2" tab
138
+ ```
139
+
140
+ The forked session shares full conversation history up to the fork point, then diverges independently.
141
+
142
+ ## Workflow: Spawning a Parallel Agent
143
+
144
+ **Before spawning, re-read "is cctabs the right tool?" above.** If the task is interconnected with your current work, use the Agent tool (subagents) instead — they share your filesystem and commits.
145
+
146
+ As a Claude Code session, you can spawn a sibling session for a **genuinely independent** parallel task:
147
+
148
+ **Preferred: pass the initial task directly to `cctabs new`** using `--prompt` or `--file`. This polls internally until Claude's `❯` prompt appears before sending — no race condition:
149
+
150
+ ```bash
151
+ cctabs new payments ~/Dev/myapp --prompt "implement the billing endpoint"
152
+ cctabs new payments ~/Dev/myapp --file /tmp/task.txt
153
+ ```
154
+
155
+ If you need to send a task after the fact, poll first:
156
+
157
+ ```bash
158
+ cctabs new payments ~/Dev/myapp
159
+ # Poll until ❯ appears (typically 10-15s with MCP servers)
160
+ cctabs scrollback payments 5 # repeat until you see ❯
161
+ cctabs send payments --file /tmp/task.txt
162
+ cctabs send payments "yes\n" # quick replies
163
+ ```
164
+
165
+ **Do NOT call `cctabs send` immediately after `cctabs new`** — Claude is still starting up and the text will land as raw shell commands.
166
+
167
+ ### Spawning gotchas (hard-won)
168
+
169
+ 1. **Verify the worktree base immediately after spawn.** `--worktree` does not always branch from your current HEAD — if you have local un-pushed commits, the child session may branch from an older commit (whatever the remote tracking branch points at). Always check:
170
+ ```bash
171
+ cctabs new kid ~/Dev/myapp --worktree -p "..."
172
+ # Then in the ORCHESTRATOR tab:
173
+ git -C ~/Dev/myapp/.claude/worktrees/kid log --oneline -1
174
+ ```
175
+ If the base is not what you expected, abort and fix: either push your commits to the tracking branch first, or spawn without `--worktree` and let the subagent work on your branch directly.
176
+
177
+ 2. **Never instruct a subagent to "rebase your branch on main/next."** Subagents interpret this liberally. A common failure mode: the subagent does `git reset --hard <remote>` and throws away its own completed commits, trying to redo the work from scratch. Instead:
178
+ - Have the orchestrator handle rebases after the subagent is done.
179
+ - Or send a precise patch/diff rather than a verbal rebase instruction.
180
+ - Or tell the subagent explicitly: *"do not rebase, do not reset; make fixup commits on top of your existing branch."*
181
+
182
+ 3. **Subagents won't see each other's commits.** Each tab has its own working tree. If ws-A commits a schema, ws-B cannot consume it until you merge A → main → rebase B. This is a fundamental property, not a bug. Only parallelize when this limitation doesn't matter.
183
+
184
+ 4. **Don't delegate rebases or merges to subagents.** Those are orchestrator work. Subagents produce content; orchestrator integrates.
185
+
186
+ ## Workflow: Monitoring Another Session
187
+
188
+ ```bash
189
+ cctabs scrollback auth # last 50 lines
190
+ cctabs scrollback auth 200 # last 200 lines
191
+ ```
192
+
193
+ ## Workflow: Sending Input to a Session
194
+
195
+ ```bash
196
+ cctabs send auth "yes\n" # approve a tool call
197
+ cctabs send auth "\n" # press enter (confirm a prompt)
198
+ cctabs send auth "/clear\n" # send a slash command
199
+ cctabs send auth --file ~/prompts/task.txt # send a full prompt from file
200
+ echo "do the thing" | cctabs send auth # pipe via stdin
201
+ ```
202
+
203
+ ## Workflow: Worktrees
204
+
205
+ **Always point tabs at the repo root — never at a manually-created worktree directory.** Claude Code manages worktrees itself via `claude --worktree <name>`, which creates `.claude/worktrees/<name>/` inside the repo and handles branch creation and cleanup automatically.
206
+
207
+ ### New isolated session (new branch, Claude manages everything)
208
+
209
+ ```bash
210
+ cctabs new feature-name ~/Dev/myapp --worktree
211
+ # Equivalent to: cd ~/Dev/myapp && claude --worktree "feature-name" --name "feature-name"
212
+ # Claude creates: ~/Dev/myapp/.claude/worktrees/feature-name/
213
+ # Claude creates branch: worktree-feature-name
214
+ ```
215
+
216
+ ### Existing branch — ask Claude to enter the worktree mid-session
217
+
218
+ ```bash
219
+ cctabs new hiring ~/Dev/myapp # open tab at repo root
220
+ cctabs send hiring "Enter a worktree for branch z.old/new-hire-ad and ..."
221
+ # Claude will use EnterWorktree tool to set up isolation
222
+ ```
223
+
224
+ ### Do NOT manage git worktrees manually
225
+
226
+ ```bash
227
+ # ❌ WRONG — do not create worktree dirs yourself and pass them to cctabs new
228
+ git worktree add ~/Dev/myapp-feature branch
229
+ cctabs new feature ~/Dev/myapp-feature
230
+
231
+ # ✅ RIGHT — always use repo root; let Claude Code manage the worktree
232
+ cctabs new feature ~/Dev/myapp --worktree
233
+ ```
234
+
235
+ **Why:** Manually created worktree dirs placed outside the repo confuse Claude Code's session tracking, project memory lookup (`.claude/` is in the main repo), and CLAUDE.md resolution. Claude Code's built-in worktree support keeps everything co-located under `.claude/worktrees/` and handles cleanup on session exit.
236
+
237
+ **Worktree base-commit caveat:** after spawning with `--worktree`, verify the branch base matches your expectation (see "Spawning gotchas" above). If your orchestrator has local commits that haven't been pushed, the worktree may branch from the stale remote tip instead of HEAD. This bites hardest when parallel tabs need to share schema/types your orchestrator has been working on — they won't see those changes if they branched before the commits landed upstream.
238
+
239
+ ## Handling `cctabs new` Timeout Errors
240
+
241
+ `cctabs new` may occasionally fail with "Timed out waiting for new terminal block". This does **NOT** mean you have too many tabs or that Wave Terminal has hit a limit.
242
+
243
+ **Possible causes** (root cause not yet confirmed):
244
+ - Wave Terminal may need to be in focus / foreground for tab creation to register
245
+ - The internal timeout may be slightly too short for the current system load
246
+ - Transient IPC timing issue between cctabs and Wave
247
+
248
+ **What to do:**
249
+ 1. **Retry the same command** — it often works on the second attempt
250
+ 2. If it fails again, wait a few seconds and retry once more
251
+ 3. If it keeps failing, ask the user to bring Wave Terminal to the foreground and try again
252
+
253
+ **What NOT to do:**
254
+ - ❌ Do NOT assume there is a "tab limit" — there isn't one
255
+ - ❌ Do NOT close other tabs to "make room" — this destroys the user's sessions
256
+ - ❌ Do NOT suggest the user has too many tabs open
257
+
258
+ ## Workflow: Cleanup
259
+
260
+ **⚠️ NEVER close tabs without explicit user approval.** Each tab may contain an active session with important context, uncommitted work, or in-progress tasks. Closing a tab is destructive and irreversible.
261
+
262
+ **Always ask first:**
263
+ > "These tabs look idle: `old-feature`, `fix-1234`. Want me to close any of them?"
264
+
265
+ Only after the user confirms:
266
+ ```bash
267
+ cctabs close old-feature # close by name (prefix match)
268
+ cctabs close e5f6a7b8 # close by block ID prefix
269
+ ```
270
+
271
+ ## Tab Naming Conventions
272
+
273
+ Name tabs after the **project or task**:
274
+ - `auth` — authentication work
275
+ - `api` — API service
276
+ - `infra` — infrastructure
277
+ - `pr-1234` — specific PR work
278
+ - `auth-v2` — forked attempt
279
+
280
+ ## Notes
281
+
282
+ - Tab names are matched by exact name or prefix (case-insensitive)
283
+ - Block IDs can be abbreviated to the first 8 characters
284
+ - `cctabs new` and `cctabs resume` automatically pass `--name <tab-name>` to claude, syncing the session display name with the tab title
285
+ - Configured `claude.flags` in `~/.config/cctabs/config.toml` are applied to every session
286
+ - `cctabs send` resolves tab names to their terminal block automatically
287
+
288
+ ## Lesson: the common failure mode
289
+
290
+ A pattern that wastes the most tokens: an orchestrator spawns three tabs for "parallel workstreams" on the same feature, each tab diverges from the base and from each other, the orchestrator loses visibility into what each is doing, one tab misinterprets a course-correct and resets its own work, and finally the orchestrator spends hours hand-merging commits that don't apply cleanly against an intervening refactor.
291
+
292
+ The fix is upstream: before spawning, ask *"are these workstreams actually independent?"* If the answer is "mostly, but they share a common data model / schema / utility module" — they are **not** independent for cctabs purposes. Either:
293
+ - Do them sequentially in one tab (cheapest).
294
+ - Use the Agent tool for subtasks that share orchestrator state.
295
+ - Land the shared pieces first on `main`/`next`, push, then spawn tabs (each branches cleanly off the new tip and work is truly orthogonal from there).
296
+
297
+ Parallel tabs earn their keep when the work is genuinely orthogonal (separate repos, separate brand-new directories, independent features) and when you'd otherwise be idle waiting for one long-running task to finish.
@@ -1,202 +0,0 @@
1
- ---
2
- name: herd
3
- description: Manage Claude Code sessions across terminal tabs (NOT browser tabs) — list running sessions, open new ones, fork, close, inspect output, and send input. Use this when working with multiple parallel Claude Code sessions in terminal tabs.
4
- ---
5
-
6
- You are managing Claude Code sessions using the `cctabs` CLI.
7
-
8
- **Important:** "tabs" here means **terminal tabs** (e.g. Wave Terminal tabs), NOT browser tabs. Each terminal tab runs its own Claude Code session. This skill is for managing those terminal-based Claude Code sessions — not for browser automation.
9
-
10
- ## First: Ensure cctabs is available
11
-
12
- ```bash
13
- which cctabs || ls "$(npm prefix -g)/bin/cctabs" 2>/dev/null
14
- ```
15
-
16
- If found, use whichever path works. If `cctabs` is on PATH, use it directly. Otherwise use the full path from `npm prefix -g`.
17
-
18
- If not found, ask the user: "cctabs isn't installed yet — want me to install it globally with npm?" If they agree, run:
19
-
20
- ```bash
21
- npm install -g @generativereality/cctabs
22
- ```
23
-
24
- Do not modify PATH or npm configuration beyond this.
25
-
26
- ---
27
-
28
- Each Claude Code session runs in its own **terminal tab**. `cctabs` lets you — and other Claude Code sessions — introspect and orchestrate the full session fleet.
29
-
30
- ## Quick Reference
31
-
32
- ```bash
33
- cctabs sessions # list all tabs with session status
34
- cctabs list # list all workspaces, tabs, and blocks
35
- cctabs new <name> [dir] [-w workspace] [-p "prompt"] [-f file] # new tab + claude
36
- cctabs resume <name> [dir] # resume last session (reuses tab or creates one)
37
- cctabs fork <tab-name> [-n new-name] # fork session into new tab (--resume <id> --fork-session)
38
- cctabs close <name-or-id> # close a tab
39
- cctabs rename <name-or-id> <new-name> # rename a tab
40
- cctabs scrollback <tab-or-block> [n] # read terminal output (default: 50 lines)
41
- cctabs send <tab-or-block> [text] # send input — arg, --file, or stdin pipe
42
- cctabs config # show config and path
43
- ```
44
-
45
- ## Workflow: Checking What's Running
46
-
47
- Before starting new sessions, always check what's already active:
48
-
49
- ```bash
50
- cctabs sessions
51
- ```
52
-
53
- Output example:
54
- ```
55
- Sessions
56
- ==================================================
57
-
58
- Workspace: work (current)
59
-
60
- [a1b2c3d4] "auth" ◄ ~/Dev/myapp
61
- ● active
62
- [e5f6a7b8] "api" ~/Dev/myapp
63
- ○ idle
64
- [c9d0e1f2] "infra" ~/Dev/myapp
65
- terminal
66
- last: $ git status
67
- ```
68
-
69
- ## Workflow: Opening a Session Batch
70
-
71
- ```bash
72
- cctabs new auth ~/Dev/myapp
73
- cctabs new api ~/Dev/myapp
74
- cctabs new infra ~/Dev/myapp
75
- ```
76
-
77
- Each tab is automatically named and the claude session name is synced to the tab title.
78
-
79
- ## Workflow: Resuming a Session
80
-
81
- `cctabs resume` finds the latest session ID for the directory and runs `claude --resume <id>`.
82
- If the named tab still exists, it reuses it. If not, it creates a new tab.
83
-
84
- ```bash
85
- cctabs resume auth ~/Dev/myapp # reuses "auth" tab if it exists, otherwise creates one
86
- cctabs resume api ~/Dev/myapp
87
- ```
88
-
89
- **Use `cctabs resume` instead of `cctabs new` when you want to continue a previous conversation.**
90
- `cctabs new` always starts a fresh Claude session. `cctabs resume` picks up where the last session left off.
91
-
92
- ## Workflow: Forking a Session
93
-
94
- Use `fork` when you want to explore an alternative approach without disrupting the original.
95
- `cctabs fork` finds the latest session ID for the source tab and opens a new tab with
96
- `claude --resume <id> --fork-session`. The source tab is not modified.
97
-
98
- ```bash
99
- cctabs fork auth # creates "auth-fork" tab
100
- cctabs fork auth -n "auth-v2" # creates "auth-v2" tab
101
- ```
102
-
103
- The forked session shares full conversation history up to the fork point, then diverges independently.
104
-
105
- ## Workflow: Spawning a Parallel Agent
106
-
107
- As a Claude Code session, you can spawn a sibling session to work on a parallel task:
108
-
109
- **Preferred: pass the initial task directly to `cctabs new`** using `--prompt` or `--file`. This polls internally until Claude's `❯` prompt appears before sending — no race condition:
110
-
111
- ```bash
112
- cctabs new payments ~/Dev/myapp --prompt "implement the billing endpoint"
113
- cctabs new payments ~/Dev/myapp --file /tmp/task.txt
114
- ```
115
-
116
- If you need to send a task after the fact, poll first:
117
-
118
- ```bash
119
- cctabs new payments ~/Dev/myapp
120
- # Poll until ❯ appears (typically 10-15s with MCP servers)
121
- cctabs scrollback payments 5 # repeat until you see ❯
122
- cctabs send payments --file /tmp/task.txt
123
- cctabs send payments "yes\n" # quick replies
124
- ```
125
-
126
- **Do NOT call `cctabs send` immediately after `cctabs new`** — Claude is still starting up and the text will land as raw shell commands.
127
-
128
- ## Workflow: Monitoring Another Session
129
-
130
- ```bash
131
- cctabs scrollback auth # last 50 lines
132
- cctabs scrollback auth 200 # last 200 lines
133
- ```
134
-
135
- ## Workflow: Sending Input to a Session
136
-
137
- ```bash
138
- cctabs send auth "yes\n" # approve a tool call
139
- cctabs send auth "\n" # press enter (confirm a prompt)
140
- cctabs send auth "/clear\n" # send a slash command
141
- cctabs send auth --file ~/prompts/task.txt # send a full prompt from file
142
- echo "do the thing" | cctabs send auth # pipe via stdin
143
- ```
144
-
145
- ## Workflow: Worktrees
146
-
147
- **Always point tabs at the repo root — never at a manually-created worktree directory.** Claude Code manages worktrees itself via `claude --worktree <name>`, which creates `.claude/worktrees/<name>/` inside the repo and handles branch creation and cleanup automatically.
148
-
149
- ### New isolated session (new branch, Claude manages everything)
150
-
151
- ```bash
152
- cctabs new feature-name ~/Dev/myapp --worktree
153
- # Equivalent to: cd ~/Dev/myapp && claude --worktree "feature-name" --name "feature-name"
154
- # Claude creates: ~/Dev/myapp/.claude/worktrees/feature-name/
155
- # Claude creates branch: worktree-feature-name
156
- ```
157
-
158
- ### Existing branch — ask Claude to enter the worktree mid-session
159
-
160
- ```bash
161
- cctabs new hiring ~/Dev/myapp # open tab at repo root
162
- cctabs send hiring "Enter a worktree for branch z.old/new-hire-ad and ..."
163
- # Claude will use EnterWorktree tool to set up isolation
164
- ```
165
-
166
- ### Do NOT manage git worktrees manually
167
-
168
- ```bash
169
- # ❌ WRONG — do not create worktree dirs yourself and pass them to cctabs new
170
- git worktree add ~/Dev/myapp-feature branch
171
- cctabs new feature ~/Dev/myapp-feature
172
-
173
- # ✅ RIGHT — always use repo root; let Claude Code manage the worktree
174
- cctabs new feature ~/Dev/myapp --worktree
175
- ```
176
-
177
- **Why:** Manually created worktree dirs placed outside the repo confuse Claude Code's session tracking, project memory lookup (`.claude/` is in the main repo), and CLAUDE.md resolution. Claude Code's built-in worktree support keeps everything co-located under `.claude/worktrees/` and handles cleanup on session exit.
178
-
179
- ## Workflow: Cleanup
180
-
181
- ```bash
182
- cctabs sessions # find idle/terminal tabs
183
- cctabs close old-feature # close by name (prefix match)
184
- cctabs close e5f6a7b8 # close by block ID prefix
185
- ```
186
-
187
- ## Tab Naming Conventions
188
-
189
- Name tabs after the **project or task**:
190
- - `auth` — authentication work
191
- - `api` — API service
192
- - `infra` — infrastructure
193
- - `pr-1234` — specific PR work
194
- - `auth-v2` — forked attempt
195
-
196
- ## Notes
197
-
198
- - Tab names are matched by exact name or prefix (case-insensitive)
199
- - Block IDs can be abbreviated to the first 8 characters
200
- - `cctabs new` and `cctabs resume` automatically pass `--name <tab-name>` to claude, syncing the session display name with the tab title
201
- - Configured `claude.flags` in `~/.config/cctabs/config.toml` are applied to every session
202
- - `cctabs send` resolves tab names to their terminal block automatically