@towles/tool 0.0.62 → 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.
- package/package.json +50 -57
- package/src/commands/agentboard.ts +176 -0
- package/src/commands/{auto-claude.ts → auto-claude/index.ts} +18 -28
- package/src/commands/auto-claude/list.ts +114 -0
- package/src/commands/auto-claude/retry.test.ts +138 -0
- package/src/commands/auto-claude/retry.ts +139 -0
- package/src/commands/auto-claude/status.test.ts +147 -0
- package/src/commands/auto-claude/status.ts +123 -0
- package/src/commands/base.ts +7 -2
- package/src/commands/config.ts +5 -7
- package/src/commands/doctor.ts +111 -12
- package/src/commands/gh/branch.ts +4 -4
- package/src/commands/gh/pr.ts +1 -0
- package/src/commands/graph/index.ts +169 -0
- package/src/commands/graph.test.ts +1 -1
- package/src/commands/install.ts +40 -68
- package/src/commands/journal/daily-notes.ts +3 -3
- package/src/commands/journal/meeting.ts +3 -3
- package/src/commands/journal/note.ts +3 -3
- package/src/lib/auto-claude/claude-cli.ts +183 -0
- package/src/lib/auto-claude/config.test.ts +6 -8
- package/src/lib/auto-claude/config.ts +3 -4
- package/src/lib/auto-claude/index.ts +2 -3
- package/src/lib/auto-claude/labels.test.ts +85 -0
- package/src/lib/auto-claude/labels.ts +42 -0
- package/src/lib/auto-claude/pipeline-execution.test.ts +129 -33
- package/src/lib/auto-claude/pipeline.test.ts +2 -2
- package/src/lib/auto-claude/pipeline.ts +120 -36
- package/src/lib/auto-claude/prompt-templates/01_plan.prompt.md +68 -0
- package/src/lib/auto-claude/prompt-templates/{05_implement.prompt.md → 02_implement.prompt.md} +3 -2
- package/src/lib/auto-claude/prompt-templates/03_simplify.prompt.md +52 -0
- package/src/lib/auto-claude/prompt-templates/{06_review.prompt.md → 04_review.prompt.md} +29 -6
- package/src/lib/auto-claude/prompt-templates/index.test.ts +9 -42
- package/src/lib/auto-claude/prompt-templates/index.ts +13 -28
- package/src/lib/auto-claude/run-claude.test.ts +48 -68
- package/src/lib/auto-claude/shell.ts +6 -0
- package/src/lib/auto-claude/steps/create-pr.ts +89 -25
- package/src/lib/auto-claude/steps/fetch-issues.ts +4 -1
- package/src/lib/auto-claude/steps/implement.ts +9 -16
- package/src/lib/auto-claude/steps/simple-steps.ts +34 -0
- package/src/lib/auto-claude/steps/steps.test.ts +68 -63
- package/src/lib/auto-claude/templates.test.ts +91 -0
- package/src/lib/auto-claude/templates.ts +34 -0
- package/src/lib/auto-claude/test-helpers.ts +2 -1
- package/src/lib/auto-claude/utils-execution.test.ts +9 -57
- package/src/lib/auto-claude/utils.test.ts +5 -9
- package/src/lib/auto-claude/utils.ts +27 -253
- package/src/lib/graph/analyzer.test.ts +451 -0
- package/src/lib/graph/analyzer.ts +165 -0
- package/src/lib/graph/index.ts +24 -0
- package/src/lib/graph/labels.ts +87 -0
- package/src/lib/graph/parser.test.ts +150 -0
- package/src/lib/graph/parser.ts +65 -0
- package/src/lib/graph/render.ts +25 -0
- package/src/lib/graph/server.ts +70 -0
- package/src/lib/graph/sessions.ts +104 -0
- package/src/lib/graph/tools.ts +90 -0
- package/src/lib/graph/treemap.ts +211 -0
- package/src/lib/graph/types.ts +80 -0
- package/src/lib/install/claude-settings.ts +64 -0
- package/src/lib/journal/editor.ts +33 -0
- package/src/lib/journal/fs.ts +13 -0
- package/src/lib/journal/index.ts +11 -0
- package/src/lib/journal/paths.ts +106 -0
- package/src/lib/journal/{utils.ts → templates.ts} +3 -151
- package/src/utils/fs.ts +19 -0
- package/src/utils/git/exec.ts +18 -0
- package/src/utils/git/gh-cli-wrapper.test.ts +47 -8
- package/src/utils/git/gh-cli-wrapper.ts +31 -19
- package/src/utils/render.ts +3 -1
- package/src/commands/graph.ts +0 -970
- package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +0 -21
- package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +0 -27
- package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +0 -15
- package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +0 -35
- package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +0 -30
- package/src/lib/auto-claude/steps/plan-annotations.ts +0 -54
- package/src/lib/auto-claude/steps/plan-implementation.ts +0 -14
- package/src/lib/auto-claude/steps/plan.ts +0 -14
- package/src/lib/auto-claude/steps/refresh.ts +0 -114
- package/src/lib/auto-claude/steps/remove-label.ts +0 -22
- package/src/lib/auto-claude/steps/research.ts +0 -21
- package/src/lib/auto-claude/steps/review.ts +0 -14
|
@@ -1,251 +1,19 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
12
|
+
import { resolveTemplate } from "./templates.js";
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
export const TEMPLATES_DIR = join(__dirname, "prompt-templates");
|
|
14
|
+
import type { TokenValues } from "./templates.js";
|
|
17
15
|
|
|
18
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
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
|
}
|