bueller-wheel 0.2.0 → 0.3.1
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/README.md +93 -17
- package/dist/index.js +141 -18
- package/dist/issue-reader.js +92 -0
- package/dist/issue-summarize.js +236 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Bueller Wheel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Life moves pretty fast. If you don't stop and look around once in a while, you could miss it.
|
|
4
|
+
|
|
5
|
+
This is a headless issue processor that runs in a loop and uses Claude to resolve issues or ticket files written in markdown. It plays nicely with Claude Code and uses the sames settings config.
|
|
4
6
|
|
|
5
7
|
## Quick Start
|
|
6
8
|
|
|
@@ -10,17 +12,17 @@ mkdir -p issues/open
|
|
|
10
12
|
echo "@user: Please create a test file with 'Hello World'" > issues/open/p0-100-my-task.md
|
|
11
13
|
|
|
12
14
|
# Run bueller-wheel to complete the task
|
|
13
|
-
npx bueller-wheel
|
|
15
|
+
npx bueller-wheel --run
|
|
14
16
|
```
|
|
15
17
|
|
|
16
18
|
## Why?
|
|
17
19
|
|
|
18
|
-
You want Claude
|
|
20
|
+
You want Claude to autonomously work on a large pile of issues while you go on a day trip into the city. Bueller Wheel helps tackle several issues you might encounter:
|
|
19
21
|
|
|
20
|
-
- **Claude stops processing after a few issues**: Claude
|
|
21
|
-
- **Claude forgets what it's doing**: As Claude
|
|
22
|
-
- **You forget what Claude was doing**: If you successfully get Claude
|
|
23
|
-
- **Claude keeps making the same mistakes**:
|
|
22
|
+
- **Claude stops processing after a few issues**: Claude tends to stop processing after completing a few tasks. Bueller Wheel keeps prompting Claude to work until all of the issues have been resolved.
|
|
23
|
+
- **Claude forgets what it's doing**: As Claude uses up its context window, it tends to forget what it was working on. Bueller Wheel runs Claude with a fresh context window and prompt for each issue.
|
|
24
|
+
- **You forget what Claude was doing**: If you successfully get Claude to work on a large number of tasks, you end up with a pile of code to review. Bueller Wheel structures each issue as a discrete reviewable chunk of work, in a format amenable to multiple iterations of feedback between you and Claude.
|
|
25
|
+
- **Claude keeps making the same mistakes**: An agent that forgets its history is doomed to repeat it. Bueller Wheel sets up an FAQ directory for Claude to speed up resolution of frequent pitfalls.
|
|
24
26
|
|
|
25
27
|
**Note**: Bueller Wheel is not a full-fledged task management system. It has no concept of assignment or dependency apart from linear file ordering. The sweet spot for this tool is **solo developers working on a single branch**. That said, you can make [parallel branches and agents](#working-with-multiple-branches) work.
|
|
26
28
|
|
|
@@ -29,7 +31,7 @@ You want Claude Code to autonomously work on a large pile of issues while you ea
|
|
|
29
31
|
**The Processing Loop**
|
|
30
32
|
|
|
31
33
|
1. Bueller Wheel finds the next issue in `issues/open/` (sorted alphabetically by filename)
|
|
32
|
-
2. Claude
|
|
34
|
+
2. Claude reads the issue and works on the task
|
|
33
35
|
3. Claude appends its work summary to the issue file
|
|
34
36
|
4. Claude decides the outcome:
|
|
35
37
|
- **CONTINUE**: Keep working (stays in `open/`)
|
|
@@ -43,7 +45,7 @@ You want Claude Code to autonomously work on a large pile of issues while you ea
|
|
|
43
45
|
- Move issues from `review/` or `stuck/` back to `open/` if more work is required
|
|
44
46
|
- Delete issues from `review/` when done reviewing, or archive them however you want
|
|
45
47
|
|
|
46
|
-
**Each iteration is a fresh Claude
|
|
48
|
+
**Each iteration is a fresh Claude session** - no memory between iterations, which keeps context focused.
|
|
47
49
|
|
|
48
50
|
**Inherits your project's Claude Code setup** - `bueller-wheel` uses the Anthropic API credential from whichever user you're logged in as. It inherits the same `.claude/settings.json` or `.claude/settings.local.json` as the Claude Code project it's used in. Whatever permissions apply to your regular `claude` CLI should also apply to `bueller-wheel`, with the exception that `bueller-wheel` starts in "accept edits" mode.
|
|
49
51
|
|
|
@@ -137,18 +139,37 @@ The `open/` directory acts as an inbox for the agent on the current branch. The
|
|
|
137
139
|
## CLI Options
|
|
138
140
|
|
|
139
141
|
```bash
|
|
140
|
-
|
|
142
|
+
# Start the agent loop with various options
|
|
143
|
+
npx bueller-wheel --run
|
|
144
|
+
npx bueller-wheel --git
|
|
145
|
+
npx bueller-wheel --max 50
|
|
146
|
+
npx bueller-wheel --continue "fix the bug"
|
|
147
|
+
|
|
148
|
+
# Summarize issues
|
|
149
|
+
npx bueller-wheel --summarize p1-003-task.md
|
|
150
|
+
npx bueller-wheel --summarize p1-003.md p2-005.md --index 1
|
|
151
|
+
|
|
152
|
+
# Combine with other options
|
|
153
|
+
npx bueller-wheel --run --issues-dir ./my-issues --faq-dir ./my-faq
|
|
154
|
+
npx bueller-wheel --max 50 --git --prompt ./my-prompt.md
|
|
141
155
|
|
|
142
156
|
# Or if installed globally
|
|
143
|
-
bueller-wheel --
|
|
157
|
+
bueller-wheel --run
|
|
158
|
+
bueller-wheel --git
|
|
144
159
|
```
|
|
145
160
|
|
|
161
|
+
**Run Commands** (one required):
|
|
162
|
+
- `--run`: Explicitly start the agent loop with defaults
|
|
163
|
+
- `--git`: Enable automatic git commits and start the loop
|
|
164
|
+
- `--max <number>`: Start with maximum N iterations (default: `25`)
|
|
165
|
+
- `--continue [prompt]`: Continue from previous session. Optional prompt defaults to "continue" if not provided
|
|
166
|
+
- `--summarize <issue...>`: Display abbreviated summaries of issue conversation history
|
|
167
|
+
|
|
168
|
+
**Configuration Options**:
|
|
146
169
|
- `--issues-dir <path>`: Issues directory (default: `./issues`)
|
|
147
170
|
- `--faq-dir <path>`: FAQ directory (default: `./faq`)
|
|
148
|
-
- `--max-iterations <number>`: Maximum iterations (default: `25`)
|
|
149
|
-
- `--git-commit`: Enable automatic git commits after each iteration (default: disabled)
|
|
150
171
|
- `--prompt <path>`: Path to custom prompt template file (default: `<issues-dir>/prompt.md`)
|
|
151
|
-
- `--
|
|
172
|
+
- `--index <N>` or `--index <M,N>`: Expand specific messages when using `--summarize` (see below)
|
|
152
173
|
|
|
153
174
|
### Custom Prompt Templates
|
|
154
175
|
|
|
@@ -210,7 +231,7 @@ Note that only the immediate prior iteration is continued. The next iteration wi
|
|
|
210
231
|
|
|
211
232
|
### Git Auto-Commit
|
|
212
233
|
|
|
213
|
-
When `--git
|
|
234
|
+
When `--git` is enabled, Bueller Wheel will automatically create a git commit after each iteration where work was done on an issue.
|
|
214
235
|
|
|
215
236
|
The commit message format includes the issue ID (the filename minus the `.md`) and status:
|
|
216
237
|
```
|
|
@@ -219,13 +240,68 @@ p0-002-git in progress
|
|
|
219
240
|
p0-002-git stuck
|
|
220
241
|
p0-002-git unknown
|
|
221
242
|
```
|
|
243
|
+
|
|
244
|
+
### Issue Summarization
|
|
245
|
+
|
|
246
|
+
The `--summarize` command provides a quick way to review issue conversation history without opening files. This is especially useful for:
|
|
247
|
+
- Quickly understanding what happened in an issue
|
|
248
|
+
- Reviewing multiple issues at once
|
|
249
|
+
- Checking the status and progress of work
|
|
250
|
+
|
|
251
|
+
**Basic Usage:**
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Summarize a single issue (by filename - searches across open/, review/, stuck/)
|
|
255
|
+
npx bueller-wheel --summarize p1-003-task.md
|
|
256
|
+
|
|
257
|
+
# Summarize by partial filename
|
|
258
|
+
npx bueller-wheel --summarize p1-003.md
|
|
259
|
+
|
|
260
|
+
# Summarize with full path
|
|
261
|
+
npx bueller-wheel --summarize /path/to/issues/open/p1-003-task.md
|
|
262
|
+
|
|
263
|
+
# Summarize multiple issues
|
|
264
|
+
npx bueller-wheel --summarize p1-003.md p1-004.md p2-001.md
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Summary Format:**
|
|
268
|
+
|
|
269
|
+
By default, summaries show:
|
|
270
|
+
- Issue filename and status (open/review/stuck)
|
|
271
|
+
- First message: up to 300 characters
|
|
272
|
+
- Middle messages: up to 80 characters each (abbreviated)
|
|
273
|
+
- Last message: up to 300 characters
|
|
274
|
+
|
|
275
|
+
**Expanding Messages:**
|
|
276
|
+
|
|
277
|
+
Use `--index` to expand specific messages to their full content:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# Expand a single message at index 2
|
|
281
|
+
npx bueller-wheel --summarize p1-003.md --index 2
|
|
282
|
+
|
|
283
|
+
# Expand a range of messages (indices 1 through 3)
|
|
284
|
+
npx bueller-wheel --summarize p1-003.md --index 1,3
|
|
285
|
+
|
|
286
|
+
# Works with multiple issues (expands same indices for all)
|
|
287
|
+
npx bueller-wheel --summarize p1-003.md p1-004.md --index 0,2
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Note:** Message indices are 0-based (first message is index 0).
|
|
291
|
+
|
|
222
292
|
## Development
|
|
223
293
|
|
|
224
|
-
`pnpm run dev` will execute the current `src/index.ts` script file with whatever args you pass to it.
|
|
294
|
+
`pnpm run dev` will execute the current `src/index.ts` script file with whatever args you pass to it. For example:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
pnpm run dev -- --run
|
|
298
|
+
pnpm run dev -- --git
|
|
299
|
+
pnpm run dev -- --max 10
|
|
300
|
+
```
|
|
225
301
|
|
|
226
302
|
## End-to-End Testing
|
|
227
303
|
|
|
228
|
-
**These tests use your actual
|
|
304
|
+
**These tests use your actual Anthropic / Claude credentials!**
|
|
229
305
|
|
|
230
306
|
```bash
|
|
231
307
|
# Run all tests
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execSync } from 'node:child_process';
|
|
|
3
3
|
import * as fs from 'node:fs/promises';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
6
|
+
import { expandMessages, formatIssueSummary, resolveIssueReference, summarizeIssue, } from './issue-summarize.js';
|
|
6
7
|
// Colors for output
|
|
7
8
|
const colors = {
|
|
8
9
|
red: '\x1b[0;31m',
|
|
@@ -17,19 +18,27 @@ const ISSUE_DIR_REVIEW = 'review';
|
|
|
17
18
|
const ISSUE_DIR_STUCK = 'stuck';
|
|
18
19
|
function showHelp() {
|
|
19
20
|
console.log(`
|
|
20
|
-
Bueller - Headless Claude Code Issue Processor
|
|
21
|
+
Bueller Wheel - Headless Claude Code Issue Processor
|
|
21
22
|
|
|
22
23
|
USAGE:
|
|
23
|
-
bueller [OPTIONS]
|
|
24
|
+
bueller-wheel run [OPTIONS] Start the agent loop
|
|
25
|
+
bueller-wheel issue ISSUE... [OPTIONS] View issue summaries
|
|
26
|
+
|
|
27
|
+
COMMANDS:
|
|
28
|
+
run Start the agent loop to process issues
|
|
29
|
+
issue ISSUE... Summarize one or more issues (accepts file paths or filenames)
|
|
24
30
|
|
|
25
31
|
OPTIONS:
|
|
26
32
|
--help Show this help message and exit
|
|
33
|
+
--git Enable automatic git commits (on by default, run command only)
|
|
34
|
+
--no-git Disable automatic git commits (run command only)
|
|
35
|
+
--max N Maximum number of iterations to run (default: 25, run command only)
|
|
36
|
+
--continue [PROMPT] Continue from previous session (default prompt: "continue", run command only)
|
|
37
|
+
--index N Expand message at index N (issue command only)
|
|
38
|
+
--index M,N Expand message range from M to N (issue command only)
|
|
27
39
|
--issues-dir DIR Directory containing issue queue (default: ./issues)
|
|
28
40
|
--faq-dir DIR Directory containing FAQ/troubleshooting guides (default: ./faq)
|
|
29
|
-
--
|
|
30
|
-
--git, --git-commit Enable automatic git commits after each iteration
|
|
31
|
-
--prompt FILE Custom prompt template file (default: ./issues/prompt.md)
|
|
32
|
-
--continue [PROMPT] Continue from previous session (default prompt: "continue")
|
|
41
|
+
--prompt FILE Custom prompt template file (default: ./issues/prompt.md, run command only)
|
|
33
42
|
|
|
34
43
|
DIRECTORY STRUCTURE:
|
|
35
44
|
issues/
|
|
@@ -48,10 +57,14 @@ ISSUE FILE FORMAT:
|
|
|
48
57
|
p2: Non-blocking follow-up
|
|
49
58
|
|
|
50
59
|
EXAMPLES:
|
|
51
|
-
bueller
|
|
52
|
-
bueller
|
|
53
|
-
bueller --max
|
|
54
|
-
bueller --
|
|
60
|
+
bueller-wheel run
|
|
61
|
+
bueller-wheel run --no-git
|
|
62
|
+
bueller-wheel run --max 50
|
|
63
|
+
bueller-wheel run --continue "fix the bug"
|
|
64
|
+
bueller-wheel run --issues-dir ./my-issues --faq-dir ./my-faq
|
|
65
|
+
bueller-wheel issue p1-003-read-helper-002.md
|
|
66
|
+
bueller-wheel issue p1-003 p2-005 --index 1
|
|
67
|
+
bueller-wheel issue /path/to/issue.md --index 0,2
|
|
55
68
|
|
|
56
69
|
For more information, visit: https://github.com/anthropics/bueller
|
|
57
70
|
`);
|
|
@@ -59,18 +72,52 @@ For more information, visit: https://github.com/anthropics/bueller
|
|
|
59
72
|
function parseArgs() {
|
|
60
73
|
const args = process.argv.slice(2);
|
|
61
74
|
// Check for help flag first
|
|
62
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
75
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
63
76
|
showHelp();
|
|
64
77
|
process.exit(0);
|
|
65
78
|
}
|
|
79
|
+
// First argument should be the command
|
|
80
|
+
const command = args[0];
|
|
81
|
+
if (command !== 'run' && command !== 'issue') {
|
|
82
|
+
console.error(`${colors.red}Error: Unknown command "${command}". Use "run" or "issue".${colors.reset}\n`);
|
|
83
|
+
showHelp();
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
// Define recognized flags
|
|
87
|
+
const recognizedFlags = new Set([
|
|
88
|
+
'--issues-dir',
|
|
89
|
+
'--faq-dir',
|
|
90
|
+
'--max',
|
|
91
|
+
'--git',
|
|
92
|
+
'--no-git',
|
|
93
|
+
'--prompt',
|
|
94
|
+
'--continue',
|
|
95
|
+
'--index',
|
|
96
|
+
'--help',
|
|
97
|
+
'-h',
|
|
98
|
+
]);
|
|
99
|
+
// Check for unrecognized flags
|
|
100
|
+
for (const arg of args.slice(1)) {
|
|
101
|
+
// Check if this looks like a flag (starts with -)
|
|
102
|
+
if (arg.startsWith('-')) {
|
|
103
|
+
if (!recognizedFlags.has(arg)) {
|
|
104
|
+
console.error(`${colors.red}Error: Unrecognized flag: ${arg}${colors.reset}\n`);
|
|
105
|
+
showHelp();
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
66
110
|
let issuesDir = './issues';
|
|
67
111
|
let faqDir = './faq';
|
|
68
112
|
let maxIterations = 25;
|
|
69
|
-
let gitCommit =
|
|
113
|
+
let gitCommit = true;
|
|
70
114
|
let promptFile = path.join('./issues', 'prompt.md');
|
|
71
115
|
let continueMode = false;
|
|
72
116
|
let continuePrompt = 'continue';
|
|
73
|
-
|
|
117
|
+
const issueReferences = [];
|
|
118
|
+
let issueIndex;
|
|
119
|
+
// Parse arguments starting from index 1 (skip the command)
|
|
120
|
+
for (let i = 1; i < args.length; i++) {
|
|
74
121
|
if (args[i] === '--issues-dir' && i + 1 < args.length) {
|
|
75
122
|
issuesDir = args[++i];
|
|
76
123
|
// Update default prompt file location if issues-dir is changed
|
|
@@ -81,12 +128,15 @@ function parseArgs() {
|
|
|
81
128
|
else if (args[i] === '--faq-dir' && i + 1 < args.length) {
|
|
82
129
|
faqDir = args[++i];
|
|
83
130
|
}
|
|
84
|
-
else if (args[i] === '--max
|
|
131
|
+
else if (args[i] === '--max' && i + 1 < args.length) {
|
|
85
132
|
maxIterations = parseInt(args[++i], 10);
|
|
86
133
|
}
|
|
87
|
-
else if (args[i] === '--git'
|
|
134
|
+
else if (args[i] === '--git') {
|
|
88
135
|
gitCommit = true;
|
|
89
136
|
}
|
|
137
|
+
else if (args[i] === '--no-git') {
|
|
138
|
+
gitCommit = false;
|
|
139
|
+
}
|
|
90
140
|
else if (args[i] === '--prompt' && i + 1 < args.length) {
|
|
91
141
|
promptFile = args[++i];
|
|
92
142
|
}
|
|
@@ -97,6 +147,21 @@ function parseArgs() {
|
|
|
97
147
|
continuePrompt = args[++i];
|
|
98
148
|
}
|
|
99
149
|
}
|
|
150
|
+
else if (args[i] === '--index' && i + 1 < args.length) {
|
|
151
|
+
issueIndex = args[++i];
|
|
152
|
+
}
|
|
153
|
+
else if (!args[i].startsWith('--')) {
|
|
154
|
+
// Non-flag argument - collect as issue reference for issue command
|
|
155
|
+
if (command === 'issue') {
|
|
156
|
+
issueReferences.push(args[i]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Validate issue command
|
|
161
|
+
if (command === 'issue' && issueReferences.length === 0) {
|
|
162
|
+
console.error(`${colors.red}Error: "issue" command requires at least one issue reference.${colors.reset}\n`);
|
|
163
|
+
showHelp();
|
|
164
|
+
process.exit(1);
|
|
100
165
|
}
|
|
101
166
|
return {
|
|
102
167
|
issuesDir,
|
|
@@ -106,6 +171,9 @@ function parseArgs() {
|
|
|
106
171
|
promptFile,
|
|
107
172
|
continueMode,
|
|
108
173
|
continuePrompt,
|
|
174
|
+
command: command,
|
|
175
|
+
issueReferences,
|
|
176
|
+
issueIndex,
|
|
109
177
|
};
|
|
110
178
|
}
|
|
111
179
|
async function ensureDirectories(issuesDir, faqDir) {
|
|
@@ -214,9 +282,16 @@ Here is a summary of the work I have done:
|
|
|
214
282
|
|
|
215
283
|
Your issue file: [ISSUE_FILE_PATH]
|
|
216
284
|
|
|
285
|
+
Issue files may be long. Use CLI commands to read:
|
|
286
|
+
- To summarize: \`npx bueller-wheel issue [ISSUE_FILE_PATH]\`
|
|
287
|
+
- To expand: \`npx bueller-wheel issue [ISSUE_FILE_PATH] --index <start>,<end>\`
|
|
288
|
+
|
|
217
289
|
1. **Read the issue**: Parse the conversation history in [ISSUE_FILE_PATH] to understand the task
|
|
218
290
|
2. **Work on the task**: Do what the issue requests. When encountering issues, always check for a relevant guide in [FAQ_DIR]/ first.
|
|
219
|
-
3. **
|
|
291
|
+
3. **Verify**: Verify the following pass:
|
|
292
|
+
- [ ] \`pnpm run lint:fix\`
|
|
293
|
+
- [ ] \`pnpm run typecheck\`
|
|
294
|
+
4. **Append your response**: Add your summary to [ISSUE_FILE_PATH] using this format:
|
|
220
295
|
\`\`\`
|
|
221
296
|
---
|
|
222
297
|
|
|
@@ -228,7 +303,7 @@ Your issue file: [ISSUE_FILE_PATH]
|
|
|
228
303
|
- Item 3
|
|
229
304
|
\`\`\`
|
|
230
305
|
|
|
231
|
-
|
|
306
|
+
5. **Decide the outcome**: Choose ONE of the following actions:
|
|
232
307
|
|
|
233
308
|
a. **CONTINUE** - You made progress but the task isn't complete yet
|
|
234
309
|
- Leave the issue in \`[ISSUES_DIR]/[ISSUE_DIR_OPEN]/\` for the next iteration
|
|
@@ -259,6 +334,10 @@ Your issue file: [ISSUE_FILE_PATH]
|
|
|
259
334
|
|
|
260
335
|
**Critical:** ALWAYS check the FAQ directory ([FAQ_DIR]/) to see if there is a guide when you encounter a problem.
|
|
261
336
|
|
|
337
|
+
## Adding to the FAQ
|
|
338
|
+
|
|
339
|
+
Consider adding a **CONCISE** FAQ in [FAQ_DIR]/ for non-obvious solutions, recurring issues, or multi-step troubleshooting that would help future agents. Skip trivial/one-off problems or topics already documented.
|
|
340
|
+
|
|
262
341
|
Now, please process the issue at [ISSUE_FILE_PATH].`;
|
|
263
342
|
}
|
|
264
343
|
async function loadOrCreatePromptTemplate(promptFile) {
|
|
@@ -314,12 +393,16 @@ function logToolUse(block) {
|
|
|
314
393
|
case 'grep': {
|
|
315
394
|
const pattern = block.input?.pattern;
|
|
316
395
|
const glob = block.input?.glob;
|
|
396
|
+
const path = block.input?.path;
|
|
317
397
|
if (pattern) {
|
|
318
398
|
process.stdout.write(`${pattern}`);
|
|
319
399
|
}
|
|
320
400
|
if (glob) {
|
|
321
401
|
process.stdout.write(` (${glob})`);
|
|
322
402
|
}
|
|
403
|
+
if (path) {
|
|
404
|
+
process.stdout.write(` (${path})`);
|
|
405
|
+
}
|
|
323
406
|
break;
|
|
324
407
|
}
|
|
325
408
|
case 'todowrite': {
|
|
@@ -388,6 +471,13 @@ async function runAgent(options) {
|
|
|
388
471
|
settingSources: ['local', 'project', 'user'],
|
|
389
472
|
permissionMode: 'acceptEdits',
|
|
390
473
|
continue: continueMode,
|
|
474
|
+
canUseTool: async (toolName, input) => {
|
|
475
|
+
console.log(`${colors.red}Auto-denied tool:${colors.reset} ${toolName} ${String(input?.['command'] ?? input?.['file_path'] ?? '')}`);
|
|
476
|
+
return {
|
|
477
|
+
behavior: 'deny',
|
|
478
|
+
message: `You are running autonomously. The user cannot grant permission. Find a workaround or mark the issue as stuck. Check ${faqDir}/ to see if this is covered.`,
|
|
479
|
+
};
|
|
480
|
+
},
|
|
391
481
|
},
|
|
392
482
|
});
|
|
393
483
|
for await (const item of stream) {
|
|
@@ -395,8 +485,39 @@ async function runAgent(options) {
|
|
|
395
485
|
}
|
|
396
486
|
console.log(`${colors.blue}\n--- Agent finished ---${colors.reset}`);
|
|
397
487
|
}
|
|
488
|
+
async function runIssue(config) {
|
|
489
|
+
for (const issueRef of config.issueReferences) {
|
|
490
|
+
// Normalize issue reference - add .md extension if missing
|
|
491
|
+
let normalizedRef = issueRef;
|
|
492
|
+
if (!issueRef.endsWith('.md')) {
|
|
493
|
+
normalizedRef = `${issueRef}.md`;
|
|
494
|
+
}
|
|
495
|
+
const located = await resolveIssueReference(normalizedRef, config.issuesDir);
|
|
496
|
+
if (!located) {
|
|
497
|
+
console.error(`${colors.red}Error: Could not find issue: ${issueRef}${colors.reset}\n`);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
let summary = await summarizeIssue(located);
|
|
502
|
+
// Apply index expansion if specified
|
|
503
|
+
if (config.issueIndex) {
|
|
504
|
+
summary = expandMessages(summary, config.issueIndex);
|
|
505
|
+
}
|
|
506
|
+
const formatted = formatIssueSummary(summary, config.issueIndex);
|
|
507
|
+
console.log(formatted);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
console.error(`${colors.red}Error summarizing ${issueRef}: ${String(error)}${colors.reset}\n`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
398
514
|
async function main() {
|
|
399
515
|
const config = parseArgs();
|
|
516
|
+
// Handle issue command
|
|
517
|
+
if (config.command === 'issue') {
|
|
518
|
+
await runIssue(config);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
400
521
|
console.log(`${colors.cyan}Bueller? Bueller?${colors.reset}`);
|
|
401
522
|
console.log(`${colors.cyan}-----------------${colors.reset}`);
|
|
402
523
|
console.log(`Issues directory: ${config.issuesDir}`);
|
|
@@ -422,12 +543,14 @@ async function main() {
|
|
|
422
543
|
console.log(`Found ${openIssues.length} open issue(s)`);
|
|
423
544
|
console.log(`Next issue: ${openIssues[0]}`);
|
|
424
545
|
const currentIssue = openIssues[0];
|
|
546
|
+
// Only use continue mode on the first iteration
|
|
547
|
+
const isFirstIteration = iteration === 1;
|
|
425
548
|
await runAgent({
|
|
426
549
|
template: promptTemplate,
|
|
427
550
|
issuesDir: config.issuesDir,
|
|
428
551
|
faqDir: config.faqDir,
|
|
429
552
|
issueFile: currentIssue,
|
|
430
|
-
continueMode: config.continueMode,
|
|
553
|
+
continueMode: config.continueMode && isFirstIteration,
|
|
431
554
|
continuePrompt: config.continuePrompt,
|
|
432
555
|
});
|
|
433
556
|
// Auto-commit if enabled and there's a current issue
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
/**
|
|
3
|
+
* Parses an issue markdown file and extracts the conversation history
|
|
4
|
+
*
|
|
5
|
+
* @param filePath - Absolute path to the issue file
|
|
6
|
+
* @returns Parsed issue with message history
|
|
7
|
+
* @throws Error if file cannot be read or if the format is invalid
|
|
8
|
+
*/
|
|
9
|
+
export async function readIssue(filePath) {
|
|
10
|
+
let rawContent;
|
|
11
|
+
try {
|
|
12
|
+
rawContent = await fs.readFile(filePath, 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
throw new Error(`Failed to read issue file at ${filePath}: ${String(error)}`);
|
|
16
|
+
}
|
|
17
|
+
return parseIssueContent(rawContent);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parses issue content and extracts the conversation history
|
|
21
|
+
*
|
|
22
|
+
* @param content - Raw markdown content of the issue file
|
|
23
|
+
* @returns Parsed issue with message history
|
|
24
|
+
*/
|
|
25
|
+
export function parseIssueContent(content) {
|
|
26
|
+
const messages = [];
|
|
27
|
+
// Split by the separator (---)
|
|
28
|
+
const sections = content.split(/\n---\n/);
|
|
29
|
+
let messageIndex = 0;
|
|
30
|
+
for (const section of sections) {
|
|
31
|
+
const trimmedSection = section.trim();
|
|
32
|
+
// Skip empty sections
|
|
33
|
+
if (!trimmedSection) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Check if this section starts with @user: or @claude:
|
|
37
|
+
const userMatch = trimmedSection.match(/^@user:\s*([\s\S]*)$/);
|
|
38
|
+
const claudeMatch = trimmedSection.match(/^@claude:\s*([\s\S]*)$/);
|
|
39
|
+
if (userMatch) {
|
|
40
|
+
messages.push({
|
|
41
|
+
index: messageIndex++,
|
|
42
|
+
author: 'user',
|
|
43
|
+
content: userMatch[1].trim(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else if (claudeMatch) {
|
|
47
|
+
messages.push({
|
|
48
|
+
index: messageIndex++,
|
|
49
|
+
author: 'claude',
|
|
50
|
+
content: claudeMatch[1].trim(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// If no match, skip this section (handles malformed sections)
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
messages,
|
|
57
|
+
rawContent: content,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Gets the latest message from an issue
|
|
62
|
+
*
|
|
63
|
+
* @param issue - Parsed issue object
|
|
64
|
+
* @returns The most recent message, or undefined if no messages exist
|
|
65
|
+
*/
|
|
66
|
+
export function getLatestMessage(issue) {
|
|
67
|
+
if (issue.messages.length === 0) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return issue.messages[issue.messages.length - 1];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Gets all messages from a specific author
|
|
74
|
+
*
|
|
75
|
+
* @param issue - Parsed issue object
|
|
76
|
+
* @param author - Author to filter by ('user' or 'claude')
|
|
77
|
+
* @returns Array of messages from the specified author
|
|
78
|
+
*/
|
|
79
|
+
export function getMessagesByAuthor(issue, author) {
|
|
80
|
+
return issue.messages.filter((msg) => msg.author === author);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Formats a message for appending to an issue file
|
|
84
|
+
*
|
|
85
|
+
* @param author - Author of the message ('user' or 'claude')
|
|
86
|
+
* @param content - Content of the message
|
|
87
|
+
* @returns Formatted message string ready to append to an issue file
|
|
88
|
+
*/
|
|
89
|
+
export function formatMessage(author, content) {
|
|
90
|
+
return `---\n\n@${author}: ${content}`;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=issue-reader.js.map
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { parseIssueContent } from './issue-reader.js';
|
|
4
|
+
/**
|
|
5
|
+
* Searches for an issue file by filename across the issue directories
|
|
6
|
+
*
|
|
7
|
+
* @param filename - The issue filename (e.g., "p1-003-read-helper-002.md")
|
|
8
|
+
* @param issuesDir - Base issues directory
|
|
9
|
+
* @returns Located issue or null if not found
|
|
10
|
+
*/
|
|
11
|
+
export async function locateIssueFile(filename, issuesDir) {
|
|
12
|
+
const directories = [
|
|
13
|
+
{ dir: path.join(issuesDir, 'open'), status: 'open' },
|
|
14
|
+
{ dir: path.join(issuesDir, 'review'), status: 'review' },
|
|
15
|
+
{ dir: path.join(issuesDir, 'stuck'), status: 'stuck' },
|
|
16
|
+
];
|
|
17
|
+
for (const { dir, status } of directories) {
|
|
18
|
+
const filePath = path.join(dir, filename);
|
|
19
|
+
try {
|
|
20
|
+
await fs.access(filePath);
|
|
21
|
+
return {
|
|
22
|
+
filePath,
|
|
23
|
+
status,
|
|
24
|
+
filename,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// File doesn't exist in this directory, continue searching
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves an issue reference (either a full path or filename) to a located issue
|
|
35
|
+
*
|
|
36
|
+
* @param reference - File path or filename
|
|
37
|
+
* @param issuesDir - Base issues directory
|
|
38
|
+
* @returns Located issue or null if not found
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveIssueReference(reference, issuesDir) {
|
|
41
|
+
// Check if it looks like a path (contains path separators)
|
|
42
|
+
if (reference.includes('/') || reference.includes('\\')) {
|
|
43
|
+
// Treat as a file path (absolute or relative)
|
|
44
|
+
const absolutePath = path.isAbsolute(reference) ? reference : path.resolve(reference);
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(absolutePath);
|
|
47
|
+
// Determine status from path
|
|
48
|
+
let status = 'open';
|
|
49
|
+
if (absolutePath.includes('/review/')) {
|
|
50
|
+
status = 'review';
|
|
51
|
+
}
|
|
52
|
+
else if (absolutePath.includes('/stuck/')) {
|
|
53
|
+
status = 'stuck';
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
filePath: absolutePath,
|
|
57
|
+
status,
|
|
58
|
+
filename: path.basename(absolutePath),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Otherwise, treat it as a filename and search for it
|
|
66
|
+
return locateIssueFile(reference, issuesDir);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Abbreviates a message based on its position in the conversation
|
|
70
|
+
*
|
|
71
|
+
* @param message - The message to abbreviate
|
|
72
|
+
* @param _position - Position in conversation ('first', 'middle', or 'last')
|
|
73
|
+
* @param maxLength - Maximum length for the abbreviated content
|
|
74
|
+
* @returns Abbreviated message
|
|
75
|
+
*/
|
|
76
|
+
function abbreviateMessage(message, _position, maxLength) {
|
|
77
|
+
const fullContent = message.content;
|
|
78
|
+
let abbreviated = fullContent;
|
|
79
|
+
let isAbbreviated = false;
|
|
80
|
+
if (fullContent.length > maxLength) {
|
|
81
|
+
abbreviated = fullContent.substring(0, maxLength).trimEnd() + '…';
|
|
82
|
+
isAbbreviated = true;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
index: message.index,
|
|
86
|
+
author: message.author,
|
|
87
|
+
content: abbreviated,
|
|
88
|
+
isAbbreviated,
|
|
89
|
+
fullContent,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Creates abbreviated messages from a parsed issue
|
|
94
|
+
*
|
|
95
|
+
* @param messages - Array of issue messages
|
|
96
|
+
* @returns Array of abbreviated messages
|
|
97
|
+
*/
|
|
98
|
+
function createAbbreviatedMessages(messages) {
|
|
99
|
+
if (messages.length === 0) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
if (messages.length === 1) {
|
|
103
|
+
// Single message - use 230 char limit
|
|
104
|
+
return [abbreviateMessage(messages[0], 'first', 230)];
|
|
105
|
+
}
|
|
106
|
+
const result = [];
|
|
107
|
+
// First message - 230 chars
|
|
108
|
+
result.push(abbreviateMessage(messages[0], 'first', 230));
|
|
109
|
+
// Middle messages - 70 chars
|
|
110
|
+
for (let i = 1; i < messages.length - 1; i++) {
|
|
111
|
+
result.push(abbreviateMessage(messages[i], 'middle', 70));
|
|
112
|
+
}
|
|
113
|
+
// Last message - 230 chars
|
|
114
|
+
result.push(abbreviateMessage(messages[messages.length - 1], 'last', 230));
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Summarizes an issue file with abbreviated messages
|
|
119
|
+
*
|
|
120
|
+
* @param locatedIssue - Located issue information
|
|
121
|
+
* @returns Issue summary
|
|
122
|
+
*/
|
|
123
|
+
export async function summarizeIssue(locatedIssue) {
|
|
124
|
+
const content = await fs.readFile(locatedIssue.filePath, 'utf-8');
|
|
125
|
+
const parsed = parseIssueContent(content);
|
|
126
|
+
const abbreviatedMessages = createAbbreviatedMessages(parsed.messages);
|
|
127
|
+
return {
|
|
128
|
+
issue: locatedIssue,
|
|
129
|
+
abbreviatedMessages,
|
|
130
|
+
messageCount: parsed.messages.length,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Parses index specification (e.g., "3" or "1,3")
|
|
135
|
+
*
|
|
136
|
+
* @param indexSpec - Index specification string
|
|
137
|
+
* @returns Object with indices array and whether it's a single index, or null if invalid
|
|
138
|
+
*/
|
|
139
|
+
export function parseIndexSpec(indexSpec) {
|
|
140
|
+
const parts = indexSpec.split(',').map((s) => s.trim());
|
|
141
|
+
if (parts.length === 1) {
|
|
142
|
+
// Single index
|
|
143
|
+
const index = parseInt(parts[0], 10);
|
|
144
|
+
if (isNaN(index) || index < 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return { indices: [index], isSingleIndex: true };
|
|
148
|
+
}
|
|
149
|
+
if (parts.length === 2) {
|
|
150
|
+
// Range
|
|
151
|
+
const start = parseInt(parts[0], 10);
|
|
152
|
+
const end = parseInt(parts[1], 10);
|
|
153
|
+
if (isNaN(start) || isNaN(end) || start < 0 || end < start) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const indices = [];
|
|
157
|
+
for (let i = start; i <= end; i++) {
|
|
158
|
+
indices.push(i);
|
|
159
|
+
}
|
|
160
|
+
return { indices, isSingleIndex: false };
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Expands specific messages in a summary based on index specification
|
|
166
|
+
*
|
|
167
|
+
* @param summary - Issue summary
|
|
168
|
+
* @param indexSpec - Index specification (e.g., "3" or "1,3")
|
|
169
|
+
* @returns New summary with expanded messages and filter info
|
|
170
|
+
*/
|
|
171
|
+
export function expandMessages(summary, indexSpec) {
|
|
172
|
+
const parsed = parseIndexSpec(indexSpec);
|
|
173
|
+
if (!parsed) {
|
|
174
|
+
return summary;
|
|
175
|
+
}
|
|
176
|
+
const { indices, isSingleIndex } = parsed;
|
|
177
|
+
const expandedMessages = summary.abbreviatedMessages.map((msg) => {
|
|
178
|
+
if (indices.includes(msg.index)) {
|
|
179
|
+
return {
|
|
180
|
+
...msg,
|
|
181
|
+
content: msg.fullContent,
|
|
182
|
+
isAbbreviated: false,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return msg;
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
...summary,
|
|
189
|
+
abbreviatedMessages: expandedMessages,
|
|
190
|
+
filterToIndices: indices,
|
|
191
|
+
isSingleIndex,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Condenses text by trimming lines and replacing newlines with single spaces
|
|
196
|
+
*
|
|
197
|
+
* @param text - Text to condense
|
|
198
|
+
* @returns Condensed text
|
|
199
|
+
*/
|
|
200
|
+
function condenseText(text) {
|
|
201
|
+
return text
|
|
202
|
+
.split('\n')
|
|
203
|
+
.map((line) => line.trim())
|
|
204
|
+
.filter((line) => line.length > 0)
|
|
205
|
+
.join(' ');
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Formats an issue summary for console output
|
|
209
|
+
*
|
|
210
|
+
* @param summary - Issue summary (may include filterToIndices and isSingleIndex)
|
|
211
|
+
* @param indexSpec - Optional index specification for filtering display
|
|
212
|
+
* @returns Formatted string
|
|
213
|
+
*/
|
|
214
|
+
export function formatIssueSummary(summary, indexSpec) {
|
|
215
|
+
const lines = [];
|
|
216
|
+
// Header
|
|
217
|
+
const directory = `${summary.issue.status}/`;
|
|
218
|
+
const filename = summary.issue.filename;
|
|
219
|
+
lines.push(`${directory}${filename}`);
|
|
220
|
+
// Determine which messages to show
|
|
221
|
+
const messagesToShow = summary.filterToIndices
|
|
222
|
+
? summary.abbreviatedMessages.filter((msg) => summary.filterToIndices.includes(msg.index))
|
|
223
|
+
: summary.abbreviatedMessages;
|
|
224
|
+
// Messages
|
|
225
|
+
for (const msg of messagesToShow) {
|
|
226
|
+
const content = msg.isAbbreviated ? condenseText(msg.content) : msg.content;
|
|
227
|
+
lines.push(`[${msg.index}] @${msg.author}: ${content}`);
|
|
228
|
+
}
|
|
229
|
+
// Add follow-up action hint if not showing specific indices
|
|
230
|
+
if (!indexSpec || !summary.isSingleIndex) {
|
|
231
|
+
lines.push('');
|
|
232
|
+
lines.push('Pass `--index N` or `--index M,N` to see more.');
|
|
233
|
+
}
|
|
234
|
+
return lines.join('\n');
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=issue-summarize.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bueller-wheel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Headless Claude Code issue processor - A wrapper that runs Claude Code in a loop to process issues from a directory queue",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"bueller-wheel": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "rm -rf dist out && tsc && mkdir -p dist && cp out/src
|
|
11
|
+
"build": "rm -rf dist out && tsc && mkdir -p dist && cp out/src/*.js dist/ && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.tmp && mv dist/index.tmp dist/index.js && chmod +x dist/index.js",
|
|
12
12
|
"dev": "tsx src/index.ts",
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
14
|
"test": "tsx tests/test-runner.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"homepage": "https://github.com/fongandrew/bueller-wheel#readme",
|
|
39
39
|
"files": [
|
|
40
|
-
"dist
|
|
40
|
+
"dist/*.js",
|
|
41
41
|
"README.md",
|
|
42
42
|
"LICENSE"
|
|
43
43
|
],
|