@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";
|
|
@@ -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) {
|