@towles/tool 0.0.61 → 0.0.63

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 (83) hide show
  1. package/package.json +50 -57
  2. package/src/commands/agentboard.ts +176 -0
  3. package/src/commands/{auto-claude.ts → auto-claude/index.ts} +18 -28
  4. package/src/commands/auto-claude/list.ts +114 -0
  5. package/src/commands/auto-claude/retry.test.ts +138 -0
  6. package/src/commands/auto-claude/retry.ts +139 -0
  7. package/src/commands/auto-claude/status.test.ts +147 -0
  8. package/src/commands/auto-claude/status.ts +123 -0
  9. package/src/commands/base.ts +7 -2
  10. package/src/commands/config.ts +5 -7
  11. package/src/commands/doctor.ts +111 -12
  12. package/src/commands/gh/branch.ts +4 -4
  13. package/src/commands/gh/pr.ts +1 -0
  14. package/src/commands/graph/index.ts +169 -0
  15. package/src/commands/graph.test.ts +1 -1
  16. package/src/commands/install.ts +40 -68
  17. package/src/commands/journal/daily-notes.ts +3 -3
  18. package/src/commands/journal/meeting.ts +3 -3
  19. package/src/commands/journal/note.ts +3 -3
  20. package/src/lib/auto-claude/claude-cli.ts +183 -0
  21. package/src/lib/auto-claude/config.test.ts +6 -8
  22. package/src/lib/auto-claude/config.ts +3 -4
  23. package/src/lib/auto-claude/index.ts +2 -3
  24. package/src/lib/auto-claude/labels.test.ts +85 -0
  25. package/src/lib/auto-claude/labels.ts +42 -0
  26. package/src/lib/auto-claude/pipeline-execution.test.ts +129 -33
  27. package/src/lib/auto-claude/pipeline.test.ts +2 -2
  28. package/src/lib/auto-claude/pipeline.ts +120 -36
  29. package/src/lib/auto-claude/prompt-templates/01_plan.prompt.md +68 -0
  30. package/src/lib/auto-claude/prompt-templates/{05_implement.prompt.md → 02_implement.prompt.md} +3 -2
  31. package/src/lib/auto-claude/prompt-templates/03_simplify.prompt.md +52 -0
  32. package/src/lib/auto-claude/prompt-templates/{06_review.prompt.md → 04_review.prompt.md} +29 -6
  33. package/src/lib/auto-claude/prompt-templates/index.test.ts +9 -42
  34. package/src/lib/auto-claude/prompt-templates/index.ts +13 -28
  35. package/src/lib/auto-claude/run-claude.test.ts +48 -68
  36. package/src/lib/auto-claude/shell.ts +6 -0
  37. package/src/lib/auto-claude/steps/create-pr.ts +89 -25
  38. package/src/lib/auto-claude/steps/fetch-issues.ts +4 -1
  39. package/src/lib/auto-claude/steps/implement.ts +9 -16
  40. package/src/lib/auto-claude/steps/simple-steps.ts +34 -0
  41. package/src/lib/auto-claude/steps/steps.test.ts +68 -63
  42. package/src/lib/auto-claude/templates.test.ts +91 -0
  43. package/src/lib/auto-claude/templates.ts +34 -0
  44. package/src/lib/auto-claude/test-helpers.ts +2 -1
  45. package/src/lib/auto-claude/utils-execution.test.ts +9 -57
  46. package/src/lib/auto-claude/utils.test.ts +5 -9
  47. package/src/lib/auto-claude/utils.ts +27 -253
  48. package/src/lib/graph/analyzer.test.ts +451 -0
  49. package/src/lib/graph/analyzer.ts +165 -0
  50. package/src/lib/graph/index.ts +24 -0
  51. package/src/lib/graph/labels.ts +87 -0
  52. package/src/lib/graph/parser.test.ts +150 -0
  53. package/src/lib/graph/parser.ts +65 -0
  54. package/src/lib/graph/render.ts +25 -0
  55. package/src/lib/graph/server.ts +70 -0
  56. package/src/lib/graph/sessions.ts +104 -0
  57. package/src/lib/graph/tools.ts +90 -0
  58. package/src/lib/graph/treemap.ts +211 -0
  59. package/src/lib/graph/types.ts +80 -0
  60. package/src/lib/install/claude-settings.ts +64 -0
  61. package/src/lib/journal/editor.ts +33 -0
  62. package/src/lib/journal/fs.ts +13 -0
  63. package/src/lib/journal/index.ts +11 -0
  64. package/src/lib/journal/paths.ts +106 -0
  65. package/src/lib/journal/{utils.ts → templates.ts} +3 -151
  66. package/src/utils/fs.ts +19 -0
  67. package/src/utils/git/exec.ts +18 -0
  68. package/src/utils/git/gh-cli-wrapper.test.ts +47 -8
  69. package/src/utils/git/gh-cli-wrapper.ts +31 -19
  70. package/src/utils/render.ts +3 -1
  71. package/src/commands/graph.ts +0 -970
  72. package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +0 -21
  73. package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +0 -27
  74. package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +0 -15
  75. package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +0 -35
  76. package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +0 -30
  77. package/src/lib/auto-claude/steps/plan-annotations.ts +0 -54
  78. package/src/lib/auto-claude/steps/plan-implementation.ts +0 -14
  79. package/src/lib/auto-claude/steps/plan.ts +0 -14
  80. package/src/lib/auto-claude/steps/refresh.ts +0 -114
  81. package/src/lib/auto-claude/steps/remove-label.ts +0 -22
  82. package/src/lib/auto-claude/steps/research.ts +0 -21
  83. package/src/lib/auto-claude/steps/review.ts +0 -14
@@ -1,251 +1,19 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join, relative } from "node:path";
3
- import { createInterface } from "node:readline";
4
- import { fileURLToPath } from "node:url";
1
+ import { join } from "node:path";
5
2
 
6
3
  import consola from "consola";
7
4
  import pc from "picocolors";
8
- import { x } from "tinyexec";
9
5
 
6
+ import { ensureDir, fileExists, readFile, writeFile } from "../../utils/fs.js";
10
7
  import { createBranchNameFromIssue } from "../../utils/git/branch-name.js";
8
+ import { execSafe, git } from "../../utils/git/exec.js";
9
+ import { runClaude } from "./claude-cli.js";
11
10
  import { getConfig } from "./config.js";
12
11
  import { ARTIFACTS } from "./prompt-templates/index.js";
13
- import { spawnClaude } from "./spawn-claude.js";
12
+ import { resolveTemplate } from "./templates.js";
14
13
 
15
- const __dirname = dirname(fileURLToPath(import.meta.url));
16
- export const TEMPLATES_DIR = join(__dirname, "prompt-templates");
14
+ import type { TokenValues } from "./templates.js";
17
15
 
18
- // ── Shell helpers ──
19
-
20
- async function exec(cmd: string, args: string[]): Promise<string> {
21
- const result = await x(cmd, args, { nodeOptions: { cwd: process.cwd() }, throwOnError: true });
22
- return result.stdout.trim();
23
- }
24
-
25
- export async function execSafe(
26
- cmd: string,
27
- args: string[],
28
- ): Promise<{ stdout: string; ok: boolean }> {
29
- const result = await x(cmd, args, { nodeOptions: { cwd: process.cwd() }, throwOnError: false });
30
- return { stdout: (result.stdout ?? "").trim(), ok: result.exitCode === 0 };
31
- }
32
-
33
- export async function gh<T = unknown>(args: string[]): Promise<T> {
34
- const out = await exec("gh", args);
35
- return JSON.parse(out) as T;
36
- }
37
-
38
- export async function ghRaw(args: string[]): Promise<string> {
39
- const result = await execSafe("gh", args);
40
- return result.stdout;
41
- }
42
-
43
- export async function git(args: string[]): Promise<string> {
44
- return exec("git", args);
45
- }
46
-
47
- export function sleep(ms: number): Promise<void> {
48
- return new Promise((resolve) => setTimeout(resolve, ms));
49
- }
50
-
51
- // ── Claude CLI ──
52
-
53
- export interface ClaudeResult {
54
- result: string;
55
- is_error: boolean;
56
- total_cost_usd: number;
57
- num_turns: number;
58
- }
59
-
60
- export async function runClaude(opts: {
61
- promptFile: string;
62
- permissionMode: "plan" | "acceptEdits";
63
- maxTurns?: number;
64
- retry?: boolean;
65
- }): Promise<ClaudeResult> {
66
- const args = [
67
- "-p",
68
- "--output-format",
69
- "stream-json",
70
- "--verbose",
71
- "--permission-mode",
72
- opts.permissionMode,
73
- ...(opts.maxTurns ? ["--max-turns", String(opts.maxTurns)] : []),
74
- `@${opts.promptFile}`,
75
- ];
76
-
77
- const cfg = getConfig();
78
- let retryDelay = cfg.retryDelayMs;
79
- let retries = 0;
80
-
81
- while (true) {
82
- try {
83
- const result = await runClaudeStreaming(args);
84
- consola.success(`Done — ${result.num_turns} turns`);
85
- if (result.result) {
86
- consola.log(result.result);
87
- }
88
- return result;
89
- } catch (e) {
90
- const shouldRetry = opts.retry ?? cfg.loopRetryEnabled ?? false;
91
- if (!shouldRetry) throw e;
92
-
93
- retries++;
94
- if (retries >= cfg.maxRetries) {
95
- throw new Error(`Claude failed after ${cfg.maxRetries} retries: ${e}`);
96
- }
97
-
98
- consola.warn(`Claude process error (attempt ${retries}/${cfg.maxRetries}): ${e}`);
99
- consola.info(`Retrying in ${retryDelay / 1000}s...`);
100
- await sleep(retryDelay);
101
- retryDelay = Math.min(retryDelay * 2, cfg.maxRetryDelayMs);
102
- }
103
- }
104
- }
105
-
106
- function runClaudeStreaming(args: string[]): Promise<ClaudeResult> {
107
- return new Promise((resolve, reject) => {
108
- const proc = spawnClaude(args);
109
- let capturedResult: ClaudeResult | null = null;
110
- let turnCount = 0;
111
-
112
- if (!proc.stdout) {
113
- reject(new Error("Claude process has no stdout"));
114
- return;
115
- }
116
-
117
- const rl = createInterface({ input: proc.stdout });
118
-
119
- rl.on("line", (line) => {
120
- if (!line.trim()) return;
121
- try {
122
- const event = JSON.parse(line) as Record<string, unknown>;
123
- handleStreamEvent(event, (turns) => {
124
- turnCount = turns;
125
- });
126
-
127
- if ("result" in event && "is_error" in event && "num_turns" in event) {
128
- capturedResult = {
129
- result: String(event.result ?? ""),
130
- is_error: Boolean(event.is_error),
131
- total_cost_usd: Number(event.total_cost_usd ?? 0),
132
- num_turns: Number(event.num_turns),
133
- };
134
- }
135
- } catch {
136
- // Skip non-JSON lines
137
- }
138
- });
139
-
140
- proc.on("error", (err) => {
141
- rl.close();
142
- reject(err);
143
- });
144
-
145
- proc.on("close", (code) => {
146
- rl.close();
147
- if (capturedResult) {
148
- resolve(capturedResult);
149
- } else if (code !== 0) {
150
- reject(new Error(`Claude process exited with code ${code}`));
151
- } else {
152
- resolve({ result: "", is_error: false, total_cost_usd: 0, num_turns: turnCount });
153
- }
154
- });
155
- });
156
- }
157
-
158
- function handleStreamEvent(event: Record<string, unknown>, onTurn: (count: number) => void): void {
159
- // Tool use events: look for content blocks with tool_use type
160
- if (event.type === "assistant" && Array.isArray(event.message)) {
161
- for (const block of event.message) {
162
- if (
163
- typeof block === "object" &&
164
- block !== null &&
165
- "type" in block &&
166
- (block as Record<string, unknown>).type === "tool_use"
167
- ) {
168
- const name = (block as Record<string, unknown>).name;
169
- if (typeof name === "string") {
170
- consola.info(` ${pc.dim("\u21B3")} ${name}`);
171
- }
172
- }
173
- }
174
- }
175
-
176
- // Content block with tool_use (alternative format)
177
- if (
178
- event.type === "content_block_start" &&
179
- typeof event.content_block === "object" &&
180
- event.content_block !== null
181
- ) {
182
- const block = event.content_block as Record<string, unknown>;
183
- if (block.type === "tool_use" && typeof block.name === "string") {
184
- consola.info(` ${pc.dim("\u21B3")} ${block.name}`);
185
- }
186
- }
187
-
188
- // Turn count tracking
189
- if (typeof event.num_turns === "number" && !("result" in event)) {
190
- onTurn(event.num_turns as number);
191
- }
192
- }
193
-
194
- // ── Template resolution ──
195
-
196
- export interface TokenValues {
197
- SCOPE_PATH: string;
198
- ISSUE_DIR: string;
199
- MAIN_BRANCH: string;
200
- }
201
-
202
- export function resolveTemplate(
203
- templateName: string,
204
- tokens: TokenValues,
205
- issueDir: string,
206
- ): string {
207
- const templatePath = join(TEMPLATES_DIR, templateName);
208
- let template = readFileSync(templatePath, "utf-8");
209
-
210
- for (const [key, value] of Object.entries(tokens)) {
211
- template = template.replaceAll(`{{${key}}}`, value);
212
- }
213
-
214
- const resolvedPath = join(issueDir, templateName);
215
- ensureDir(dirname(resolvedPath));
216
- writeFileSync(resolvedPath, template, "utf-8");
217
-
218
- return relative(process.cwd(), resolvedPath);
219
- }
220
-
221
- // ── File helpers ──
222
-
223
- export function ensureDir(dir: string): void {
224
- mkdirSync(dir, { recursive: true });
225
- }
226
-
227
- export function fileExists(path: string): boolean {
228
- return existsSync(path);
229
- }
230
-
231
- export function readFile(path: string): string {
232
- return readFileSync(path, "utf-8");
233
- }
234
-
235
- export function writeFile(path: string, content: string): void {
236
- ensureDir(dirname(path));
237
- writeFileSync(path, content, "utf-8");
238
- }
239
-
240
- // ── Git helpers ──
241
-
242
- export async function commitArtifacts(ctx: IssueContext, message: string): Promise<void> {
243
- await git(["add", ctx.issueDirRel]);
244
- const staged = await execSafe("git", ["diff", "--cached", "--name-only"]);
245
- if (staged.ok && staged.stdout.length > 0) {
246
- await git(["commit", "-m", message]);
247
- }
248
- }
16
+ export { ensureDir, fileExists, readFile, writeFile } from "../../utils/fs.js";
249
17
 
250
18
  // ── Issue context ──
251
19
 
@@ -278,11 +46,13 @@ export function buildIssueContext(
278
46
  };
279
47
  }
280
48
 
281
- export function buildTokens(ctx: IssueContext): TokenValues {
49
+ export function buildTokens(ctx: IssueContext, overrides?: Partial<TokenValues>): TokenValues {
282
50
  return {
283
51
  SCOPE_PATH: ctx.scopePath,
284
52
  ISSUE_DIR: ctx.issueDirRel,
285
53
  MAIN_BRANCH: getConfig().mainBranch,
54
+ REVIEW_FEEDBACK: "",
55
+ ...overrides,
286
56
  };
287
57
  }
288
58
 
@@ -321,6 +91,8 @@ function findNthBlankLine(lines: string[], n: number): number {
321
91
  return 0;
322
92
  }
323
93
 
94
+ // ── Logging ──
95
+
324
96
  export function log(msg: string): void {
325
97
  consola.info(`[auto-claude] ${msg}`);
326
98
  }
@@ -347,16 +119,22 @@ export function logStep(step: string, ctx: IssueContext, skipped = false): void
347
119
  export async function ensureBranch(branch: string): Promise<void> {
348
120
  const { mainBranch, remote } = getConfig();
349
121
 
350
- try {
351
- const branches = await git(["branch", "--list", branch]);
352
- if (branches.includes(branch)) {
353
- await git(["checkout", branch]);
354
- return;
355
- }
356
- } catch {
357
- /* ignore */
122
+ // Stash uncommitted changes so branch switching works from a dirty tree
123
+ const status = await execSafe("git", ["status", "--porcelain"]);
124
+ const hadDirtyTree = status.ok && status.stdout.length > 0;
125
+ if (hadDirtyTree) {
126
+ await git(["stash", "push", "-m", `auto-claude: before switching to ${branch}`]);
127
+ log("Stashed uncommitted changes");
128
+ }
129
+
130
+ // Check if branch exists locally (rev-parse is reliable, no output parsing)
131
+ const local = await execSafe("git", ["rev-parse", "--verify", `refs/heads/${branch}`]);
132
+ if (local.ok) {
133
+ await git(["checkout", branch]);
134
+ return;
358
135
  }
359
136
 
137
+ // Check if branch exists on remote
360
138
  try {
361
139
  await git(["fetch", remote, branch]);
362
140
  await git(["checkout", branch]);
@@ -365,6 +143,7 @@ export async function ensureBranch(branch: string): Promise<void> {
365
143
  /* doesn't exist remotely */
366
144
  }
367
145
 
146
+ // Create new branch from main
368
147
  await git(["checkout", mainBranch]);
369
148
  await git(["pull", remote, mainBranch]);
370
149
  await git(["checkout", "-b", branch]);
@@ -403,7 +182,6 @@ export async function runStepWithArtifact(opts: StepRunnerOptions): Promise<bool
403
182
 
404
183
  const result = await runClaude({
405
184
  promptFile,
406
- permissionMode: "acceptEdits",
407
185
  maxTurns: getConfig().maxTurns,
408
186
  });
409
187
 
@@ -417,9 +195,5 @@ export async function runStepWithArtifact(opts: StepRunnerOptions): Promise<bool
417
195
  return false;
418
196
  }
419
197
 
420
- await commitArtifacts(
421
- ctx,
422
- `chore(auto-claude): ${stepName.toLowerCase()} for ${ctx.repo}#${ctx.number}`,
423
- );
424
198
  return true;
425
199
  }