@towles/tool 0.0.127 → 0.0.129

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.
Files changed (35) hide show
  1. package/node_modules/@towles/shared/src/errors.test.ts +33 -0
  2. package/node_modules/@towles/shared/src/errors.ts +14 -0
  3. package/node_modules/@towles/shared/src/index.ts +1 -0
  4. package/package.json +15 -15
  5. package/packages/agentboard/apps/tui/package.json +2 -2
  6. package/packages/agentboard/apps/tui/src/constants.ts +3 -1
  7. package/packages/agentboard/apps/tui/src/index.tsx +3 -1
  8. package/packages/agentboard/apps/tui/src/mux-context.ts +3 -1
  9. package/packages/agentboard/packages/runtime/src/agents/watchers/amp.ts +6 -2
  10. package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.ts +21 -7
  11. package/packages/agentboard/packages/runtime/src/agents/watchers/codex.ts +9 -3
  12. package/packages/agentboard/packages/runtime/src/agents/watchers/opencode.ts +18 -6
  13. package/packages/agentboard/packages/runtime/src/debug.ts +3 -1
  14. package/packages/agentboard/packages/runtime/src/server/index.ts +44 -14
  15. package/packages/agentboard/packages/runtime/src/server/launcher.ts +3 -1
  16. package/packages/agentboard/packages/runtime/src/server/pane-scanner.ts +7 -2
  17. package/packages/core/.claude-plugin/plugin.json +2 -2
  18. package/packages/core/README.md +7 -0
  19. package/packages/core/skills/parallel-slots/SKILL.md +69 -0
  20. package/packages/shared/src/errors.test.ts +33 -0
  21. package/packages/shared/src/errors.ts +14 -0
  22. package/packages/shared/src/index.ts +1 -0
  23. package/src/commands/agentboard.ts +3 -1
  24. package/src/commands/auto-claude/claude-cli.ts +15 -6
  25. package/src/commands/auto-claude/index.ts +21 -21
  26. package/src/commands/auto-claude/logger.ts +3 -0
  27. package/src/commands/auto-claude/pipeline.ts +12 -11
  28. package/src/commands/auto-claude/steps/create-pr.ts +8 -10
  29. package/src/commands/auto-claude/steps/fetch-issues.ts +8 -8
  30. package/src/commands/auto-claude/steps/implement.ts +7 -8
  31. package/src/commands/auto-claude/utils.ts +5 -8
  32. package/src/commands/graph/analyzer.test.ts +3 -1
  33. package/src/commands/graph.test.ts +2 -0
  34. package/src/commands/journal/list.ts +2 -1
  35. package/src/commands/journal/search.ts +3 -3
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AppError } from "./errors";
3
+
4
+ describe("AppError", () => {
5
+ it("carries code and message", () => {
6
+ const err = new AppError("my_code", "boom");
7
+ expect(err.code).toBe("my_code");
8
+ expect(err.message).toBe("boom");
9
+ expect(err).toBeInstanceOf(Error);
10
+ expect(err).toBeInstanceOf(AppError);
11
+ });
12
+
13
+ it("uses the constructor name as the error name", () => {
14
+ class ChildError extends AppError {
15
+ constructor(message: string) {
16
+ super("child_code", message);
17
+ }
18
+ }
19
+ expect(new AppError("c", "m").name).toBe("AppError");
20
+ expect(new ChildError("m").name).toBe("ChildError");
21
+ });
22
+
23
+ it("forwards cause to the native Error.cause", () => {
24
+ const root = new Error("root");
25
+ const err = new AppError("wrapped", "outer", { cause: root });
26
+ expect(err.cause).toBe(root);
27
+ });
28
+
29
+ it("leaves cause undefined when not provided", () => {
30
+ const err = new AppError("nope", "no cause");
31
+ expect(err.cause).toBeUndefined();
32
+ });
33
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared error base class for towles-tool.
3
+ *
4
+ * Uses the native ES2022 `Error.cause` option so stack traces chain properly.
5
+ */
6
+ export class AppError extends Error {
7
+ readonly code: string;
8
+
9
+ constructor(code: string, message: string, options?: { cause?: unknown }) {
10
+ super(message, options);
11
+ this.name = new.target.name;
12
+ this.code = code;
13
+ }
14
+ }
@@ -1,3 +1,4 @@
1
+ export { AppError } from "./errors.js";
1
2
  export { ensureDir, fileExists, readFile, writeFile } from "./fs.js";
2
3
  export { getTerminalColumns, limitText, printWithHexColor } from "./render.js";
3
4
  export { formatDate, generateJournalFilename, getMondayOfWeek, getWeekInfo } from "./date-utils.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towles/tool",
3
- "version": "0.0.127",
3
+ "version": "0.0.129",
4
4
  "description": "One off quality of life scripts that I use on a daily basis.",
5
5
  "homepage": "https://github.com/ChrisTowles/towles-tool#readme",
6
6
  "bugs": {
@@ -50,32 +50,32 @@
50
50
  "prepare": "simple-git-hooks"
51
51
  },
52
52
  "dependencies": {
53
- "@anthropic-ai/claude-code": "^2.1.4",
54
- "@anthropic-ai/sdk": "^0.56.0",
53
+ "@anthropic-ai/claude-code": "^2.1.107",
54
+ "@anthropic-ai/sdk": "^0.88.0",
55
55
  "@towles/shared": "workspace:*",
56
- "citty": "^0.1.6",
56
+ "citty": "^0.2.2",
57
57
  "consola": "^3.4.2",
58
58
  "d3-hierarchy": "^3.1.2",
59
59
  "fzf": "^0.5.2",
60
- "luxon": "^3.7.1",
60
+ "luxon": "^3.7.2",
61
61
  "neverthrow": "^8.2.0",
62
62
  "picocolors": "^1.1.1",
63
63
  "prompts": "^2.4.2",
64
- "zod": "^4.0.5"
64
+ "zod": "^4.3.6"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/bun": "latest",
68
68
  "@types/d3-hierarchy": "^3.1.7",
69
- "@types/luxon": "^3.6.2",
70
- "@types/node": "^22.16.3",
69
+ "@types/luxon": "^3.7.1",
70
+ "@types/node": "^25.6.0",
71
71
  "@types/prompts": "^2.4.9",
72
- "@typescript/native-preview": "^7.0.0-dev.20260111.1",
73
- "bumpp": "^10.4.0",
74
- "oxfmt": "^0.24.0",
75
- "oxlint": "^1.7.0",
76
- "simple-git-hooks": "^2.13.0",
77
- "typescript": "^5.8.3",
78
- "vitest": "^4.0.17"
72
+ "@typescript/native-preview": "^7.0.0-dev.20260414.1",
73
+ "bumpp": "^11.0.1",
74
+ "oxfmt": "^0.45.0",
75
+ "oxlint": "^1.60.0",
76
+ "simple-git-hooks": "^2.13.1",
77
+ "typescript": "^6.0.2",
78
+ "vitest": "^4.1.4"
79
79
  },
80
80
  "bundledDependencies": [
81
81
  "@towles/shared"
@@ -12,8 +12,8 @@
12
12
  "dependencies": {
13
13
  "@babel/core": "^7.28.0",
14
14
  "@babel/preset-typescript": "^7.27.1",
15
- "@opentui/core": "^0.1.88",
16
- "@opentui/solid": "^0.1.88",
15
+ "@opentui/core": "^0.1.99",
16
+ "@opentui/solid": "^0.1.99",
17
17
  "@tt-agentboard/mux-tmux": "workspace:*",
18
18
  "@tt-agentboard/runtime": "workspace:*",
19
19
  "babel-plugin-module-resolver": "^5.0.2",
@@ -32,5 +32,7 @@ export function logResizeDebug(message: string, data?: Record<string, unknown>):
32
32
  const extra = data ? ` ${JSON.stringify(data)}` : "";
33
33
  try {
34
34
  appendFileSync(TUI_RESIZE_LOG, `[${ts}] [pid:${process.pid}] ${message}${extra}\n`);
35
- } catch {}
35
+ } catch {
36
+ // intentionally ignored: debug log file write is best-effort
37
+ }
36
38
  }
@@ -360,7 +360,9 @@ function App() {
360
360
  if (startupFocusToPublish) {
361
361
  send({ type: "focus-session", name: startupFocusToPublish });
362
362
  }
363
- } catch {}
363
+ } catch {
364
+ // intentionally ignored: socket setup errors are non-fatal at startup
365
+ }
364
366
  };
365
367
 
366
368
  socket.onclose = () => {
@@ -34,7 +34,9 @@ export function refocusMainPane(muxCtx: MuxContext): void {
34
34
  const paneId = main.split(" ")[0];
35
35
  Bun.spawnSync(["tmux", "select-pane", "-t", paneId], { stdout: "pipe", stderr: "pipe" });
36
36
  }
37
- } catch {}
37
+ } catch {
38
+ // intentionally ignored: pane discovery is best-effort
39
+ }
38
40
  }
39
41
  }
40
42
 
@@ -129,13 +129,17 @@ export class AmpAgentWatcher implements AgentWatcher {
129
129
  if (this.fsWatcher) {
130
130
  try {
131
131
  this.fsWatcher.close();
132
- } catch {}
132
+ } catch {
133
+ // intentionally ignored: watcher may already be closed during shutdown
134
+ }
133
135
  this.fsWatcher = null;
134
136
  }
135
137
  if (this.sessionWatcher) {
136
138
  try {
137
139
  this.sessionWatcher.close();
138
- } catch {}
140
+ } catch {
141
+ // intentionally ignored: watcher may already be closed during shutdown
142
+ }
139
143
  this.sessionWatcher = null;
140
144
  }
141
145
  if (this.pollTimer) {
@@ -175,7 +175,9 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
175
175
  for (const w of this.fsWatchers) {
176
176
  try {
177
177
  w.close();
178
- } catch {}
178
+ } catch {
179
+ // intentionally ignored: watcher may already be closed during shutdown
180
+ }
179
181
  }
180
182
  this.fsWatchers = [];
181
183
  if (this.pollTimer) {
@@ -211,7 +213,9 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
211
213
  try {
212
214
  const mtime = (await stat(filePath)).mtimeMs;
213
215
  if (Date.now() - mtime > JOURNAL_IDLE_TIMEOUT_MS) becomeIdle = true;
214
- } catch {}
216
+ } catch {
217
+ // intentionally ignored: stat failure leaves becomeIdle unchanged
218
+ }
215
219
  }
216
220
 
217
221
  if (becomeIdle) {
@@ -271,7 +275,9 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
271
275
  try {
272
276
  const mtime = (await stat(filePath)).mtimeMs;
273
277
  if (Date.now() - mtime > JOURNAL_IDLE_TIMEOUT_MS) latestStatus = "idle";
274
- } catch {}
278
+ } catch {
279
+ // intentionally ignored: stat failure leaves status unchanged
280
+ }
275
281
  }
276
282
 
277
283
  this.sessions.set(threadId, {
@@ -448,7 +454,9 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
448
454
  });
449
455
  this.fsWatchers.push(w);
450
456
  this.watchedDirs.add(dirPath);
451
- } catch {}
457
+ } catch {
458
+ // intentionally ignored: watching dir is best-effort, will retry on next setup
459
+ }
452
460
  }
453
461
 
454
462
  private hasRecentFiles(dirPath: string): boolean {
@@ -461,9 +469,13 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
461
469
  try {
462
470
  const s = fs.statSync(join(dirPath, file));
463
471
  if (now - s.mtimeMs < STALE_MS) return true;
464
- } catch {}
472
+ } catch {
473
+ // intentionally ignored: skip unreadable file
474
+ }
465
475
  }
466
- } catch {}
476
+ } catch {
477
+ // intentionally ignored: dir unreadable, treat as no recent files
478
+ }
467
479
  return false;
468
480
  }
469
481
 
@@ -503,6 +515,8 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
503
515
  this.watchDir(dirPath);
504
516
  });
505
517
  this.fsWatchers.push(w);
506
- } catch {}
518
+ } catch {
519
+ // intentionally ignored: top-level watch is best-effort
520
+ }
507
521
  }
508
522
  }
@@ -212,7 +212,9 @@ export class CodexAgentWatcher implements AgentWatcher {
212
212
  if (this.fsWatcher) {
213
213
  try {
214
214
  this.fsWatcher.close();
215
- } catch {}
215
+ } catch {
216
+ // intentionally ignored: watcher may already be closed during shutdown
217
+ }
216
218
  this.fsWatcher = null;
217
219
  }
218
220
  if (this.pollTimer) {
@@ -238,7 +240,9 @@ export class CodexAgentWatcher implements AgentWatcher {
238
240
  if (entry.id && entry.thread_name) {
239
241
  names.set(entry.id, entry.thread_name);
240
242
  }
241
- } catch {}
243
+ } catch {
244
+ // intentionally ignored: skip malformed JSON line
245
+ }
242
246
  }
243
247
 
244
248
  this.threadNames = names;
@@ -359,6 +363,8 @@ export class CodexAgentWatcher implements AgentWatcher {
359
363
  if (!filename?.endsWith(".jsonl")) return;
360
364
  this.processFile(join(this.sessionsDir, filename));
361
365
  });
362
- } catch {}
366
+ } catch {
367
+ // intentionally ignored: sessions dir watch is best-effort
368
+ }
363
369
  }
364
370
  }
@@ -86,7 +86,9 @@ export class OpenCodeAgentWatcher implements AgentWatcher {
86
86
  }
87
87
  try {
88
88
  this.db?.close();
89
- } catch {}
89
+ } catch {
90
+ // intentionally ignored: best-effort sqlite handle cleanup
91
+ }
90
92
  this.db = null;
91
93
  this.ctx = null;
92
94
  }
@@ -124,7 +126,9 @@ export class OpenCodeAgentWatcher implements AgentWatcher {
124
126
  } catch {
125
127
  try {
126
128
  this.db.close();
127
- } catch {}
129
+ } catch {
130
+ // intentionally ignored: best-effort sqlite handle cleanup
131
+ }
128
132
  this.db = null;
129
133
  return;
130
134
  }
@@ -153,7 +157,9 @@ export class OpenCodeAgentWatcher implements AgentWatcher {
153
157
  for (const pr of partRows) {
154
158
  try {
155
159
  lastParts.push(JSON.parse(pr.data));
156
- } catch {}
160
+ } catch {
161
+ // intentionally ignored: skip malformed part JSON
162
+ }
157
163
  }
158
164
  }
159
165
  } catch {
@@ -164,7 +170,9 @@ export class OpenCodeAgentWatcher implements AgentWatcher {
164
170
  if (lastMsg) {
165
171
  try {
166
172
  lastMsgData = JSON.parse(lastMsg.data);
167
- } catch {}
173
+ } catch {
174
+ // intentionally ignored: leave lastMsgData null on parse failure
175
+ }
168
176
  }
169
177
 
170
178
  const status = determineStatus(lastMsgData, lastParts);
@@ -211,7 +219,9 @@ export class OpenCodeAgentWatcher implements AgentWatcher {
211
219
  for (const pr of partRows) {
212
220
  try {
213
221
  lastParts.push(JSON.parse(pr.data));
214
- } catch {}
222
+ } catch {
223
+ // intentionally ignored: skip malformed part JSON
224
+ }
215
225
  }
216
226
  }
217
227
  } catch {
@@ -222,7 +232,9 @@ export class OpenCodeAgentWatcher implements AgentWatcher {
222
232
  if (lastMsg) {
223
233
  try {
224
234
  lastMsgData = JSON.parse(lastMsg.data);
225
- } catch {}
235
+ } catch {
236
+ // intentionally ignored: leave lastMsgData null on parse failure
237
+ }
226
238
  }
227
239
 
228
240
  const status = determineStatus(lastMsgData, lastParts);
@@ -15,5 +15,7 @@ export function debugLog(category: string, msg: string, data?: Record<string, un
15
15
  const line = `[${ts}] [${category}] ${msg}${extra}\n`;
16
16
  try {
17
17
  appendFileSync(DEBUG_LOG, line);
18
- } catch {}
18
+ } catch {
19
+ // intentionally ignored: debug log file write is best-effort
20
+ }
19
21
  }
@@ -1,6 +1,9 @@
1
1
  import { readFileSync, unlinkSync, writeFileSync, appendFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
+
5
+ import consola from "consola";
6
+
4
7
  import type { MuxProvider } from "../contracts/mux";
5
8
  import { isFullSidebarCapable, isBatchCapable } from "../contracts/mux";
6
9
  import type { AgentEvent } from "../contracts/agent";
@@ -40,7 +43,9 @@ function log(category: string, msg: string, data?: Record<string, unknown>) {
40
43
  const line = `[${ts}] [${category}] ${msg}${extra}\n`;
41
44
  try {
42
45
  appendFileSync(DEBUG_LOG, line);
43
- } catch {}
46
+ } catch {
47
+ // intentionally ignored: debug log file write is best-effort
48
+ }
44
49
  }
45
50
 
46
51
  // shell, getGitInfo, invalidateGitCache imported from ./git-info
@@ -67,7 +72,9 @@ export function startServer(
67
72
  // Clear previous log on server start
68
73
  try {
69
74
  writeFileSync(DEBUG_LOG, "");
70
- } catch {}
75
+ } catch {
76
+ // intentionally ignored: clearing debug log is best-effort
77
+ }
71
78
  log("server", "starting", { providers: allProviders.map((p) => p.name) });
72
79
 
73
80
  // Load initial theme from config
@@ -840,7 +847,9 @@ export function startServer(
840
847
  try {
841
848
  const data = JSON.parse(readFileSync(join(sessionsDir, `${agentPid}.json`), "utf-8"));
842
849
  if (data.sessionId === threadId) return pane.id;
843
- } catch {}
850
+ } catch {
851
+ // intentionally ignored: Claude session file missing or malformed — try next pane
852
+ }
844
853
  }
845
854
  return undefined;
846
855
  }
@@ -870,7 +879,9 @@ export function startServer(
870
879
  } finally {
871
880
  try {
872
881
  db.close();
873
- } catch {}
882
+ } catch {
883
+ // intentionally ignored: best-effort sqlite handle cleanup
884
+ }
874
885
  }
875
886
  return undefined;
876
887
  }
@@ -894,7 +905,9 @@ export function startServer(
894
905
  const logText = readFileSync(pathMatch[1], "utf-8");
895
906
  const match = logText.match(/ses_[A-Za-z0-9]+/);
896
907
  if (match?.[0] === threadId) return pane.id;
897
- } catch {}
908
+ } catch {
909
+ // intentionally ignored: OpenCode log file unreadable — try next pane
910
+ }
898
911
  }
899
912
  return undefined;
900
913
  }
@@ -1189,7 +1202,9 @@ export function startServer(
1189
1202
  continue;
1190
1203
  }
1191
1204
  }
1192
- } catch {}
1205
+ } catch {
1206
+ // intentionally ignored: Claude projects dir missing or unreadable
1207
+ }
1193
1208
  return {};
1194
1209
  }
1195
1210
 
@@ -1216,10 +1231,13 @@ export function startServer(
1216
1231
  .get(`pid:${agentPid}:%`);
1217
1232
  if (row?.thread_id) return { threadId: row.thread_id };
1218
1233
  } catch {
1234
+ // intentionally ignored: Codex sqlite query failed — no thread info available
1219
1235
  } finally {
1220
1236
  try {
1221
1237
  db.close();
1222
- } catch {}
1238
+ } catch {
1239
+ // intentionally ignored: best-effort sqlite handle cleanup
1240
+ }
1223
1241
  }
1224
1242
  return {};
1225
1243
  }
@@ -1498,7 +1516,9 @@ export function startServer(
1498
1516
  if (idleTimer) clearTimeout(idleTimer);
1499
1517
  try {
1500
1518
  unlinkSync(PID_FILE);
1501
- } catch {}
1519
+ } catch {
1520
+ // intentionally ignored: PID file may already be gone during shutdown
1521
+ }
1502
1522
  for (const p of allProviders) p.cleanupHooks();
1503
1523
  }
1504
1524
 
@@ -1542,7 +1562,9 @@ export function startServer(
1542
1562
  const name = body.trim().replace(/^"+|"+$/g, "");
1543
1563
  if (name) handleFocus(name);
1544
1564
  }
1545
- } catch {}
1565
+ } catch {
1566
+ // intentionally ignored: malformed focus body is non-fatal, respond ok
1567
+ }
1546
1568
  return new Response("ok", { status: 200 });
1547
1569
  }
1548
1570
 
@@ -1553,7 +1575,9 @@ export function startServer(
1553
1575
  log("http", "POST /toggle", { ctx });
1554
1576
  toggleSidebar(ctx);
1555
1577
  broadcastState();
1556
- } catch {}
1578
+ } catch {
1579
+ // intentionally ignored: malformed toggle body is non-fatal, respond ok
1580
+ }
1557
1581
  return new Response("ok", { status: 200 });
1558
1582
  }
1559
1583
 
@@ -1573,7 +1597,9 @@ export function startServer(
1573
1597
  const ctx = parseContext(body) ?? undefined;
1574
1598
  log("http", "POST /switch-index", { index, ctx });
1575
1599
  switchToVisibleIndex(index, ctx?.clientTty);
1576
- } catch {}
1600
+ } catch {
1601
+ // intentionally ignored: malformed switch-index body is non-fatal, respond ok
1602
+ }
1577
1603
  return new Response("ok", { status: 200 });
1578
1604
  }
1579
1605
 
@@ -1585,7 +1611,9 @@ export function startServer(
1585
1611
  // Debounce ensure-sidebar during rapid switching — intermediate sessions
1586
1612
  // don't need full sidebar validation immediately.
1587
1613
  debouncedEnsureSidebar(ctx ?? undefined);
1588
- } catch {}
1614
+ } catch {
1615
+ // intentionally ignored: malformed ensure-sidebar body is non-fatal, respond ok
1616
+ }
1589
1617
  return new Response("ok", { status: 200 });
1590
1618
  }
1591
1619
 
@@ -1737,7 +1765,9 @@ export function startServer(
1737
1765
  const cmd = JSON.parse(msg as string) as ClientCommand;
1738
1766
  log("ws", "command", { type: cmd.type });
1739
1767
  handleCommand(cmd, ws);
1740
- } catch {}
1768
+ } catch {
1769
+ // intentionally ignored: malformed websocket command is non-fatal
1770
+ }
1741
1771
  },
1742
1772
  },
1743
1773
  });
@@ -1775,5 +1805,5 @@ export function startServer(
1775
1805
  });
1776
1806
 
1777
1807
  const names = allProviders.map((p) => p.name).join(", ");
1778
- console.log(`agentboard server listening on ${SERVER_HOST}:${SERVER_PORT} (mux: ${names})`);
1808
+ consola.info(`agentboard server listening on ${SERVER_HOST}:${SERVER_PORT} (mux: ${names})`);
1779
1809
  }
@@ -40,7 +40,9 @@ export async function ensureServer(): Promise<void> {
40
40
  // Stale PID file — remove before spawning a new server
41
41
  try {
42
42
  unlinkSync(PID_FILE);
43
- } catch {}
43
+ } catch {
44
+ // intentionally ignored: PID file may already be gone
45
+ }
44
46
  }
45
47
 
46
48
  const proc = Bun.spawn(["tt", "agentboard", "server"], {
@@ -170,7 +170,9 @@ function resolveClaudeCodeJournalInfo(threadId: string): {
170
170
  continue;
171
171
  }
172
172
  }
173
- } catch {}
173
+ } catch {
174
+ // intentionally ignored: Claude projects dir missing or unreadable
175
+ }
174
176
  return {};
175
177
  }
176
178
 
@@ -197,10 +199,13 @@ function resolveCodexPaneInfo(
197
199
  .get(`pid:${agentPid}:%`);
198
200
  if (row?.thread_id) return { threadId: row.thread_id };
199
201
  } catch {
202
+ // intentionally ignored: Codex sqlite query failed — no thread info available
200
203
  } finally {
201
204
  try {
202
205
  db.close();
203
- } catch {}
206
+ } catch {
207
+ // intentionally ignored: best-effort sqlite handle cleanup
208
+ }
204
209
  }
205
210
  return {};
206
211
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tt",
3
- "description": "Core dev workflow commands: interview-me, write-prd, prd-to-issues, tdd, improve-architecture, refine-text, task.",
4
- "version": "0.0.109",
3
+ "description": "Core dev workflow commands and skills: interview-me, write-prd, prd-to-issues, tdd, improve-architecture, refine-text, task, parallel-slots, towles-tool.",
4
+ "version": "0.0.129",
5
5
  "author": {
6
6
  "name": "Chris Towles"
7
7
  }
@@ -10,6 +10,13 @@ Core workflow automation commands for Claude Code.
10
10
  | `/tt:improve` | Explore codebase and suggest improvements |
11
11
  | `/tt:refine` | Fix grammar/spelling in files |
12
12
 
13
+ ## Skills
14
+
15
+ | Skill | Description |
16
+ | ------------------- | --------------------------------------------------------------------------------- |
17
+ | `tt:towles-tool` | `tt` CLI reference: git/gh helpers, journaling, dependency checks. |
18
+ | `tt:parallel-slots` | Fan out parallel Claude Code agents across slot clones of any repo, via `gh` CLI. |
19
+
13
20
  ## Installation
14
21
 
15
22
  ```bash
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: parallel-slots
3
+ description: Use when the user wants to dispatch parallel Claude Code agents across slot clones of a repo, asks to "fan out", "run N in parallel", "use the slots", or wants to coordinate multiple isolated working copies of the same repo. Explains the slot directory layout, when to fan out vs. stay in primary, and the `gh`-driven workflow that ties slots together.
4
+ user_invocable: true
5
+ ---
6
+
7
+ # Parallel slots
8
+
9
+ The slot pattern lets you run independent Claude Code sessions on the same repo without stepping on each other. Mirrors Boris Cherny's "5 terminal tabs, each a separate git checkout" workflow.
10
+
11
+ ## Layout
12
+
13
+ ```
14
+ ~/code/<scope>/<repo>-repos/
15
+ <repo>-primary/ # interactive work
16
+ <repo>-slot-1/ # parallel agent slot
17
+ <repo>-slot-2/
18
+ <repo>-slot-3/
19
+ <repo>-slot-4/
20
+ <repo>-slot-5/
21
+ ```
22
+
23
+ Each slot is a full clone of the same GitHub remote, not a worktree. They check out branches independently. Use the `gh` CLI for all GitHub-side operations (issue → branch, PR create, PR merge, status). If the repo ships a tmux sidebar (e.g. AgentBoard in towles-tool), it watches every slot and surfaces completion via the stop-hook sweep.
24
+
25
+ ## When to fan out
26
+
27
+ Fan out (use slots) when:
28
+
29
+ - Three or more independent tasks would benefit from running simultaneously (e.g. one PR, one bug, one refactor).
30
+ - A task is risky and you want a clean, throwaway slot that won't pollute primary's working tree.
31
+ - You're iterating on the agent harness itself and want to leave primary stable.
32
+
33
+ Stay in primary when:
34
+
35
+ - The work is sequential or all the changes need to land in the same commit.
36
+ - You're reading/exploring; spinning a slot just adds overhead.
37
+
38
+ ## Dispatch flow
39
+
40
+ 1. Pick a free slot (any slot whose sidebar pane is idle).
41
+ 2. `cd` into it and confirm the working tree is clean.
42
+ 3. Branch off from a GitHub issue: `gh issue develop <issue-number> --checkout` — creates a remote branch tied to the issue and switches the slot to it. If there's no issue, name the branch and use `gh pr checkout <pr>` later if you need to hop onto a colleague's PR.
43
+ 4. Hand the task to Claude in that slot — either via the repo's sidebar TUI or by running `tt auto-claude` with a prompt.
44
+ 5. Watch the sidebar pane for completion. The stop-hook prints results back to it.
45
+
46
+ ## Coordination rules
47
+
48
+ - Never run two agents on the _same_ branch in two slots — push/pull races destroy work.
49
+ - Branch names should be unique per slot for the duration of the run.
50
+ - If a slot's working tree is dirty when you arrive, treat it as in-progress work — investigate before resetting.
51
+ - Pre-commit hooks (format + lint + typecheck) run in every slot, so `--no-verify` is forbidden.
52
+
53
+ ## Verifying a slot's output
54
+
55
+ Before merging from a slot, run that repo's verify command (`/verify` in towles-tool) inside it. Don't trust the slot's own self-report; the agent that wrote the change is not the right reviewer.
56
+
57
+ ## Shipping from a slot
58
+
59
+ Open the PR with `gh pr create` (use `--fill` to seed title/body from commits, or pass `--title`/`--body` explicitly). Merge with `gh pr merge --rebase --admin` — the standard merge style.
60
+
61
+ ## Cleanup
62
+
63
+ After a slot's branch is merged: confirm with `gh pr status` that the slot's PR is merged, then prune the local branch. Use `compound-engineering:ce-clean-gone-branches` to bulk-prune across multiple slots in one pass.
64
+
65
+ ## Anti-patterns
66
+
67
+ - Spinning all 5 slots on the same task "for redundancy". You'll spend the time merging conflicts.
68
+ - Treating slots as long-lived workspaces. They are scratch checkouts — keep them transient.
69
+ - Editing files in primary while a slot has them open. Stay in one or the other for any given file.