@towles/tool 0.0.127 → 0.0.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/node_modules/@towles/shared/src/errors.test.ts +33 -0
  2. package/node_modules/@towles/shared/src/errors.ts +14 -0
  3. package/node_modules/@towles/shared/src/index.ts +1 -0
  4. package/package.json +15 -15
  5. package/packages/agentboard/apps/tui/package.json +2 -2
  6. package/packages/agentboard/apps/tui/src/constants.ts +3 -1
  7. package/packages/agentboard/apps/tui/src/index.tsx +3 -1
  8. package/packages/agentboard/apps/tui/src/mux-context.ts +3 -1
  9. package/packages/agentboard/packages/runtime/src/agents/watchers/amp.ts +6 -2
  10. package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.ts +21 -7
  11. package/packages/agentboard/packages/runtime/src/agents/watchers/codex.ts +9 -3
  12. package/packages/agentboard/packages/runtime/src/agents/watchers/opencode.ts +18 -6
  13. package/packages/agentboard/packages/runtime/src/debug.ts +3 -1
  14. package/packages/agentboard/packages/runtime/src/server/index.ts +44 -14
  15. package/packages/agentboard/packages/runtime/src/server/launcher.ts +3 -1
  16. package/packages/agentboard/packages/runtime/src/server/pane-scanner.ts +7 -2
  17. package/packages/core/.claude-plugin/plugin.json +2 -2
  18. package/packages/core/README.md +7 -0
  19. package/packages/core/skills/parallel-slots/SKILL.md +69 -0
  20. package/packages/shared/src/errors.test.ts +33 -0
  21. package/packages/shared/src/errors.ts +14 -0
  22. package/packages/shared/src/index.ts +1 -0
  23. package/src/commands/agentboard.ts +3 -1
  24. package/src/commands/auto-claude/claude-cli.ts +15 -6
  25. package/src/commands/auto-claude/index.ts +21 -21
  26. package/src/commands/auto-claude/logger.ts +3 -0
  27. package/src/commands/auto-claude/pipeline.ts +12 -11
  28. package/src/commands/auto-claude/steps/create-pr.ts +8 -10
  29. package/src/commands/auto-claude/steps/fetch-issues.ts +8 -8
  30. package/src/commands/auto-claude/steps/implement.ts +7 -8
  31. package/src/commands/auto-claude/utils.ts +5 -8
  32. package/src/commands/graph/analyzer.test.ts +3 -1
  33. package/src/commands/graph.test.ts +2 -0
  34. package/src/commands/journal/list.ts +2 -1
  35. package/src/commands/journal/search.ts +3 -3
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AppError } from "./errors";
3
+
4
+ describe("AppError", () => {
5
+ it("carries code and message", () => {
6
+ const err = new AppError("my_code", "boom");
7
+ expect(err.code).toBe("my_code");
8
+ expect(err.message).toBe("boom");
9
+ expect(err).toBeInstanceOf(Error);
10
+ expect(err).toBeInstanceOf(AppError);
11
+ });
12
+
13
+ it("uses the constructor name as the error name", () => {
14
+ class ChildError extends AppError {
15
+ constructor(message: string) {
16
+ super("child_code", message);
17
+ }
18
+ }
19
+ expect(new AppError("c", "m").name).toBe("AppError");
20
+ expect(new ChildError("m").name).toBe("ChildError");
21
+ });
22
+
23
+ it("forwards cause to the native Error.cause", () => {
24
+ const root = new Error("root");
25
+ const err = new AppError("wrapped", "outer", { cause: root });
26
+ expect(err.cause).toBe(root);
27
+ });
28
+
29
+ it("leaves cause undefined when not provided", () => {
30
+ const err = new AppError("nope", "no cause");
31
+ expect(err.cause).toBeUndefined();
32
+ });
33
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared error base class for towles-tool.
3
+ *
4
+ * Uses the native ES2022 `Error.cause` option so stack traces chain properly.
5
+ */
6
+ export class AppError extends Error {
7
+ readonly code: string;
8
+
9
+ constructor(code: string, message: string, options?: { cause?: unknown }) {
10
+ super(message, options);
11
+ this.name = new.target.name;
12
+ this.code = code;
13
+ }
14
+ }
@@ -1,3 +1,4 @@
1
+ export { AppError } from "./errors.js";
1
2
  export { ensureDir, fileExists, readFile, writeFile } from "./fs.js";
2
3
  export { getTerminalColumns, limitText, printWithHexColor } from "./render.js";
3
4
  export { formatDate, generateJournalFilename, getMondayOfWeek, getWeekInfo } from "./date-utils.js";
@@ -199,7 +199,9 @@ function stopServer(): boolean {
199
199
  }
200
200
  try {
201
201
  unlinkSync(PID_FILE);
202
- } catch {}
202
+ } catch {
203
+ // intentionally ignored: PID file may already be gone
204
+ }
203
205
  return true;
204
206
  }
205
207
 
@@ -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 ?? consola;
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
- /* CLAUDE.md not present */
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
- /* prompt file not present */
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 lastError ?? new Error("runClaude failed after all retries");
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 { log, logBanner } from "./utils.js";
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
- consola.error(e instanceof Error ? e.message : String(e));
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
- consola.error("Claude reported an error");
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
- log(`Resetting state for issue-${resetIssue}...`);
145
+ logger.info(`Resetting state for issue-${resetIssue}...`);
146
146
  rmSync(issueDir, { recursive: true, force: true });
147
- log(`Cleaned ${issueDir}`);
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
- log(`Loop mode — interval: ${intervalMs / 60_000}min, limit: ${limit}`);
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
- log(`Sync failed: ${e instanceof Error ? e.message : String(e)}`);
175
+ logger.warn(`Sync failed: ${e instanceof Error ? e.message : String(e)}`);
176
176
  if (loopMode) {
177
- log(`Will retry in ${Math.round(intervalMs / 1000)}s...`);
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
- log("Fetching labeled issues…");
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
- log("No issues to process.");
194
+ logger.info("No issues to process.");
195
195
  } else {
196
- log(`Processing ${contexts.length} issue(s)...\n`);
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
- consola.error(`Pipeline error for ${ctx.repo}#${ctx.number}:`, e);
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
- log(`Completed ${ctx.repo}#${ctx.number} in ${elapsed}s`);
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
- log(`Waiting ${Math.round(waitMs / 1000)}s until next iteration...`);
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
- log("Done.");
220
+ logger.info("Done.");
221
221
  },
222
222
  });
223
223
 
224
224
  async function syncWithRemote(): Promise<void> {
225
225
  const cfg = getConfig();
226
- log("Syncing with remote...");
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
- log(`Warning: on branch "${branch}", switching to ${cfg.mainBranch}...`);
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
- consola.warn(`Working tree has ${files.length} uncommitted change(s):`);
238
+ logger.warn(`Working tree has ${files.length} uncommitted change(s):`);
239
239
  for (const file of files) {
240
- consola.warn(` ${file.trim()}`);
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
- log(`Received ${signal}, shutting down...`);
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(() => {
@@ -0,0 +1,3 @@
1
+ import consola from "consola";
2
+
3
+ export const logger = consola.withTag("auto-claude");
@@ -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
- log(`Pipeline starting for ${ctx.repo}#${ctx.number}: ${ctx.title}`);
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
- log("Saved initial-ramblings.md");
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
- log(`Pipeline paused after "plan" (--until plan)`);
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
- log(`Pipeline paused after "implement" (--until implement)`);
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
- log(`Pipeline paused after "simplify" (--until simplify)`);
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
- log(`Pipeline paused after "review" (--until review)`);
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
- log(`Pipeline complete for ${ctx.repo}#${ctx.number} — ${prUrl}`);
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
- log(
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
- log(`Pipeline stopped at "${stepName}" for ${ctx.repo}#${ctx.number}`);
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
- log("Restored stashed changes");
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
- log(`PR already exists: ${existingUrl}`);
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
- log(`PR created: ${prUrl}`);
69
+ logger.info(`PR created: ${prUrl}`);
72
70
 
73
71
  try {
74
72
  await attachArtifacts(ctx, prUrl, exec);
75
73
  } catch (e) {
76
- consola.warn(`Artifact upload failed (non-blocking): ${e}`);
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
- log("Archiving pipeline artifacts…");
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
- log("Uploading artifacts to GitHub release…");
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
- consola.warn("Could not get artifact download URL — skipping PR comment");
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
- consola.debug(`Failed to parse PR list JSON for branch "${branch}"`);
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 { buildIssueContext, log } from "../utils.js";
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
- log(`Scanning ${cfg.repo} for issues labeled "${cfg.triggerLabel}"...`);
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
- log(`Warning: could not fetch issues from ${cfg.repo}: ${e}`);
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
- log("No issues found.");
39
+ logger.info("No issues found.");
40
40
  return [];
41
41
  }
42
42
 
43
- log(`Found ${issues.length} issue(s).`);
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
- consola.debug(`Could not fetch issue #${issueNumber} from ${cfg.repo}`);
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, log, logStep } from "../utils.js";
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
- log(`Implementation iteration ${i}/${maxIterations}`);
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
- consola.error(`Implement iteration ${i} failed: ${result.result}`);
42
+ logger.error(`Implement iteration ${i} failed: ${result.result}`);
44
43
  return false;
45
44
  }
46
45
 
47
46
  if (fileExists(completedPath)) {
48
- log(`Implementation complete after ${i} iteration(s)`);
47
+ logger.info(`Implementation complete after ${i} iteration(s)`);
49
48
  return true;
50
49
  }
51
50
 
52
- log(`Iteration ${i} finished but completed-summary.md not yet created — tasks remain`);
51
+ logger.warn(`Iteration ${i} finished but completed-summary.md not yet created — tasks remain`);
53
52
  }
54
53
 
55
- consola.error(`Implementation did not complete after ${maxIterations} iterations`);
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
- log("Stashed uncommitted changes");
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
- /* doesn't exist remotely */
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
- consola.error(`${stepName} step failed: ${result.result}`);
195
+ logger.error(`${stepName} step failed: ${result.result}`);
199
196
  return false;
200
197
  }
201
198
 
202
199
  if (!isValid(artifactPath)) {
203
- consola.error(`${stepName} step did not produce expected artifact`);
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
- console.log(renderTable(result));
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
- console.log(colors.bold(colors.cyan(relative)));
243
+ consola.log(colors.bold(colors.cyan(relative)));
244
244
  for (const m of fileMatches) {
245
245
  for (const line of m.context) {
246
- console.log(line);
246
+ consola.log(line);
247
247
  }
248
- console.log("");
248
+ consola.log("");
249
249
  }
250
250
  }
251
251
  } catch (error) {