@towles/tool 0.0.62 → 0.0.64
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
package/package.json
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towles/tool",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"keywords": [
|
|
6
|
-
"auto-claude",
|
|
7
|
-
"autonomic",
|
|
8
|
-
"claude",
|
|
9
|
-
"cli",
|
|
10
|
-
"git",
|
|
11
|
-
"journal",
|
|
12
|
-
"oclif"
|
|
13
|
-
],
|
|
3
|
+
"version": "0.0.64",
|
|
4
|
+
"description": "One off quality of life scripts that I use on a daily basis.",
|
|
14
5
|
"homepage": "https://github.com/ChrisTowles/towles-tool#readme",
|
|
15
6
|
"bugs": {
|
|
16
7
|
"url": "https://github.com/ChrisTowles/towles-tool/issues"
|
|
@@ -34,79 +25,81 @@
|
|
|
34
25
|
"src"
|
|
35
26
|
],
|
|
36
27
|
"type": "module",
|
|
37
|
-
"
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
38
31
|
"scripts": {
|
|
39
|
-
"
|
|
40
|
-
"
|
|
32
|
+
"version:sync": "pnpm tsx scripts/sync-versions.ts",
|
|
33
|
+
"prepublishOnly": "pnpm run version:sync",
|
|
34
|
+
"dev": "tsx bin/run.ts",
|
|
35
|
+
"format": "oxfmt --write .",
|
|
36
|
+
"format:check": "oxfmt --check .",
|
|
41
37
|
"lint": "oxlint",
|
|
42
38
|
"lint:fix": "oxlint --fix",
|
|
43
|
-
"format": "oxfmt --write",
|
|
44
|
-
"format:check": "oxfmt --check",
|
|
45
39
|
"test": "vitest run",
|
|
46
|
-
"test:
|
|
47
|
-
"
|
|
48
|
-
"
|
|
40
|
+
"test:prompts": "promptfoo eval && promptfoo eval -c plugins/tt-core/promptfooconfig.yaml && promptfoo eval -c plugins/tt-auto-claude/promptfooconfig.yaml",
|
|
41
|
+
"test:prompts:root": "promptfoo eval",
|
|
42
|
+
"test:prompts:tt-core": "promptfoo eval -c plugins/tt-core/promptfooconfig.yaml",
|
|
43
|
+
"test:prompts:tt-core:llm": "promptfoo eval -c plugins/tt-core/promptfooconfig.llm.yaml",
|
|
44
|
+
"test:prompts:tt-auto-claude": "promptfoo eval -c plugins/tt-auto-claude/promptfooconfig.yaml",
|
|
45
|
+
"test:watch": "CI=DisableCallingClaude vitest watch",
|
|
46
|
+
"typecheck": "tsgo --noEmit --incremental",
|
|
47
|
+
"prepare": "simple-git-hooks"
|
|
49
48
|
},
|
|
50
49
|
"dependencies": {
|
|
51
|
-
"@
|
|
50
|
+
"@anthropic-ai/claude-code": "^2.1.4",
|
|
51
|
+
"@anthropic-ai/sdk": "^0.56.0",
|
|
52
|
+
"@oclif/core": "^4.3.2",
|
|
52
53
|
"consola": "^3.4.2",
|
|
53
54
|
"d3-hierarchy": "^3.1.2",
|
|
54
55
|
"fzf": "^0.5.2",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"open": "^10.1.1",
|
|
56
|
+
"luxon": "^3.7.1",
|
|
57
|
+
"neverthrow": "^8.2.0",
|
|
58
58
|
"picocolors": "^1.1.1",
|
|
59
59
|
"prompts": "^2.4.2",
|
|
60
|
-
"strip-ansi": "^7.1.
|
|
61
|
-
"tinyexec": "^
|
|
62
|
-
"zod": "^
|
|
60
|
+
"strip-ansi": "^7.1.0",
|
|
61
|
+
"tinyexec": "^0.3.2",
|
|
62
|
+
"zod": "^4.0.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@oclif/test": "^4.1.
|
|
66
|
-
"@total-typescript/tsconfig": "^1.0.4",
|
|
67
|
-
"@tsconfig/strictest": "^2.0.5",
|
|
65
|
+
"@oclif/test": "^4.1.10",
|
|
68
66
|
"@types/d3-hierarchy": "^3.1.7",
|
|
69
|
-
"@types/luxon": "^3.
|
|
70
|
-
"@types/node": "^22.
|
|
67
|
+
"@types/luxon": "^3.6.2",
|
|
68
|
+
"@types/node": "^22.16.3",
|
|
71
69
|
"@types/prompts": "^2.4.9",
|
|
70
|
+
"@typescript/native-preview": "^7.0.0-dev.20260111.1",
|
|
72
71
|
"bumpp": "^10.4.0",
|
|
73
|
-
"lint-staged": "^15.5.1",
|
|
74
72
|
"oxfmt": "^0.24.0",
|
|
75
|
-
"oxlint": "^1.
|
|
76
|
-
"
|
|
77
|
-
"
|
|
73
|
+
"oxlint": "^1.7.0",
|
|
74
|
+
"promptfoo": "^0.121.2",
|
|
75
|
+
"simple-git-hooks": "^2.13.0",
|
|
76
|
+
"tsx": "^4.19.4",
|
|
78
77
|
"typescript": "^5.8.3",
|
|
79
|
-
"vitest": "^
|
|
78
|
+
"vitest": "^4.0.17"
|
|
80
79
|
},
|
|
81
80
|
"simple-git-hooks": {
|
|
82
|
-
"pre-commit": "pnpm
|
|
83
|
-
},
|
|
84
|
-
"lint-staged": {
|
|
85
|
-
"package.json": "oxfmt --write",
|
|
86
|
-
"*.{ts,tsx,mts,cts,js,cjs,mjs}": [
|
|
87
|
-
"oxlint --fix"
|
|
88
|
-
],
|
|
89
|
-
"*.*": [
|
|
90
|
-
"oxfmt --write"
|
|
91
|
-
]
|
|
81
|
+
"pre-commit": "pnpm format && pnpm lint:fix && pnpm typecheck && claude plugin validate ."
|
|
92
82
|
},
|
|
93
83
|
"oclif": {
|
|
94
84
|
"bin": "tt",
|
|
95
|
-
"commands":
|
|
96
|
-
"strategy": "pattern",
|
|
97
|
-
"target": "./src/commands"
|
|
98
|
-
},
|
|
85
|
+
"commands": "./src/commands",
|
|
99
86
|
"dirname": "towles-tool",
|
|
100
|
-
"plugins": [],
|
|
101
87
|
"topicSeparator": " "
|
|
102
88
|
},
|
|
103
|
-
"
|
|
104
|
-
"node": ">=18.0.0"
|
|
105
|
-
},
|
|
106
|
-
"packageManager": "pnpm@10.11.0",
|
|
89
|
+
"packageManager": "pnpm@10.27.0",
|
|
107
90
|
"pnpm": {
|
|
108
91
|
"patchedDependencies": {
|
|
109
92
|
"prompts@2.4.2": "patches/prompts.patch"
|
|
110
|
-
}
|
|
111
|
-
|
|
93
|
+
},
|
|
94
|
+
"onlyBuiltDependencies": [
|
|
95
|
+
"@anthropic-ai/claude-code",
|
|
96
|
+
"better-sqlite3",
|
|
97
|
+
"esbuild",
|
|
98
|
+
"simple-git-hooks",
|
|
99
|
+
"@parcel/watcher"
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"trustedDependencies": [
|
|
103
|
+
"@anthropic-ai/claude-code"
|
|
104
|
+
]
|
|
112
105
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Args, Flags } from "@oclif/core";
|
|
2
|
+
import { execSync, spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { resolve, join } from "node:path";
|
|
5
|
+
import { networkInterfaces } from "node:os";
|
|
6
|
+
import consola from "consola";
|
|
7
|
+
import { BaseCommand } from "./base.js";
|
|
8
|
+
|
|
9
|
+
function getLocalIp(): string {
|
|
10
|
+
const nets = networkInterfaces();
|
|
11
|
+
for (const ifaces of Object.values(nets)) {
|
|
12
|
+
if (!ifaces) continue;
|
|
13
|
+
for (const iface of ifaces) {
|
|
14
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
15
|
+
return iface.address;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return "localhost";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default class Agentboard extends BaseCommand {
|
|
23
|
+
static override aliases = ["ag"];
|
|
24
|
+
static override description = "Start AgentBoard — agentic workflow orchestration IDE";
|
|
25
|
+
|
|
26
|
+
static override examples = [
|
|
27
|
+
{
|
|
28
|
+
description: "Start AgentBoard on default port",
|
|
29
|
+
command: "<%= config.bin %> agentboard",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
description: "Start on a custom port",
|
|
33
|
+
command: "<%= config.bin %> ag --port 3000",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
description: "Start without opening browser",
|
|
37
|
+
command: "<%= config.bin %> ag --no-open",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
description: "Attach to a running card tmux session",
|
|
41
|
+
command: "<%= config.bin %> ag attach 42",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
static override flags = {
|
|
46
|
+
port: Flags.string({
|
|
47
|
+
char: "p",
|
|
48
|
+
description: "Port to serve on",
|
|
49
|
+
default: "4200",
|
|
50
|
+
}),
|
|
51
|
+
open: Flags.boolean({
|
|
52
|
+
description: "Open browser after starting",
|
|
53
|
+
default: true,
|
|
54
|
+
allowNo: true,
|
|
55
|
+
}),
|
|
56
|
+
"data-dir": Flags.string({
|
|
57
|
+
char: "d",
|
|
58
|
+
description: "Directory for AgentBoard data (SQLite DB, artifacts)",
|
|
59
|
+
env: "AGENTBOARD_DATA_DIR",
|
|
60
|
+
}),
|
|
61
|
+
lan: Flags.boolean({
|
|
62
|
+
description: "Listen on all interfaces (0.0.0.0) for LAN access. Default: localhost only.",
|
|
63
|
+
default: false,
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
static override args = {
|
|
68
|
+
subcommand: Args.string({
|
|
69
|
+
description: "Subcommand (attach, reset)",
|
|
70
|
+
required: false,
|
|
71
|
+
}),
|
|
72
|
+
cardId: Args.string({
|
|
73
|
+
description: "Card ID for attach subcommand",
|
|
74
|
+
required: false,
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
async run(): Promise<void> {
|
|
79
|
+
const { args, flags } = await this.parse(Agentboard);
|
|
80
|
+
|
|
81
|
+
if (args.subcommand === "attach") {
|
|
82
|
+
if (!args.cardId) {
|
|
83
|
+
this.error("Card ID is required for attach subcommand");
|
|
84
|
+
}
|
|
85
|
+
execSync(`tmux attach-session -t card-${args.cardId}`, {
|
|
86
|
+
stdio: "inherit",
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (args.subcommand === "reset") {
|
|
92
|
+
const defaultDataDir = resolve(
|
|
93
|
+
process.env.XDG_CONFIG_HOME ?? resolve(process.env.HOME ?? "~", ".config"),
|
|
94
|
+
"towles-tool",
|
|
95
|
+
"agentboard",
|
|
96
|
+
);
|
|
97
|
+
const dataDir = flags["data-dir"] ? resolve(flags["data-dir"]) : defaultDataDir;
|
|
98
|
+
const dbPath = join(dataDir, "agentboard.db");
|
|
99
|
+
const walPath = `${dbPath}-wal`;
|
|
100
|
+
const shmPath = `${dbPath}-shm`;
|
|
101
|
+
|
|
102
|
+
if (!existsSync(dbPath)) {
|
|
103
|
+
consola.info("No database found — nothing to reset.");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
consola.warn(`This will delete: ${dbPath}`);
|
|
108
|
+
for (const f of [dbPath, walPath, shmPath]) {
|
|
109
|
+
if (existsSync(f)) {
|
|
110
|
+
const { unlinkSync } = await import("node:fs");
|
|
111
|
+
unlinkSync(f);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
consola.success("Database reset. Start AgentBoard to create a fresh DB.");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const agentboardDir = resolve(import.meta.dirname, "../../plugins/tt-agentboard");
|
|
119
|
+
const port = flags.port;
|
|
120
|
+
const defaultDataDir = resolve(
|
|
121
|
+
process.env.XDG_CONFIG_HOME ?? resolve(process.env.HOME ?? "~", ".config"),
|
|
122
|
+
"towles-tool",
|
|
123
|
+
"agentboard",
|
|
124
|
+
);
|
|
125
|
+
const dataDir = flags["data-dir"] ? resolve(flags["data-dir"]) : defaultDataDir;
|
|
126
|
+
const localIp = getLocalIp();
|
|
127
|
+
const dbPath = join(dataDir, "agentboard.db");
|
|
128
|
+
const isFirstRun = !existsSync(dbPath);
|
|
129
|
+
|
|
130
|
+
const lanMode = flags.lan;
|
|
131
|
+
const host = lanMode ? "0.0.0.0" : "127.0.0.1";
|
|
132
|
+
|
|
133
|
+
const lines = [`AgentBoard\n\n Local: http://localhost:${port}`];
|
|
134
|
+
if (lanMode) {
|
|
135
|
+
lines.push(` Network: http://${localIp}:${port}`);
|
|
136
|
+
} else {
|
|
137
|
+
lines.push(` Network: disabled (use --lan to enable)`);
|
|
138
|
+
}
|
|
139
|
+
lines.push(` Data: ${dataDir}`);
|
|
140
|
+
consola.box(lines.join("\n"));
|
|
141
|
+
|
|
142
|
+
if (isFirstRun) {
|
|
143
|
+
consola.info("First run detected — a new database will be created at startup.");
|
|
144
|
+
consola.info(
|
|
145
|
+
"Setup checklist:\n" +
|
|
146
|
+
" 1. Ensure tmux is installed (sudo apt install tmux / brew install tmux)\n" +
|
|
147
|
+
" 2. Set GITHUB_TOKEN for GitHub features (optional)\n" +
|
|
148
|
+
" 3. Open the board → Workspaces → Add a workspace slot\n" +
|
|
149
|
+
" 4. Create your first card and drag it to In Progress",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const proc = spawn("pnpm", ["dev", "--port", port], {
|
|
154
|
+
cwd: agentboardDir,
|
|
155
|
+
stdio: "inherit",
|
|
156
|
+
env: {
|
|
157
|
+
...process.env,
|
|
158
|
+
NUXT_DEV_HOST: host,
|
|
159
|
+
AGENTBOARD_DATA_DIR: dataDir,
|
|
160
|
+
AGENTBOARD_LAN: lanMode ? "1" : "0",
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (flags.open) {
|
|
165
|
+
setTimeout(() => {
|
|
166
|
+
try {
|
|
167
|
+
execSync(`xdg-open http://localhost:${port}`, { stdio: "ignore" });
|
|
168
|
+
} catch {
|
|
169
|
+
consola.debug("Could not open browser automatically");
|
|
170
|
+
}
|
|
171
|
+
}, 2000);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
proc.on("exit", (code) => process.exit(code ?? 0));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -4,10 +4,9 @@ import { join } from "node:path";
|
|
|
4
4
|
import { Flags } from "@oclif/core";
|
|
5
5
|
import consola from "consola";
|
|
6
6
|
|
|
7
|
-
import { BaseCommand } from "
|
|
7
|
+
import { BaseCommand } from "../base.js";
|
|
8
8
|
import {
|
|
9
9
|
STEP_NAMES,
|
|
10
|
-
buildIssueContext,
|
|
11
10
|
fetchIssue,
|
|
12
11
|
fetchIssues,
|
|
13
12
|
getConfig,
|
|
@@ -17,9 +16,8 @@ import {
|
|
|
17
16
|
logBanner,
|
|
18
17
|
runPipeline,
|
|
19
18
|
sleep,
|
|
20
|
-
|
|
21
|
-
} from "
|
|
22
|
-
import type { IssueContext, StepName } from "../lib/auto-claude/index.js";
|
|
19
|
+
} from "../../lib/auto-claude/index.js";
|
|
20
|
+
import type { IssueContext, StepName } from "../../lib/auto-claude/index.js";
|
|
23
21
|
|
|
24
22
|
export default class AutoClaude extends BaseCommand {
|
|
25
23
|
static override aliases = ["ac"];
|
|
@@ -39,10 +37,6 @@ export default class AutoClaude extends BaseCommand {
|
|
|
39
37
|
description: "Reset local state for an issue",
|
|
40
38
|
command: "<%= config.bin %> auto-claude --reset 42",
|
|
41
39
|
},
|
|
42
|
-
{
|
|
43
|
-
description: "Refresh a stale PR branch",
|
|
44
|
-
command: "<%= config.bin %> auto-claude --refresh --issue 42",
|
|
45
|
-
},
|
|
46
40
|
{
|
|
47
41
|
description: "Loop mode: poll for labeled issues",
|
|
48
42
|
command: "<%= config.bin %> auto-claude --loop",
|
|
@@ -67,9 +61,9 @@ export default class AutoClaude extends BaseCommand {
|
|
|
67
61
|
reset: Flags.integer({
|
|
68
62
|
description: "Delete local state for an issue (force restart)",
|
|
69
63
|
}),
|
|
70
|
-
|
|
71
|
-
description: "
|
|
72
|
-
default:
|
|
64
|
+
model: Flags.string({
|
|
65
|
+
description: "Claude model to use (default: opus)",
|
|
66
|
+
default: "opus",
|
|
73
67
|
}),
|
|
74
68
|
loop: Flags.boolean({
|
|
75
69
|
description: "Poll for labeled issues continuously",
|
|
@@ -100,7 +94,7 @@ export default class AutoClaude extends BaseCommand {
|
|
|
100
94
|
triggerLabel: flags.label,
|
|
101
95
|
mainBranch: flags["main-branch"],
|
|
102
96
|
scopePath: flags["scope-path"],
|
|
103
|
-
|
|
97
|
+
model: flags.model,
|
|
104
98
|
});
|
|
105
99
|
|
|
106
100
|
if (flags.reset) {
|
|
@@ -111,19 +105,6 @@ export default class AutoClaude extends BaseCommand {
|
|
|
111
105
|
return;
|
|
112
106
|
}
|
|
113
107
|
|
|
114
|
-
if (flags.refresh) {
|
|
115
|
-
if (!flags.issue) {
|
|
116
|
-
this.error("--refresh requires --issue <number>");
|
|
117
|
-
}
|
|
118
|
-
const ctx = buildIssueContext(
|
|
119
|
-
{ number: flags.issue, title: `Issue #${flags.issue}`, body: "" },
|
|
120
|
-
cfg.repo,
|
|
121
|
-
cfg.scopePath,
|
|
122
|
-
);
|
|
123
|
-
await stepRefresh(ctx);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
108
|
const untilStep = flags.until as StepName | undefined;
|
|
128
109
|
const loopMode = flags.loop;
|
|
129
110
|
const intervalMs = (flags.interval ?? cfg.loopIntervalMinutes) * 60_000;
|
|
@@ -156,6 +137,7 @@ export default class AutoClaude extends BaseCommand {
|
|
|
156
137
|
throw e;
|
|
157
138
|
}
|
|
158
139
|
|
|
140
|
+
log("Fetching labeled issues…");
|
|
159
141
|
let contexts: IssueContext[];
|
|
160
142
|
if (flags.issue) {
|
|
161
143
|
const ctx = await fetchIssue(flags.issue);
|
|
@@ -170,10 +152,14 @@ export default class AutoClaude extends BaseCommand {
|
|
|
170
152
|
log(`Processing ${contexts.length} issue(s)...\n`);
|
|
171
153
|
|
|
172
154
|
for (const ctx of contexts) {
|
|
155
|
+
const issueStart = Date.now();
|
|
173
156
|
try {
|
|
174
157
|
await runPipeline(ctx, untilStep);
|
|
175
158
|
} catch (e) {
|
|
176
159
|
consola.error(`Pipeline error for ${ctx.repo}#${ctx.number}:`, e);
|
|
160
|
+
} finally {
|
|
161
|
+
const elapsed = ((Date.now() - issueStart) / 1000).toFixed(1);
|
|
162
|
+
log(`Completed ${ctx.repo}#${ctx.number} in ${elapsed}s`);
|
|
177
163
|
}
|
|
178
164
|
}
|
|
179
165
|
}
|
|
@@ -198,7 +184,9 @@ async function syncWithRemote(): Promise<void> {
|
|
|
198
184
|
const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
199
185
|
if (branch !== cfg.mainBranch) {
|
|
200
186
|
log(`Warning: on branch "${branch}", switching to ${cfg.mainBranch}...`);
|
|
201
|
-
await git(["checkout", cfg.mainBranch]).catch(() => {
|
|
187
|
+
await git(["checkout", cfg.mainBranch]).catch(() => {
|
|
188
|
+
// Best-effort checkout — may fail if working tree is dirty
|
|
189
|
+
});
|
|
202
190
|
}
|
|
203
191
|
const status = await git(["status", "--porcelain"]);
|
|
204
192
|
if (status.length > 0) {
|
|
@@ -217,7 +205,9 @@ function registerShutdownHandlers(): void {
|
|
|
217
205
|
log(`Received ${signal}, shutting down...`);
|
|
218
206
|
setTimeout(() => process.exit(1), 5_000).unref();
|
|
219
207
|
git(["checkout", getConfig().mainBranch])
|
|
220
|
-
.catch(() => {
|
|
208
|
+
.catch(() => {
|
|
209
|
+
// Best-effort cleanup on shutdown — ignore failures
|
|
210
|
+
})
|
|
221
211
|
.then(() => process.exit(0));
|
|
222
212
|
});
|
|
223
213
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { colors } from "consola/utils";
|
|
4
|
+
import { Fzf } from "fzf";
|
|
5
|
+
import prompts from "prompts";
|
|
6
|
+
import type { Choice } from "prompts";
|
|
7
|
+
|
|
8
|
+
import { BaseCommand } from "../base.js";
|
|
9
|
+
import { buildIssueChoices, computeColumnLayout } from "../gh/branch.js";
|
|
10
|
+
import { STEP_NAMES, fetchIssue, initConfig, runPipeline } from "../../lib/auto-claude/index.js";
|
|
11
|
+
import type { StepName } from "../../lib/auto-claude/index.js";
|
|
12
|
+
import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
|
|
13
|
+
import { getTerminalColumns } from "../../utils/render.js";
|
|
14
|
+
|
|
15
|
+
export default class AutoClaudeList extends BaseCommand {
|
|
16
|
+
static override description = "Interactively pick an auto-claude issue to process";
|
|
17
|
+
|
|
18
|
+
static override examples = [
|
|
19
|
+
{
|
|
20
|
+
description: "Browse auto-claude labeled issues",
|
|
21
|
+
command: "<%= config.bin %> auto-claude list",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
description: "Pick an issue and run until plan step",
|
|
25
|
+
command: "<%= config.bin %> auto-claude list --until plan",
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
static override flags = {
|
|
30
|
+
...BaseCommand.baseFlags,
|
|
31
|
+
until: Flags.string({
|
|
32
|
+
char: "u",
|
|
33
|
+
description: `Stop after this step (${STEP_NAMES.join(", ")})`,
|
|
34
|
+
options: [...STEP_NAMES],
|
|
35
|
+
}),
|
|
36
|
+
label: Flags.string({
|
|
37
|
+
description: "Trigger label (default: auto-claude)",
|
|
38
|
+
}),
|
|
39
|
+
"main-branch": Flags.string({
|
|
40
|
+
description: "Override main branch detection",
|
|
41
|
+
}),
|
|
42
|
+
"scope-path": Flags.string({
|
|
43
|
+
description: "Path within repo to scope work (default: .)",
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
async run(): Promise<void> {
|
|
48
|
+
const { flags } = await this.parse(AutoClaudeList);
|
|
49
|
+
|
|
50
|
+
const cfg = await initConfig({
|
|
51
|
+
triggerLabel: flags.label,
|
|
52
|
+
mainBranch: flags["main-branch"],
|
|
53
|
+
scopePath: flags["scope-path"],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const cliInstalled = await isGithubCliInstalled();
|
|
57
|
+
if (!cliInstalled) {
|
|
58
|
+
this.error("GitHub CLI (gh) is not installed");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const issues = await getIssues({ cwd: process.cwd(), label: cfg.triggerLabel });
|
|
62
|
+
if (issues.length === 0) {
|
|
63
|
+
consola.info(`No open issues with '${cfg.triggerLabel}' label`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
consola.info(colors.green(`${issues.length} issue(s) with '${cfg.triggerLabel}' label`));
|
|
68
|
+
|
|
69
|
+
const layout = computeColumnLayout(issues, getTerminalColumns());
|
|
70
|
+
const choices = buildIssueChoices(issues, layout);
|
|
71
|
+
|
|
72
|
+
const fzf = new Fzf(choices, {
|
|
73
|
+
selector: (item) => `${item.value} ${item.description}`,
|
|
74
|
+
casing: "case-insensitive",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const result = await prompts(
|
|
79
|
+
{
|
|
80
|
+
name: "issueNumber",
|
|
81
|
+
message: "Pick an issue to process:",
|
|
82
|
+
type: "autocomplete",
|
|
83
|
+
choices,
|
|
84
|
+
async suggest(input: string, choices: Choice[]) {
|
|
85
|
+
const results = fzf.find(input);
|
|
86
|
+
return results.map((r) => choices.find((c) => c.value === r.item.value));
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
onCancel: () => {
|
|
91
|
+
consola.info(colors.dim("Canceled"));
|
|
92
|
+
this.exit(0);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (result.issueNumber === "cancel") {
|
|
98
|
+
consola.info(colors.dim("Canceled"));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const ctx = await fetchIssue(result.issueNumber);
|
|
103
|
+
if (!ctx) {
|
|
104
|
+
this.error(`Could not fetch issue #${result.issueNumber}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const untilStep = flags.until as StepName | undefined;
|
|
108
|
+
await runPipeline(ctx, untilStep);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
consola.error(e);
|
|
111
|
+
this.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { mkdirSync, existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
|
|
5
|
+
import consola from "consola";
|
|
6
|
+
import { x } from "tinyexec";
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
8
|
+
|
|
9
|
+
import { LABELS } from "../../lib/auto-claude/labels.js";
|
|
10
|
+
import { retryIssues } from "./retry.js";
|
|
11
|
+
|
|
12
|
+
// Suppress consola output during tests
|
|
13
|
+
consola.level = -999;
|
|
14
|
+
|
|
15
|
+
// Mock tinyexec so setLabel/removeLabel (which use execSafe -> x) work
|
|
16
|
+
vi.mock("tinyexec", () => ({
|
|
17
|
+
x: vi.fn().mockResolvedValue({ stdout: "", exitCode: 0, stderr: "" }),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
function getGhEditCalls() {
|
|
21
|
+
return vi
|
|
22
|
+
.mocked(x)
|
|
23
|
+
.mock.calls.filter(
|
|
24
|
+
([cmd, args]) => cmd === "gh" && args?.[0] === "issue" && args?.[1] === "edit",
|
|
25
|
+
)
|
|
26
|
+
.map(([, args]) => args as string[]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("retryIssues", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("removes failed label and adds trigger label for each issue", async () => {
|
|
35
|
+
const issues = [
|
|
36
|
+
{
|
|
37
|
+
number: 42,
|
|
38
|
+
title: "Fix bug",
|
|
39
|
+
state: "OPEN",
|
|
40
|
+
labels: [{ name: LABELS.failed, color: "red" }],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const count = await retryIssues("owner/repo", "auto-claude", issues, false);
|
|
45
|
+
|
|
46
|
+
expect(count).toBe(1);
|
|
47
|
+
const editCalls = getGhEditCalls();
|
|
48
|
+
expect(editCalls.length).toBe(2);
|
|
49
|
+
expect(editCalls[0]).toEqual(
|
|
50
|
+
expect.arrayContaining(["42", "--repo", "owner/repo", "--remove-label", LABELS.failed]),
|
|
51
|
+
);
|
|
52
|
+
expect(editCalls[1]).toEqual(
|
|
53
|
+
expect.arrayContaining(["42", "--repo", "owner/repo", "--add-label", "auto-claude"]),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("retries multiple issues", async () => {
|
|
58
|
+
const issues = [
|
|
59
|
+
{
|
|
60
|
+
number: 10,
|
|
61
|
+
title: "Issue A",
|
|
62
|
+
state: "OPEN",
|
|
63
|
+
labels: [{ name: LABELS.failed, color: "red" }],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
number: 20,
|
|
67
|
+
title: "Issue B",
|
|
68
|
+
state: "OPEN",
|
|
69
|
+
labels: [{ name: LABELS.failed, color: "red" }],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const count = await retryIssues("owner/repo", "auto-claude", issues, false);
|
|
74
|
+
|
|
75
|
+
expect(count).toBe(2);
|
|
76
|
+
const editCalls = getGhEditCalls();
|
|
77
|
+
// 2 issues * 2 label ops = 4
|
|
78
|
+
expect(editCalls.length).toBe(4);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns 0 when given empty selection", async () => {
|
|
82
|
+
const count = await retryIssues("owner/repo", "auto-claude", [], false);
|
|
83
|
+
expect(count).toBe(0);
|
|
84
|
+
expect(getGhEditCalls().length).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("cleans artifact directory when clean=true", async () => {
|
|
88
|
+
const tmpDir = join(tmpdir(), `retry-test-${Date.now()}`);
|
|
89
|
+
const issueDir = join(tmpDir, ".auto-claude", "issue-42");
|
|
90
|
+
mkdirSync(issueDir, { recursive: true });
|
|
91
|
+
writeFileSync(join(issueDir, "test.txt"), "data");
|
|
92
|
+
|
|
93
|
+
const originalCwd = process.cwd();
|
|
94
|
+
process.chdir(tmpDir);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const issues = [
|
|
98
|
+
{
|
|
99
|
+
number: 42,
|
|
100
|
+
title: "Fix bug",
|
|
101
|
+
state: "OPEN",
|
|
102
|
+
labels: [{ name: LABELS.failed, color: "red" }],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
await retryIssues("owner/repo", "auto-claude", issues, true);
|
|
107
|
+
expect(existsSync(issueDir)).toBe(false);
|
|
108
|
+
} finally {
|
|
109
|
+
process.chdir(originalCwd);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does not clean artifacts when clean=false", async () => {
|
|
114
|
+
const tmpDir = join(tmpdir(), `retry-test-${Date.now()}`);
|
|
115
|
+
const issueDir = join(tmpDir, ".auto-claude", "issue-42");
|
|
116
|
+
mkdirSync(issueDir, { recursive: true });
|
|
117
|
+
writeFileSync(join(issueDir, "test.txt"), "data");
|
|
118
|
+
|
|
119
|
+
const originalCwd = process.cwd();
|
|
120
|
+
process.chdir(tmpDir);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const issues = [
|
|
124
|
+
{
|
|
125
|
+
number: 42,
|
|
126
|
+
title: "Fix bug",
|
|
127
|
+
state: "OPEN",
|
|
128
|
+
labels: [{ name: LABELS.failed, color: "red" }],
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
await retryIssues("owner/repo", "auto-claude", issues, false);
|
|
133
|
+
expect(existsSync(issueDir)).toBe(true);
|
|
134
|
+
} finally {
|
|
135
|
+
process.chdir(originalCwd);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|