@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.
- package/node_modules/@towles/shared/src/errors.test.ts +33 -0
- package/node_modules/@towles/shared/src/errors.ts +14 -0
- package/node_modules/@towles/shared/src/index.ts +1 -0
- package/package.json +15 -15
- package/packages/agentboard/apps/tui/package.json +2 -2
- package/packages/agentboard/apps/tui/src/constants.ts +3 -1
- package/packages/agentboard/apps/tui/src/index.tsx +3 -1
- package/packages/agentboard/apps/tui/src/mux-context.ts +3 -1
- package/packages/agentboard/packages/runtime/src/agents/watchers/amp.ts +6 -2
- package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.ts +21 -7
- package/packages/agentboard/packages/runtime/src/agents/watchers/codex.ts +9 -3
- package/packages/agentboard/packages/runtime/src/agents/watchers/opencode.ts +18 -6
- package/packages/agentboard/packages/runtime/src/debug.ts +3 -1
- package/packages/agentboard/packages/runtime/src/server/index.ts +44 -14
- package/packages/agentboard/packages/runtime/src/server/launcher.ts +3 -1
- package/packages/agentboard/packages/runtime/src/server/pane-scanner.ts +7 -2
- package/packages/core/.claude-plugin/plugin.json +2 -2
- package/packages/core/README.md +7 -0
- package/packages/core/skills/parallel-slots/SKILL.md +69 -0
- package/packages/shared/src/errors.test.ts +33 -0
- package/packages/shared/src/errors.ts +14 -0
- package/packages/shared/src/index.ts +1 -0
- package/src/commands/agentboard.ts +3 -1
- package/src/commands/auto-claude/claude-cli.ts +15 -6
- package/src/commands/auto-claude/index.ts +21 -21
- package/src/commands/auto-claude/logger.ts +3 -0
- package/src/commands/auto-claude/pipeline.ts +12 -11
- package/src/commands/auto-claude/steps/create-pr.ts +8 -10
- package/src/commands/auto-claude/steps/fetch-issues.ts +8 -8
- package/src/commands/auto-claude/steps/implement.ts +7 -8
- package/src/commands/auto-claude/utils.ts +5 -8
- package/src/commands/graph/analyzer.test.ts +3 -1
- package/src/commands/graph.test.ts +2 -0
- package/src/commands/journal/list.ts +2 -1
- 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.
|
|
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.
|
|
54
|
-
"@anthropic-ai/sdk": "^0.
|
|
53
|
+
"@anthropic-ai/claude-code": "^2.1.107",
|
|
54
|
+
"@anthropic-ai/sdk": "^0.88.0",
|
|
55
55
|
"@towles/shared": "workspace:*",
|
|
56
|
-
"citty": "^0.
|
|
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.
|
|
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.
|
|
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.
|
|
70
|
-
"@types/node": "^
|
|
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.
|
|
73
|
-
"bumpp": "^
|
|
74
|
-
"oxfmt": "^0.
|
|
75
|
-
"oxlint": "^1.
|
|
76
|
-
"simple-git-hooks": "^2.13.
|
|
77
|
-
"typescript": "^
|
|
78
|
-
"vitest": "^4.
|
|
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.
|
|
16
|
-
"@opentui/solid": "^0.1.
|
|
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
|
-
|
|
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.
|
|
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
|
}
|
package/packages/core/README.md
CHANGED
|
@@ -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.
|