@towles/tool 0.0.126 → 0.0.128
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/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.128",
|
|
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
|
}
|
|
@@ -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";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import consola from "consola";
|
|
5
4
|
import pc from "picocolors";
|
|
6
5
|
|
|
7
|
-
import { readFile } from "@towles/shared";
|
|
6
|
+
import { AppError, readFile } from "@towles/shared";
|
|
8
7
|
import { getConfig } from "./config.js";
|
|
8
|
+
import { logger } from "./logger.js";
|
|
9
9
|
import { sleep } from "./shell.js";
|
|
10
10
|
import { spawnClaude as defaultSpawnClaude } from "./spawn-claude.js";
|
|
11
11
|
import type { SpawnClaudeFn } from "./spawn-claude.js";
|
|
@@ -28,6 +28,13 @@ export interface ClaudeLogger {
|
|
|
28
28
|
log: (...args: unknown[]) => void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Raised when the Claude CLI process fails after all retries. */
|
|
32
|
+
export class ClaudeProcessError extends AppError {
|
|
33
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
34
|
+
super("claude_process_failed", message, options);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
const PROCESS_RETRIES = 3;
|
|
32
39
|
const PROCESS_RETRY_DELAY_MS = 5_000;
|
|
33
40
|
|
|
@@ -39,7 +46,7 @@ export async function runClaude(opts: {
|
|
|
39
46
|
}): Promise<ClaudeResult> {
|
|
40
47
|
const cfg = getConfig();
|
|
41
48
|
const spawnFn = opts.spawnFn ?? defaultSpawnClaude;
|
|
42
|
-
const log = opts.logger ??
|
|
49
|
+
const log = opts.logger ?? logger;
|
|
43
50
|
const args = [
|
|
44
51
|
"-p",
|
|
45
52
|
"--output-format",
|
|
@@ -61,7 +68,7 @@ export async function runClaude(opts: {
|
|
|
61
68
|
`\n${pc.bold(pc.cyan("── System Prompt (CLAUDE.md) ──"))}\n${pc.dim(systemPrompt.trimEnd())}\n`,
|
|
62
69
|
);
|
|
63
70
|
} catch {
|
|
64
|
-
|
|
71
|
+
// intentionally ignored: CLAUDE.md is optional
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
try {
|
|
@@ -70,7 +77,7 @@ export async function runClaude(opts: {
|
|
|
70
77
|
`\n${pc.bold(pc.cyan(`── Prompt (${opts.promptFile}) ──`))}\n${pc.dim(promptContent.trimEnd())}\n`,
|
|
71
78
|
);
|
|
72
79
|
} catch {
|
|
73
|
-
|
|
80
|
+
// intentionally ignored: prompt file preview is optional
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
let lastError: Error | undefined;
|
|
@@ -92,7 +99,9 @@ export async function runClaude(opts: {
|
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
|
-
throw
|
|
102
|
+
throw new ClaudeProcessError(`runClaude failed after ${PROCESS_RETRIES} attempts`, {
|
|
103
|
+
cause: lastError,
|
|
104
|
+
});
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
function logActivityEvent(event: ReturnType<typeof parseStreamLine>, log: ClaudeLogger): void {
|
|
@@ -3,7 +3,6 @@ import { join } from "node:path";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
|
-
import consola from "consola";
|
|
7
6
|
|
|
8
7
|
import { debugArg } from "../shared.js";
|
|
9
8
|
import { printExplain, printStepTemplate } from "./explain.js";
|
|
@@ -12,8 +11,9 @@ import { fetchIssue, fetchIssues } from "./steps/fetch-issues.js";
|
|
|
12
11
|
import { getConfig, initConfig } from "./config.js";
|
|
13
12
|
import { git } from "@towles/shared";
|
|
14
13
|
import { runClaude } from "./claude-cli.js";
|
|
14
|
+
import { logger } from "./logger.js";
|
|
15
15
|
import { sleep } from "./shell.js";
|
|
16
|
-
import {
|
|
16
|
+
import { logBanner } from "./utils.js";
|
|
17
17
|
import type { IssueContext } from "./utils.js";
|
|
18
18
|
import type { StepName } from "./prompt-templates/index.js";
|
|
19
19
|
|
|
@@ -104,7 +104,7 @@ export default defineCommand({
|
|
|
104
104
|
try {
|
|
105
105
|
printStepTemplate(args["step-template"] as string);
|
|
106
106
|
} catch (e) {
|
|
107
|
-
|
|
107
|
+
logger.error(e instanceof Error ? e.message : String(e));
|
|
108
108
|
process.exit(1);
|
|
109
109
|
}
|
|
110
110
|
return;
|
|
@@ -125,7 +125,7 @@ export default defineCommand({
|
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
if (result.is_error) {
|
|
128
|
-
|
|
128
|
+
logger.error("Claude reported an error");
|
|
129
129
|
process.exit(1);
|
|
130
130
|
}
|
|
131
131
|
return;
|
|
@@ -142,9 +142,9 @@ export default defineCommand({
|
|
|
142
142
|
const resetIssue = args.reset ? Number(args.reset) : undefined;
|
|
143
143
|
if (resetIssue) {
|
|
144
144
|
const issueDir = join(process.cwd(), `.auto-claude/issue-${resetIssue}`);
|
|
145
|
-
|
|
145
|
+
logger.info(`Resetting state for issue-${resetIssue}...`);
|
|
146
146
|
rmSync(issueDir, { recursive: true, force: true });
|
|
147
|
-
|
|
147
|
+
logger.info(`Cleaned ${issueDir}`);
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -155,7 +155,7 @@ export default defineCommand({
|
|
|
155
155
|
|
|
156
156
|
if (loopMode) {
|
|
157
157
|
registerShutdownHandlers();
|
|
158
|
-
|
|
158
|
+
logger.info(`Loop mode — interval: ${intervalMs / 60_000}min, limit: ${limit}`);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
const issueNumber = args.issue ? Number(args.issue) : undefined;
|
|
@@ -172,16 +172,16 @@ export default defineCommand({
|
|
|
172
172
|
try {
|
|
173
173
|
await syncWithRemote();
|
|
174
174
|
} catch (e) {
|
|
175
|
-
|
|
175
|
+
logger.warn(`Sync failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
176
176
|
if (loopMode) {
|
|
177
|
-
|
|
177
|
+
logger.info(`Will retry in ${Math.round(intervalMs / 1000)}s...`);
|
|
178
178
|
await sleep(intervalMs);
|
|
179
179
|
continue;
|
|
180
180
|
}
|
|
181
181
|
throw e;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
logger.info("Fetching labeled issues…");
|
|
185
185
|
let contexts: IssueContext[];
|
|
186
186
|
if (issueNumber) {
|
|
187
187
|
const ctx = await fetchIssue(issueNumber);
|
|
@@ -191,19 +191,19 @@ export default defineCommand({
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
if (contexts.length === 0) {
|
|
194
|
-
|
|
194
|
+
logger.info("No issues to process.");
|
|
195
195
|
} else {
|
|
196
|
-
|
|
196
|
+
logger.info(`Processing ${contexts.length} issue(s)...\n`);
|
|
197
197
|
|
|
198
198
|
for (const ctx of contexts) {
|
|
199
199
|
const issueStart = Date.now();
|
|
200
200
|
try {
|
|
201
201
|
await runPipeline(ctx, untilStep);
|
|
202
202
|
} catch (e) {
|
|
203
|
-
|
|
203
|
+
logger.error(`Pipeline error for ${ctx.repo}#${ctx.number}:`, e);
|
|
204
204
|
} finally {
|
|
205
205
|
const elapsed = ((Date.now() - issueStart) / 1000).toFixed(1);
|
|
206
|
-
|
|
206
|
+
logger.info(`Completed ${ctx.repo}#${ctx.number} in ${elapsed}s`);
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
}
|
|
@@ -211,23 +211,23 @@ export default defineCommand({
|
|
|
211
211
|
if (loopMode) {
|
|
212
212
|
const waitMs = Math.max(0, intervalMs - (Date.now() - iterationStart));
|
|
213
213
|
if (waitMs > 0) {
|
|
214
|
-
|
|
214
|
+
logger.info(`Waiting ${Math.round(waitMs / 1000)}s until next iteration...`);
|
|
215
215
|
await sleep(waitMs);
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
} while (loopMode);
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
logger.info("Done.");
|
|
221
221
|
},
|
|
222
222
|
});
|
|
223
223
|
|
|
224
224
|
async function syncWithRemote(): Promise<void> {
|
|
225
225
|
const cfg = getConfig();
|
|
226
|
-
|
|
226
|
+
logger.info("Syncing with remote...");
|
|
227
227
|
await git(["fetch", "--all", "--prune"]);
|
|
228
228
|
const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
229
229
|
if (branch !== cfg.mainBranch) {
|
|
230
|
-
|
|
230
|
+
logger.warn(`On branch "${branch}", switching to ${cfg.mainBranch}...`);
|
|
231
231
|
await git(["checkout", cfg.mainBranch]).catch(() => {
|
|
232
232
|
// Best-effort checkout — may fail if working tree is dirty
|
|
233
233
|
});
|
|
@@ -235,9 +235,9 @@ async function syncWithRemote(): Promise<void> {
|
|
|
235
235
|
const status = await git(["status", "--porcelain"]);
|
|
236
236
|
if (status.length > 0) {
|
|
237
237
|
const files = status.trim().split("\n");
|
|
238
|
-
|
|
238
|
+
logger.warn(`Working tree has ${files.length} uncommitted change(s):`);
|
|
239
239
|
for (const file of files) {
|
|
240
|
-
|
|
240
|
+
logger.warn(` ${file.trim()}`);
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
await git(["pull", cfg.remote, cfg.mainBranch]);
|
|
@@ -246,7 +246,7 @@ async function syncWithRemote(): Promise<void> {
|
|
|
246
246
|
function registerShutdownHandlers(): void {
|
|
247
247
|
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
248
248
|
process.on(signal, () => {
|
|
249
|
-
|
|
249
|
+
logger.info(`Received ${signal}, shutting down...`);
|
|
250
250
|
setTimeout(() => process.exit(1), 5_000).unref();
|
|
251
251
|
git(["checkout", getConfig().mainBranch])
|
|
252
252
|
.catch(() => {
|
|
@@ -9,8 +9,9 @@ import { stepImplement } from "./steps/implement.js";
|
|
|
9
9
|
import { stepPlan, stepReview, stepSimplify } from "./steps/simple-steps.js";
|
|
10
10
|
import { LABELS, ensureLabelsExist, removeLabel, setLabel } from "./labels.js";
|
|
11
11
|
import type { ExecSafeFn } from "./labels.js";
|
|
12
|
+
import { logger } from "./logger.js";
|
|
13
|
+
|
|
12
14
|
import { ensureDir, execSafe, fileExists, ghRaw, git, readFile, writeFile } from "@towles/shared";
|
|
13
|
-
import { log } from "./utils.js";
|
|
14
15
|
import type { IssueContext } from "./utils.js";
|
|
15
16
|
import type { SpawnClaudeFn } from "./spawn-claude.js";
|
|
16
17
|
|
|
@@ -29,14 +30,14 @@ export async function runPipeline(
|
|
|
29
30
|
const cfg = getConfig();
|
|
30
31
|
const exec = deps?.exec;
|
|
31
32
|
const spawnFn = deps?.spawnFn;
|
|
32
|
-
|
|
33
|
+
logger.info(`Pipeline starting for ${ctx.repo}#${ctx.number}: ${ctx.title}`);
|
|
33
34
|
|
|
34
35
|
ensureDir(ctx.issueDir);
|
|
35
36
|
const ramblingsPath = join(ctx.issueDir, ARTIFACTS.initialRamblings);
|
|
36
37
|
if (!fileExists(ramblingsPath)) {
|
|
37
38
|
const content = `# ${ctx.title}\n\n> ${ctx.repo}#${ctx.number}\n\n${ctx.body ?? ""}`;
|
|
38
39
|
writeFile(ramblingsPath, content);
|
|
39
|
-
|
|
40
|
+
logger.info("Saved initial-ramblings.md");
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// Label management
|
|
@@ -51,7 +52,7 @@ export async function runPipeline(
|
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
if (untilStep === "plan") {
|
|
54
|
-
|
|
55
|
+
logger.info(`Pipeline paused after "plan" (--until plan)`);
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -71,7 +72,7 @@ export async function runPipeline(
|
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
73
74
|
if (untilStep === "implement") {
|
|
74
|
-
|
|
75
|
+
logger.info(`Pipeline paused after "implement" (--until implement)`);
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -81,7 +82,7 @@ export async function runPipeline(
|
|
|
81
82
|
return;
|
|
82
83
|
}
|
|
83
84
|
if (untilStep === "simplify") {
|
|
84
|
-
|
|
85
|
+
logger.info(`Pipeline paused after "simplify" (--until simplify)`);
|
|
85
86
|
return;
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -91,7 +92,7 @@ export async function runPipeline(
|
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
if (untilStep === "review") {
|
|
94
|
-
|
|
95
|
+
logger.info(`Pipeline paused after "review" (--until review)`);
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -101,13 +102,13 @@ export async function runPipeline(
|
|
|
101
102
|
await removeLabel(ctx.repo, ctx.number, LABELS.inProgress, exec);
|
|
102
103
|
await setLabel(ctx.repo, ctx.number, LABELS.success, exec);
|
|
103
104
|
await setLabel(ctx.repo, ctx.number, LABELS.review, exec);
|
|
104
|
-
|
|
105
|
+
logger.info(`Pipeline complete for ${ctx.repo}#${ctx.number} — ${prUrl}`);
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// Review failed
|
|
109
110
|
if (attempt < maxRetries) {
|
|
110
|
-
|
|
111
|
+
logger.warn(
|
|
111
112
|
`Review did not pass (attempt ${attempt + 1}/${maxRetries + 1}), retrying implement→simplify→review…`,
|
|
112
113
|
);
|
|
113
114
|
}
|
|
@@ -151,7 +152,7 @@ async function handleFailure(
|
|
|
151
152
|
exec,
|
|
152
153
|
);
|
|
153
154
|
}
|
|
154
|
-
|
|
155
|
+
logger.info(`Pipeline stopped at "${stepName}" for ${ctx.repo}#${ctx.number}`);
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
async function checkoutMain(): Promise<void> {
|
|
@@ -165,7 +166,7 @@ async function checkoutMain(): Promise<void> {
|
|
|
165
166
|
const idx = lines.findIndex((l) => l.includes("auto-claude: before switching to"));
|
|
166
167
|
if (idx >= 0) {
|
|
167
168
|
await execSafe("git", ["stash", "pop", `stash@{${idx}}`]);
|
|
168
|
-
|
|
169
|
+
logger.info("Restored stashed changes");
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
|
|
3
|
-
import consola from "consola";
|
|
4
|
-
|
|
5
3
|
import { execSafe, fileExists, ghRaw, git, readFile, writeFile } from "@towles/shared";
|
|
6
4
|
import { getConfig } from "../config.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
7
6
|
import { ARTIFACTS } from "../prompt-templates/index.js";
|
|
8
|
-
import { log } from "../utils.js";
|
|
9
7
|
import type { IssueContext } from "../utils.js";
|
|
10
8
|
import type { ExecSafeFn } from "../labels.js";
|
|
11
9
|
|
|
@@ -14,7 +12,7 @@ export async function createPr(ctx: IssueContext, exec?: ExecSafeFn): Promise<st
|
|
|
14
12
|
|
|
15
13
|
const existingUrl = await getExistingPrUrl(ctx.branch, exec);
|
|
16
14
|
if (existingUrl) {
|
|
17
|
-
|
|
15
|
+
logger.info(`PR already exists: ${existingUrl}`);
|
|
18
16
|
return existingUrl;
|
|
19
17
|
}
|
|
20
18
|
|
|
@@ -68,12 +66,12 @@ export async function createPr(ctx: IssueContext, exec?: ExecSafeFn): Promise<st
|
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
writeFile(join(ctx.issueDir, ARTIFACTS.prUrl), prUrl);
|
|
71
|
-
|
|
69
|
+
logger.info(`PR created: ${prUrl}`);
|
|
72
70
|
|
|
73
71
|
try {
|
|
74
72
|
await attachArtifacts(ctx, prUrl, exec);
|
|
75
73
|
} catch (e) {
|
|
76
|
-
|
|
74
|
+
logger.warn(`Artifact upload failed (non-blocking): ${e}`);
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
return prUrl;
|
|
@@ -84,7 +82,7 @@ async function attachArtifacts(ctx: IssueContext, prUrl: string, exec?: ExecSafe
|
|
|
84
82
|
const tag = `ac-issue-${ctx.number}`;
|
|
85
83
|
const cfg = getConfig();
|
|
86
84
|
|
|
87
|
-
|
|
85
|
+
logger.info("Archiving pipeline artifacts…");
|
|
88
86
|
await execSafe("tar", [
|
|
89
87
|
"czf",
|
|
90
88
|
archivePath,
|
|
@@ -95,7 +93,7 @@ async function attachArtifacts(ctx: IssueContext, prUrl: string, exec?: ExecSafe
|
|
|
95
93
|
".",
|
|
96
94
|
]);
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
logger.info("Uploading artifacts to GitHub release…");
|
|
99
97
|
await ghRaw(["release", "delete", tag, "--yes", "--repo", cfg.repo], exec);
|
|
100
98
|
|
|
101
99
|
await ghRaw(
|
|
@@ -119,7 +117,7 @@ async function attachArtifacts(ctx: IssueContext, prUrl: string, exec?: ExecSafe
|
|
|
119
117
|
);
|
|
120
118
|
|
|
121
119
|
if (!assetUrl) {
|
|
122
|
-
|
|
120
|
+
logger.warn("Could not get artifact download URL — skipping PR comment");
|
|
123
121
|
return;
|
|
124
122
|
}
|
|
125
123
|
|
|
@@ -154,7 +152,7 @@ async function getExistingPrUrl(branch: string, exec?: ExecSafeFn): Promise<stri
|
|
|
154
152
|
const prs = JSON.parse(out) as Array<{ url: string }>;
|
|
155
153
|
return prs.length > 0 ? prs[0].url : null;
|
|
156
154
|
} catch {
|
|
157
|
-
|
|
155
|
+
logger.debug(`Failed to parse PR list JSON for branch "${branch}"`);
|
|
158
156
|
return null;
|
|
159
157
|
}
|
|
160
158
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import consola from "consola";
|
|
2
|
-
import { getConfig } from "../config.js";
|
|
3
1
|
import { gh } from "@towles/shared";
|
|
4
|
-
import {
|
|
2
|
+
import { getConfig } from "../config.js";
|
|
3
|
+
import { logger } from "../logger.js";
|
|
4
|
+
import { buildIssueContext } from "../utils.js";
|
|
5
5
|
import type { IssueContext } from "../utils.js";
|
|
6
6
|
|
|
7
7
|
interface GhIssue {
|
|
@@ -14,7 +14,7 @@ interface GhIssue {
|
|
|
14
14
|
export async function fetchIssues(limit?: number): Promise<IssueContext[]> {
|
|
15
15
|
const cfg = getConfig();
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
logger.info(`Scanning ${cfg.repo} for issues labeled "${cfg.triggerLabel}"...`);
|
|
18
18
|
|
|
19
19
|
let issues: GhIssue[];
|
|
20
20
|
try {
|
|
@@ -31,16 +31,16 @@ export async function fetchIssues(limit?: number): Promise<IssueContext[]> {
|
|
|
31
31
|
"number,title,body,labels",
|
|
32
32
|
]);
|
|
33
33
|
} catch (e) {
|
|
34
|
-
|
|
34
|
+
logger.warn(`Could not fetch issues from ${cfg.repo}: ${e}`);
|
|
35
35
|
return [];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (issues.length === 0) {
|
|
39
|
-
|
|
39
|
+
logger.info("No issues found.");
|
|
40
40
|
return [];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
logger.info(`Found ${issues.length} issue(s).`);
|
|
44
44
|
|
|
45
45
|
const selected = limit != null ? issues.slice(0, limit) : issues;
|
|
46
46
|
return selected.map((issue) => buildIssueContext(issue, cfg.repo, cfg.scopePath));
|
|
@@ -61,7 +61,7 @@ export async function fetchIssue(issueNumber: number): Promise<IssueContext | un
|
|
|
61
61
|
]);
|
|
62
62
|
return buildIssueContext(issue, cfg.repo, cfg.scopePath);
|
|
63
63
|
} catch {
|
|
64
|
-
|
|
64
|
+
logger.debug(`Could not fetch issue #${issueNumber} from ${cfg.repo}`);
|
|
65
65
|
return undefined;
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
|
|
3
|
-
import consola from "consola";
|
|
4
|
-
|
|
5
3
|
import { getConfig } from "../config.js";
|
|
6
4
|
import { ARTIFACTS, STEP_LABELS, TEMPLATES } from "../prompt-templates/index.js";
|
|
7
5
|
import { fileExists, git, readFile } from "@towles/shared";
|
|
8
6
|
import { runClaude } from "../claude-cli.js";
|
|
7
|
+
import { logger } from "../logger.js";
|
|
9
8
|
import { resolveTemplate } from "../templates.js";
|
|
10
|
-
import { buildTokens,
|
|
9
|
+
import { buildTokens, logStep } from "../utils.js";
|
|
11
10
|
import type { IssueContext } from "../utils.js";
|
|
12
11
|
import type { SpawnClaudeFn } from "../spawn-claude.js";
|
|
13
12
|
|
|
@@ -28,7 +27,7 @@ export async function stepImplement(ctx: IssueContext, spawnFn?: SpawnClaudeFn):
|
|
|
28
27
|
const reviewFeedback = fileExists(reviewPath) ? readFile(reviewPath) : "";
|
|
29
28
|
|
|
30
29
|
for (let i = 1; i <= maxIterations; i++) {
|
|
31
|
-
|
|
30
|
+
logger.info(`Implementation iteration ${i}/${maxIterations}`);
|
|
32
31
|
|
|
33
32
|
const tokens = buildTokens(ctx, { REVIEW_FEEDBACK: reviewFeedback });
|
|
34
33
|
const promptFile = resolveTemplate(TEMPLATES.implement, tokens, ctx.issueDir);
|
|
@@ -40,18 +39,18 @@ export async function stepImplement(ctx: IssueContext, spawnFn?: SpawnClaudeFn):
|
|
|
40
39
|
});
|
|
41
40
|
|
|
42
41
|
if (result.is_error) {
|
|
43
|
-
|
|
42
|
+
logger.error(`Implement iteration ${i} failed: ${result.result}`);
|
|
44
43
|
return false;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
if (fileExists(completedPath)) {
|
|
48
|
-
|
|
47
|
+
logger.info(`Implementation complete after ${i} iteration(s)`);
|
|
49
48
|
return true;
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
logger.warn(`Iteration ${i} finished but completed-summary.md not yet created — tasks remain`);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
logger.error(`Implementation did not complete after ${maxIterations} iterations`);
|
|
56
55
|
return false;
|
|
57
56
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "@towles/shared";
|
|
15
15
|
import { runClaude } from "./claude-cli.js";
|
|
16
16
|
import { getConfig } from "./config.js";
|
|
17
|
+
import { logger } from "./logger.js";
|
|
17
18
|
import { ARTIFACTS } from "./prompt-templates/index.js";
|
|
18
19
|
import { resolveTemplate } from "./templates.js";
|
|
19
20
|
|
|
@@ -100,10 +101,6 @@ function findNthBlankLine(lines: string[], n: number): number {
|
|
|
100
101
|
|
|
101
102
|
// ── Logging ──
|
|
102
103
|
|
|
103
|
-
export function log(msg: string): void {
|
|
104
|
-
consola.info(`[auto-claude] ${msg}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
104
|
export function logBanner(label: string, width = 60): void {
|
|
108
105
|
const inner = ` ${label} `;
|
|
109
106
|
const totalDashes = Math.max(0, width - inner.length - 2);
|
|
@@ -131,7 +128,7 @@ export async function ensureBranch(branch: string): Promise<void> {
|
|
|
131
128
|
const hadDirtyTree = status.ok && status.stdout.length > 0;
|
|
132
129
|
if (hadDirtyTree) {
|
|
133
130
|
await git(["stash", "push", "-m", `auto-claude: before switching to ${branch}`]);
|
|
134
|
-
|
|
131
|
+
logger.info("Stashed uncommitted changes");
|
|
135
132
|
}
|
|
136
133
|
|
|
137
134
|
// Check if branch exists locally (rev-parse is reliable, no output parsing)
|
|
@@ -147,7 +144,7 @@ export async function ensureBranch(branch: string): Promise<void> {
|
|
|
147
144
|
await git(["checkout", branch]);
|
|
148
145
|
return;
|
|
149
146
|
} catch {
|
|
150
|
-
|
|
147
|
+
// intentionally ignored: branch doesn't exist remotely, fall through to create it
|
|
151
148
|
}
|
|
152
149
|
|
|
153
150
|
// Create new branch from main
|
|
@@ -195,12 +192,12 @@ export async function runStepWithArtifact(opts: StepRunnerOptions): Promise<bool
|
|
|
195
192
|
});
|
|
196
193
|
|
|
197
194
|
if (result.is_error) {
|
|
198
|
-
|
|
195
|
+
logger.error(`${stepName} step failed: ${result.result}`);
|
|
199
196
|
return false;
|
|
200
197
|
}
|
|
201
198
|
|
|
202
199
|
if (!isValid(artifactPath)) {
|
|
203
|
-
|
|
200
|
+
logger.error(`${stepName} step did not produce expected artifact`);
|
|
204
201
|
return false;
|
|
205
202
|
}
|
|
206
203
|
|
|
@@ -18,7 +18,7 @@ function textBlock(text: string): ContentBlock {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function toolUseBlock(name: string, input: Record<string, unknown>): ContentBlock {
|
|
21
|
-
return { type: "tool_use" as const, id: "tool-stub", name, input };
|
|
21
|
+
return { type: "tool_use" as const, id: "tool-stub", name, input, caller: { type: "direct" } };
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function makeUsage(overrides: Partial<Usage> = {}): Usage {
|
|
@@ -27,6 +27,8 @@ function makeUsage(overrides: Partial<Usage> = {}): Usage {
|
|
|
27
27
|
output_tokens: 0,
|
|
28
28
|
cache_read_input_tokens: null,
|
|
29
29
|
cache_creation_input_tokens: null,
|
|
30
|
+
cache_creation: null,
|
|
31
|
+
inference_geo: null,
|
|
30
32
|
server_tool_use: null,
|
|
31
33
|
service_tier: null,
|
|
32
34
|
...overrides,
|
|
@@ -11,6 +11,8 @@ function makeUsage(overrides: Partial<Usage> = {}): Usage {
|
|
|
11
11
|
output_tokens: 0,
|
|
12
12
|
cache_read_input_tokens: null,
|
|
13
13
|
cache_creation_input_tokens: null,
|
|
14
|
+
cache_creation: null,
|
|
15
|
+
inference_geo: null,
|
|
14
16
|
server_tool_use: null,
|
|
15
17
|
service_tier: null,
|
|
16
18
|
...overrides,
|
|
@@ -33,6 +33,7 @@ export function collectJournalEntries(files: string[], baseDir: string): Journal
|
|
|
33
33
|
try {
|
|
34
34
|
size = statSync(filePath).size;
|
|
35
35
|
} catch {
|
|
36
|
+
// intentionally ignored: skip files that can't be stat'd (deleted, permission)
|
|
36
37
|
continue;
|
|
37
38
|
}
|
|
38
39
|
entries.push({
|
|
@@ -204,7 +205,7 @@ export default defineCommand({
|
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
consola.info(`Showing ${colors.green(String(result.length))} journal entries:\n`);
|
|
207
|
-
|
|
208
|
+
consola.log(renderTable(result));
|
|
208
209
|
} catch (error) {
|
|
209
210
|
consola.error("Failed to list journal entries:", error);
|
|
210
211
|
process.exit(1);
|
|
@@ -240,12 +240,12 @@ export default defineCommand({
|
|
|
240
240
|
|
|
241
241
|
for (const [filePath, fileMatches] of byFile) {
|
|
242
242
|
const relative = path.relative(baseFolder, filePath);
|
|
243
|
-
|
|
243
|
+
consola.log(colors.bold(colors.cyan(relative)));
|
|
244
244
|
for (const m of fileMatches) {
|
|
245
245
|
for (const line of m.context) {
|
|
246
|
-
|
|
246
|
+
consola.log(line);
|
|
247
247
|
}
|
|
248
|
-
|
|
248
|
+
consola.log("");
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
} catch (error) {
|