bueller-wheel 0.1.0
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/LICENSE.md +7 -0
- package/README.md +244 -0
- package/dist/index.js +462 -0
- package/package.json +69 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Andrew Fong
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Bueller Wheel
|
|
2
|
+
|
|
3
|
+
A headless Claude Code issue processor that runs in a loop and resolves issues or tickets written in markdown
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Create an issue
|
|
9
|
+
mkdir -p issues/open
|
|
10
|
+
echo "@user: Please create a test file with 'Hello World'" > issues/open/p0-100-my-task.md
|
|
11
|
+
|
|
12
|
+
# Run bueller-wheel to complete the task
|
|
13
|
+
npx bueller-wheel
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Why?
|
|
17
|
+
|
|
18
|
+
Claude Code agents work better when they focus on one thing at a time. Bueller Wheel helps defer context until it's needed through two mechanisms:
|
|
19
|
+
|
|
20
|
+
**Issues as directory-based threads** - Each task becomes a reviewable conversation stored as markdown that gets moved along a file-based "kanban" board. This solves a few problems:
|
|
21
|
+
- **Code review**: When Claude burns through multiple tasks, it's hard to know what happened. The issues structure creates discrete, reviewable units of work.
|
|
22
|
+
- **Iteration without re-prompting**: Markdown files are structured as a back-and-forth between you and Claude, which lets you append follow-ups naturally without starting over.
|
|
23
|
+
- **Human-editable prompts**: Storing issues as simple markdown (not JSON or a database) makes it easy to edit, append, or retroactively clean up conversations to improve prompting.
|
|
24
|
+
|
|
25
|
+
**FAQ system** - Common mistakes get documented once, then referenced automatically. Instead of repeating corrections or watching Claude make the same errors over and over, capture solutions in FAQ files that the agent checks when stuck.
|
|
26
|
+
|
|
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**, but you can make [parallel branches and agents](#working-with-multiple-branches) work.
|
|
28
|
+
|
|
29
|
+
## How It Works
|
|
30
|
+
|
|
31
|
+
**The Processing Loop**
|
|
32
|
+
|
|
33
|
+
1. Bueller Wheel finds the next issue in `issues/open/` (sorted alphabetically by filename)
|
|
34
|
+
2. Claude Code reads the issue and works on the task
|
|
35
|
+
3. Claude appends its work summary to the issue file
|
|
36
|
+
4. Claude decides the outcome:
|
|
37
|
+
- **CONTINUE**: Keep working (stays in `open/`)
|
|
38
|
+
- **COMPLETE**: Done (moves to `review/`)
|
|
39
|
+
- **DECOMPOSE**: Split into sub-tasks (creates child issues, moves parent to `review/`)
|
|
40
|
+
- **STUCK**: Needs help (moves to `stuck/`)
|
|
41
|
+
|
|
42
|
+
**Managing the "kanban" board:**
|
|
43
|
+
|
|
44
|
+
- Create issue markdown in `open/` for each task you want Claude to work on. Name files alphabetically in the order you want them processed. By default, the prompt used tells our agent to name files with something like `p1-101-name-of-task.md` (`p1` is a priority between 0 to 2, and `101` is just an arbitrary number for ordering).
|
|
45
|
+
- Move issues from `review/` or `stuck/` back to `open/` if more work is required
|
|
46
|
+
- Delete issues from `review/` when done reviewing, or archive them however you want
|
|
47
|
+
|
|
48
|
+
**Each iteration is a fresh Claude Code session** - no memory between iterations, which keeps context focused.
|
|
49
|
+
|
|
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.
|
|
51
|
+
|
|
52
|
+
## Issue Structure
|
|
53
|
+
|
|
54
|
+
**Directory Layout**
|
|
55
|
+
```
|
|
56
|
+
issues/
|
|
57
|
+
open/ - Issues to be processed
|
|
58
|
+
review/ - Completed issues ready for human review
|
|
59
|
+
stuck/ - Issues blocked, requiring human intervention
|
|
60
|
+
prompt.md - Custom prompt template (optional)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Filename Format:** `p{priority}-{order}-{description}.md`
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
- `p0-100-fix-critical-bug.md` (urgent work)
|
|
67
|
+
- `p1-050-add-feature.md` (normal feature work)
|
|
68
|
+
- `p2-020-refactor-code.md` (non-blocking follow-up)
|
|
69
|
+
|
|
70
|
+
Files are processed alphabetically (p0 before p1, lower order numbers first).
|
|
71
|
+
|
|
72
|
+
**Issue Content: A Conversation**
|
|
73
|
+
|
|
74
|
+
Issues are markdown files with a simple conversation structure:
|
|
75
|
+
|
|
76
|
+
```markdown
|
|
77
|
+
@user: Please build the widget factory.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
@claude: I have built the widget factory.
|
|
82
|
+
|
|
83
|
+
Here is a summary of the work I have done:
|
|
84
|
+
- Created WidgetFactory class
|
|
85
|
+
- Added unit tests
|
|
86
|
+
- Updated documentation
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
@user: Please add error handling.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
@claude: I have added error handling.
|
|
95
|
+
|
|
96
|
+
Here is a summary of the work I have done:
|
|
97
|
+
- Added try-catch blocks
|
|
98
|
+
- Added custom error types
|
|
99
|
+
- Updated tests
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Because it's just markdown, you can:
|
|
103
|
+
- Append new instructions at any time
|
|
104
|
+
- Edit previous messages to clarify intent
|
|
105
|
+
- Delete irrelevant parts of the conversation
|
|
106
|
+
- Copy successful patterns to new issues
|
|
107
|
+
|
|
108
|
+
## FAQ System
|
|
109
|
+
|
|
110
|
+
The `faq/` directory contains markdown files with solutions to common problems. When Claude encounters issues, it's instructed to check this directory for relevant guidance.
|
|
111
|
+
|
|
112
|
+
**Why this helps:**
|
|
113
|
+
- Document a fix once, reference it forever
|
|
114
|
+
- Keep Claude on track with project-specific conventions
|
|
115
|
+
- Reduce repeated mistakes without cluttering every issue prompt
|
|
116
|
+
|
|
117
|
+
**Example FAQ structure:**
|
|
118
|
+
```
|
|
119
|
+
faq/
|
|
120
|
+
testing-guidelines.md
|
|
121
|
+
code-style.md
|
|
122
|
+
common-errors.md
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Configure the location with `--faq-dir` (default: `./faq`)
|
|
126
|
+
|
|
127
|
+
## Working with Multiple Branches
|
|
128
|
+
|
|
129
|
+
The `open/` directory acts as an inbox for the agent on the current branch. The agent will attempt to burn through all issues in that directory (or until it hits `--max-iterations`).
|
|
130
|
+
|
|
131
|
+
**If you want to divide work across multiple branches or run multiple agents in parallel:**
|
|
132
|
+
|
|
133
|
+
1. **Track issues outside `open/`** - Use an external issue tracker or create an `issues/backlog/` directory. Bueller Wheel doesn't process anything except in the `open/` directory.
|
|
134
|
+
|
|
135
|
+
2. **One branch per agent** - Create a separate branch (or checkout/worktree) for each agent working in parallel.
|
|
136
|
+
|
|
137
|
+
3. **Move issues into `open/` as tasks for a single agent** - Only move issues from your backlog into `open/` that you want the current agent to work on. Think of it as dividing work into reviewable chunks that are easier to merge.
|
|
138
|
+
|
|
139
|
+
## CLI Options
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx bueller-wheel --issues-dir ./my-issues --faq-dir ./my-faq --max-iterations 50 --git-commit --prompt ./my-prompt.md
|
|
143
|
+
|
|
144
|
+
# Or if installed globally
|
|
145
|
+
bueller-wheel --issues-dir ./my-issues --faq-dir ./my-faq --max-iterations 50 --git-commit --prompt ./my-prompt.md
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- `--issues-dir <path>`: Issues directory (default: `./issues`)
|
|
149
|
+
- `--faq-dir <path>`: FAQ directory (default: `./faq`)
|
|
150
|
+
- `--max-iterations <number>`: Maximum iterations (default: `25`)
|
|
151
|
+
- `--git-commit`: Enable automatic git commits after each iteration (default: disabled)
|
|
152
|
+
- `--prompt <path>`: Path to custom prompt template file (default: `<issues-dir>/prompt.md`)
|
|
153
|
+
- `--continue [prompt]`: Continue from previous session. Optional prompt defaults to "continue" if not provided
|
|
154
|
+
|
|
155
|
+
### Custom Prompt Templates
|
|
156
|
+
|
|
157
|
+
Bueller Wheel uses a customizable prompt template system that allows you to tailor the agent's behavior.
|
|
158
|
+
|
|
159
|
+
#### How It Works
|
|
160
|
+
|
|
161
|
+
1. **Default Template**: On first run, Bueller Wheel creates a default prompt template at `<issues-dir>/prompt.md`
|
|
162
|
+
2. **Custom Template**: You can edit this file or specify a different template with `--prompt`
|
|
163
|
+
3. **Template Variables**: The template uses bracketed variables that are replaced at runtime
|
|
164
|
+
|
|
165
|
+
#### Template Variables
|
|
166
|
+
|
|
167
|
+
The following variables are available in your prompt template:
|
|
168
|
+
|
|
169
|
+
- `[ISSUES_DIR]` - The issues directory path (e.g., `./issues`)
|
|
170
|
+
- `[FAQ_DIR]` - The FAQ directory path (e.g., `./faq`)
|
|
171
|
+
- `[ISSUE_DIR_OPEN]` - The open subdirectory name (always `open`)
|
|
172
|
+
- `[ISSUE_DIR_REVIEW]` - The review subdirectory name (always `review`)
|
|
173
|
+
- `[ISSUE_DIR_STUCK]` - The stuck subdirectory name (always `stuck`)
|
|
174
|
+
- `[ISSUE_FILE_PATH]` - Full path to the current issue file (e.g., `./issues/open/p0-100-task.md`)
|
|
175
|
+
- `[ISSUE_FILE]` - Just the issue filename (e.g., `p0-100-task.md`)
|
|
176
|
+
|
|
177
|
+
#### Example Usage
|
|
178
|
+
|
|
179
|
+
```markdown
|
|
180
|
+
Your task is to process: [ISSUE_FILE_PATH]
|
|
181
|
+
|
|
182
|
+
When complete, move it to: [ISSUES_DIR]/[ISSUE_DIR_REVIEW]/[ISSUE_FILE]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
This will be rendered as:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
Your task is to process: ./issues/open/p0-100-task.md
|
|
189
|
+
|
|
190
|
+
When complete, move it to: ./issues/review/p0-100-task.md
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Creating a Custom Prompt
|
|
194
|
+
|
|
195
|
+
1. Copy the default template from `issues/prompt.md`
|
|
196
|
+
2. Modify the instructions while keeping the template variables
|
|
197
|
+
3. Optionally save it to a different location and use `--prompt` to specify it
|
|
198
|
+
|
|
199
|
+
### Continue Mode
|
|
200
|
+
|
|
201
|
+
The `--continue` flag continues from the last Claude session with a custom prompt. You can use this to interrupt a live loop that's going sideways and nudge it back on track.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Continue with default "continue" prompt
|
|
205
|
+
npx bueller-wheel --continue
|
|
206
|
+
|
|
207
|
+
# Continue with custom instructions
|
|
208
|
+
npx bueller-wheel --continue "no use foo instead of bar"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Note that only the immediate prior iteration is continued. The next iteration will start with a fresh context and the original prompt.
|
|
212
|
+
|
|
213
|
+
### Git Auto-Commit
|
|
214
|
+
|
|
215
|
+
When `--git-commit` is enabled, Bueller Wheel will automatically create a git commit after each iteration where work was done on an issue.
|
|
216
|
+
|
|
217
|
+
The commit message format includes the issue ID (the filename minus the `.md`) and status:
|
|
218
|
+
```
|
|
219
|
+
p0-002-git done
|
|
220
|
+
p0-002-git in progress
|
|
221
|
+
p0-002-git stuck
|
|
222
|
+
p0-002-git unknown
|
|
223
|
+
```
|
|
224
|
+
## Development
|
|
225
|
+
|
|
226
|
+
`pnpm run dev` will execute the current `src/index.ts` script file with whatever args you pass to it.
|
|
227
|
+
|
|
228
|
+
## End-to-End Testing
|
|
229
|
+
|
|
230
|
+
**These tests use your actual live instance of Claude Code!**
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
# Run all tests
|
|
234
|
+
pnpm test
|
|
235
|
+
|
|
236
|
+
# Run a specific test
|
|
237
|
+
./tests/run-test.sh simple-task
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Tests are located in `tests/fixtures/` and consist of:
|
|
241
|
+
- Pre-configured issue directories with markdown files
|
|
242
|
+
- Verification scripts to check outcomes
|
|
243
|
+
|
|
244
|
+
See [tests/README.md](tests/README.md) for more details on creating new test cases.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
6
|
+
const ISSUE_DIR_OPEN = 'open';
|
|
7
|
+
const ISSUE_DIR_REVIEW = 'review';
|
|
8
|
+
const ISSUE_DIR_STUCK = 'stuck';
|
|
9
|
+
function showHelp() {
|
|
10
|
+
console.log(`
|
|
11
|
+
Bueller - Headless Claude Code Issue Processor
|
|
12
|
+
|
|
13
|
+
USAGE:
|
|
14
|
+
bueller [OPTIONS]
|
|
15
|
+
|
|
16
|
+
OPTIONS:
|
|
17
|
+
--help Show this help message and exit
|
|
18
|
+
--issues-dir DIR Directory containing issue queue (default: ./issues)
|
|
19
|
+
--faq-dir DIR Directory containing FAQ/troubleshooting guides (default: ./faq)
|
|
20
|
+
--max-iterations N Maximum number of iterations to run (default: 25)
|
|
21
|
+
--git, --git-commit Enable automatic git commits after each iteration
|
|
22
|
+
--prompt FILE Custom prompt template file (default: ./issues/prompt.md)
|
|
23
|
+
--continue [PROMPT] Continue from previous session (default prompt: "continue")
|
|
24
|
+
|
|
25
|
+
DIRECTORY STRUCTURE:
|
|
26
|
+
issues/
|
|
27
|
+
open/ Issues to be processed
|
|
28
|
+
review/ Completed issues
|
|
29
|
+
stuck/ Issues requiring human intervention
|
|
30
|
+
prompt.md Custom prompt template (optional)
|
|
31
|
+
faq/ FAQ and troubleshooting guides
|
|
32
|
+
|
|
33
|
+
ISSUE FILE FORMAT:
|
|
34
|
+
Issues are markdown files named: p{priority}-{order}-{description}.md
|
|
35
|
+
|
|
36
|
+
Priority levels:
|
|
37
|
+
p0: Urgent/unexpected work
|
|
38
|
+
p1: Normal feature work
|
|
39
|
+
p2: Non-blocking follow-up
|
|
40
|
+
|
|
41
|
+
EXAMPLES:
|
|
42
|
+
bueller
|
|
43
|
+
bueller --issues-dir ./my-issues --faq-dir ./my-faq
|
|
44
|
+
bueller --max-iterations 50 --git-commit
|
|
45
|
+
bueller --prompt ./custom-prompt.md
|
|
46
|
+
|
|
47
|
+
For more information, visit: https://github.com/anthropics/bueller
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
function parseArgs() {
|
|
51
|
+
const args = process.argv.slice(2);
|
|
52
|
+
// Check for help flag first
|
|
53
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
54
|
+
showHelp();
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
let issuesDir = './issues';
|
|
58
|
+
let faqDir = './faq';
|
|
59
|
+
let maxIterations = 25;
|
|
60
|
+
let gitCommit = false;
|
|
61
|
+
let promptFile = path.join('./issues', 'prompt.md');
|
|
62
|
+
let continueMode = false;
|
|
63
|
+
let continuePrompt = 'continue';
|
|
64
|
+
for (let i = 0; i < args.length; i++) {
|
|
65
|
+
if (args[i] === '--issues-dir' && i + 1 < args.length) {
|
|
66
|
+
issuesDir = args[++i];
|
|
67
|
+
// Update default prompt file location if issues-dir is changed
|
|
68
|
+
if (!args.includes('--prompt')) {
|
|
69
|
+
promptFile = path.join(issuesDir, 'prompt.md');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (args[i] === '--faq-dir' && i + 1 < args.length) {
|
|
73
|
+
faqDir = args[++i];
|
|
74
|
+
}
|
|
75
|
+
else if (args[i] === '--max-iterations' && i + 1 < args.length) {
|
|
76
|
+
maxIterations = parseInt(args[++i], 10);
|
|
77
|
+
}
|
|
78
|
+
else if (args[i] === '--git' || args[i] === '--git-commit') {
|
|
79
|
+
gitCommit = true;
|
|
80
|
+
}
|
|
81
|
+
else if (args[i] === '--prompt' && i + 1 < args.length) {
|
|
82
|
+
promptFile = args[++i];
|
|
83
|
+
}
|
|
84
|
+
else if (args[i] === '--continue') {
|
|
85
|
+
continueMode = true;
|
|
86
|
+
// Check if next arg exists and doesn't start with --
|
|
87
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
88
|
+
continuePrompt = args[++i];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
issuesDir,
|
|
94
|
+
faqDir,
|
|
95
|
+
maxIterations,
|
|
96
|
+
gitCommit,
|
|
97
|
+
promptFile,
|
|
98
|
+
continueMode,
|
|
99
|
+
continuePrompt,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function ensureDirectories(issuesDir, faqDir) {
|
|
103
|
+
const dirs = [
|
|
104
|
+
issuesDir,
|
|
105
|
+
path.join(issuesDir, ISSUE_DIR_OPEN),
|
|
106
|
+
path.join(issuesDir, ISSUE_DIR_REVIEW),
|
|
107
|
+
path.join(issuesDir, ISSUE_DIR_STUCK),
|
|
108
|
+
faqDir,
|
|
109
|
+
];
|
|
110
|
+
for (const dir of dirs) {
|
|
111
|
+
try {
|
|
112
|
+
await fs.access(dir);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
await fs.mkdir(dir, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function getOpenIssues(issuesDir) {
|
|
120
|
+
const openDir = path.join(issuesDir, ISSUE_DIR_OPEN);
|
|
121
|
+
const files = await fs.readdir(openDir);
|
|
122
|
+
return files.filter((f) => f.endsWith('.md')).sort();
|
|
123
|
+
}
|
|
124
|
+
function extractIssueId(issueFile) {
|
|
125
|
+
// Extract ID from filename like "p0-002-git.md" -> "p0-002-git"
|
|
126
|
+
return issueFile.replace('.md', '');
|
|
127
|
+
}
|
|
128
|
+
function gitCommit(issueFile, status) {
|
|
129
|
+
const issueId = extractIssueId(issueFile);
|
|
130
|
+
try {
|
|
131
|
+
// Check if there are any changes to commit
|
|
132
|
+
try {
|
|
133
|
+
execSync('git diff --quiet && git diff --cached --quiet');
|
|
134
|
+
// Also check for untracked files
|
|
135
|
+
const untrackedFiles = execSync('git ls-files --others --exclude-standard', {
|
|
136
|
+
encoding: 'utf-8',
|
|
137
|
+
}).trim();
|
|
138
|
+
if (!untrackedFiles) {
|
|
139
|
+
console.log('No changes to commit');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// There are changes, proceed with commit
|
|
145
|
+
}
|
|
146
|
+
// Stage all changes
|
|
147
|
+
execSync('git add -A', { stdio: 'inherit' });
|
|
148
|
+
// Create commit with issue ID and status
|
|
149
|
+
const commitMessage = `${issueId} ${status}`;
|
|
150
|
+
execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
|
|
151
|
+
stdio: 'inherit',
|
|
152
|
+
});
|
|
153
|
+
console.log(`\nGit commit created: ${commitMessage}`);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error(`Failed to create git commit: ${String(error)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function getDefaultPromptTemplate() {
|
|
160
|
+
return `You are a task automation agent processing issues from a queue.
|
|
161
|
+
|
|
162
|
+
## Your Environment
|
|
163
|
+
|
|
164
|
+
- [ISSUES_DIR]/[ISSUE_DIR_OPEN]/ - Issues to be processed
|
|
165
|
+
- [ISSUES_DIR]/[ISSUE_DIR_REVIEW]/ - Completed issues
|
|
166
|
+
- [ISSUES_DIR]/[ISSUE_DIR_STUCK]/ - Issues requiring human intervention
|
|
167
|
+
- [FAQ_DIR]/ - Troubleshooting
|
|
168
|
+
|
|
169
|
+
## Issue File Format
|
|
170
|
+
|
|
171
|
+
Issues are markdown files named: \`p{priority}-{order}-{description}.md\`
|
|
172
|
+
|
|
173
|
+
Examples:
|
|
174
|
+
- \`p0-100-fix-critical-bug.md\` (priority 0, order 100)
|
|
175
|
+
- \`p1-050-add-feature.md\` (priority 1, order 50)
|
|
176
|
+
|
|
177
|
+
Priority scheme:
|
|
178
|
+
- p0: Urgent/unexpected work
|
|
179
|
+
- p1: Normal feature work
|
|
180
|
+
- p2: Non-blocking follow-up
|
|
181
|
+
|
|
182
|
+
Issue files contain a conversation in this format:
|
|
183
|
+
|
|
184
|
+
\`\`\`
|
|
185
|
+
@user: Please build the widget factory.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
@claude: I have built the widget factory.
|
|
190
|
+
|
|
191
|
+
Here is a summary of the work I have done:
|
|
192
|
+
- Item 1
|
|
193
|
+
- Item 2
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
@user: Here is feedback on your work.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
@claude: I have implemented your feedback.
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
## Your Task for This Iteration
|
|
205
|
+
|
|
206
|
+
Your issue file: [ISSUE_FILE_PATH]
|
|
207
|
+
|
|
208
|
+
1. **Read the issue**: Parse the conversation history in [ISSUE_FILE_PATH] to understand the task
|
|
209
|
+
2. **Work on the task**: Do what the issue requests. When encountering issues, always check for a relevant guide in [FAQ_DIR]/ first.
|
|
210
|
+
3. **Append your response**: Add your summary to [ISSUE_FILE_PATH] using this format:
|
|
211
|
+
\`\`\`
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
@claude: [Your summary here]
|
|
215
|
+
|
|
216
|
+
Here is a summary of the work I have done:
|
|
217
|
+
- Item 1
|
|
218
|
+
- Item 2
|
|
219
|
+
- Item 3
|
|
220
|
+
\`\`\`
|
|
221
|
+
|
|
222
|
+
4. **Decide the outcome**: Choose ONE of the following actions:
|
|
223
|
+
|
|
224
|
+
a. **CONTINUE** - You made progress but the task isn't complete yet
|
|
225
|
+
- Leave the issue in \`[ISSUES_DIR]/[ISSUE_DIR_OPEN]/\` for the next iteration
|
|
226
|
+
- Use this when you need multiple iterations to complete a complex task
|
|
227
|
+
|
|
228
|
+
b. **COMPLETE** - The task is fully finished
|
|
229
|
+
- Move the issue to \`[ISSUES_DIR]/[ISSUE_DIR_REVIEW]/\` using: \`mv "[ISSUE_FILE_PATH]" "[ISSUES_DIR]/[ISSUE_DIR_REVIEW]/[ISSUE_FILE]"\`
|
|
230
|
+
|
|
231
|
+
c. **DECOMPOSE** - The task is too large and should be broken into smaller sub-tasks
|
|
232
|
+
- Create child issues in \`[ISSUES_DIR]/[ISSUE_DIR_OPEN]/\` with \`-001.md\`, \`-002.md\` suffixes
|
|
233
|
+
- Each child issue should start with: \`@user: [clear, actionable task description]\`
|
|
234
|
+
- Example: If parent is \`p1-050-add-auth.md\`, create:
|
|
235
|
+
- \`p1-050-add-auth-001.md\` for subtask 1
|
|
236
|
+
- \`p1-050-add-auth-002.md\` for subtask 2
|
|
237
|
+
- Move the parent issue to \`[ISSUES_DIR]/[ISSUE_DIR_REVIEW]/\`
|
|
238
|
+
|
|
239
|
+
d. **STUCK** - You cannot proceed without human intervention
|
|
240
|
+
- Explain clearly why you're stuck in your summary
|
|
241
|
+
- Move the issue to \`[ISSUES_DIR]/[ISSUE_DIR_STUCK]/\` using: \`mv "[ISSUE_FILE_PATH]" "[ISSUES_DIR]/[ISSUE_DIR_STUCK]/[ISSUE_FILE]"\`
|
|
242
|
+
|
|
243
|
+
## Important Notes
|
|
244
|
+
|
|
245
|
+
- Each invocation of this script is a separate session - you won't remember previous iterations
|
|
246
|
+
- Always read the full conversation history in the issue file to understand context
|
|
247
|
+
- Be thoughtful about when to CONTINUE vs COMPLETE - don't leave trivial tasks incomplete
|
|
248
|
+
- When creating child issues, make each one focused and actionable
|
|
249
|
+
- Use bash commands (mv, cat, echo) to manage files - you have full filesystem access
|
|
250
|
+
|
|
251
|
+
**Critical:** ALWAYS check the FAQ directory ([FAQ_DIR]/) to see if there is a guide when you encounter a problem.
|
|
252
|
+
|
|
253
|
+
Now, please process the issue at [ISSUE_FILE_PATH].`;
|
|
254
|
+
}
|
|
255
|
+
async function loadOrCreatePromptTemplate(promptFile) {
|
|
256
|
+
// If prompt file exists, load it
|
|
257
|
+
try {
|
|
258
|
+
await fs.access(promptFile);
|
|
259
|
+
console.log(`Loading prompt template from: ${promptFile}`);
|
|
260
|
+
return await fs.readFile(promptFile, 'utf-8');
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Otherwise, create the default prompt template
|
|
264
|
+
console.log(`Prompt file not found. Creating default template at: ${promptFile}`);
|
|
265
|
+
const defaultTemplate = getDefaultPromptTemplate();
|
|
266
|
+
// Ensure the directory exists
|
|
267
|
+
const promptDir = path.dirname(promptFile);
|
|
268
|
+
await fs.mkdir(promptDir, { recursive: true });
|
|
269
|
+
// Write the default template
|
|
270
|
+
await fs.writeFile(promptFile, defaultTemplate, 'utf-8');
|
|
271
|
+
return defaultTemplate;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function buildSystemPrompt(template, issuesDir, faqDir, issueFile) {
|
|
275
|
+
const issueFilePath = path.join(issuesDir, ISSUE_DIR_OPEN, issueFile);
|
|
276
|
+
// Convert all paths to absolute paths for clarity in the prompt
|
|
277
|
+
const absoluteIssuesDir = path.resolve(issuesDir);
|
|
278
|
+
const absoluteFaqDir = path.resolve(faqDir);
|
|
279
|
+
const absoluteIssueFilePath = path.resolve(issueFilePath);
|
|
280
|
+
// Replace template variables with actual values
|
|
281
|
+
return template
|
|
282
|
+
.replace(/\[ISSUES_DIR\]/g, absoluteIssuesDir)
|
|
283
|
+
.replace(/\[FAQ_DIR\]/g, absoluteFaqDir)
|
|
284
|
+
.replace(/\[ISSUE_DIR_OPEN\]/g, ISSUE_DIR_OPEN)
|
|
285
|
+
.replace(/\[ISSUE_DIR_REVIEW\]/g, ISSUE_DIR_REVIEW)
|
|
286
|
+
.replace(/\[ISSUE_DIR_STUCK\]/g, ISSUE_DIR_STUCK)
|
|
287
|
+
.replace(/\[ISSUE_FILE_PATH\]/g, absoluteIssueFilePath)
|
|
288
|
+
.replace(/\[ISSUE_FILE\]/g, issueFile);
|
|
289
|
+
}
|
|
290
|
+
function logToolUse(block) {
|
|
291
|
+
process.stdout.write('\n');
|
|
292
|
+
process.stdout.write(`[${block.name}] `);
|
|
293
|
+
switch (block.name.toLowerCase()) {
|
|
294
|
+
case 'read':
|
|
295
|
+
case 'write':
|
|
296
|
+
case 'edit':
|
|
297
|
+
process.stdout.write(`${block.input?.file_path}`);
|
|
298
|
+
break;
|
|
299
|
+
case 'bash':
|
|
300
|
+
process.stdout.write(`${block.input?.command}`);
|
|
301
|
+
break;
|
|
302
|
+
case 'glob':
|
|
303
|
+
process.stdout.write(`${block.input?.pattern}`);
|
|
304
|
+
break;
|
|
305
|
+
case 'todowrite': {
|
|
306
|
+
for (const todo of block.input?.todos ?? []) {
|
|
307
|
+
process.stdout.write('\n');
|
|
308
|
+
switch (todo.status) {
|
|
309
|
+
case 'in_progress':
|
|
310
|
+
process.stdout.write('⧖');
|
|
311
|
+
break;
|
|
312
|
+
case 'pending':
|
|
313
|
+
process.stdout.write('☐');
|
|
314
|
+
break;
|
|
315
|
+
case 'completed':
|
|
316
|
+
process.stdout.write('✓');
|
|
317
|
+
break;
|
|
318
|
+
default:
|
|
319
|
+
process.stdout.write(todo.status);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
process.stdout.write(' ');
|
|
323
|
+
process.stdout.write(String(todo.content));
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
default:
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
process.stdout.write('\n');
|
|
331
|
+
}
|
|
332
|
+
function logSDKMessage(item) {
|
|
333
|
+
switch (item.type) {
|
|
334
|
+
case 'assistant':
|
|
335
|
+
case 'user':
|
|
336
|
+
for (const chunk of item.message.content) {
|
|
337
|
+
if (typeof chunk === 'string') {
|
|
338
|
+
process.stdout.write('\n');
|
|
339
|
+
process.stdout.write(chunk);
|
|
340
|
+
process.stdout.write('\n');
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
switch (chunk.type) {
|
|
344
|
+
case 'text':
|
|
345
|
+
process.stdout.write('\n');
|
|
346
|
+
process.stdout.write(chunk.text);
|
|
347
|
+
process.stdout.write('\n');
|
|
348
|
+
break;
|
|
349
|
+
case 'tool_use':
|
|
350
|
+
logToolUse(chunk);
|
|
351
|
+
break;
|
|
352
|
+
default:
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
default:
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function runAgent(options) {
|
|
362
|
+
const { template, issuesDir, faqDir, issueFile, continueMode, continuePrompt } = options;
|
|
363
|
+
const systemPrompt = buildSystemPrompt(template, issuesDir, faqDir, issueFile);
|
|
364
|
+
console.log('\n--- Starting agent ---');
|
|
365
|
+
const stream = query({
|
|
366
|
+
prompt: continueMode ? continuePrompt : systemPrompt,
|
|
367
|
+
options: {
|
|
368
|
+
settingSources: ['local', 'project', 'user'],
|
|
369
|
+
permissionMode: 'acceptEdits',
|
|
370
|
+
continue: continueMode,
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
for await (const item of stream) {
|
|
374
|
+
logSDKMessage(item);
|
|
375
|
+
}
|
|
376
|
+
console.log('\n--- Agent finished ---');
|
|
377
|
+
}
|
|
378
|
+
async function main() {
|
|
379
|
+
const config = parseArgs();
|
|
380
|
+
console.log('Bueller? Bueller?');
|
|
381
|
+
console.log('-----------------');
|
|
382
|
+
console.log(`Issues directory: ${config.issuesDir}`);
|
|
383
|
+
console.log(`FAQ directory: ${config.faqDir}`);
|
|
384
|
+
console.log(`Max iterations: ${config.maxIterations}`);
|
|
385
|
+
console.log(`Git auto-commit: ${config.gitCommit ? 'enabled' : 'disabled'}`);
|
|
386
|
+
console.log(`Prompt file: ${config.promptFile}`);
|
|
387
|
+
if (config.continueMode) {
|
|
388
|
+
console.log(`Continue mode: enabled (prompt: "${config.continuePrompt}")`);
|
|
389
|
+
}
|
|
390
|
+
await ensureDirectories(config.issuesDir, config.faqDir);
|
|
391
|
+
// Load or create the prompt template
|
|
392
|
+
const promptTemplate = await loadOrCreatePromptTemplate(config.promptFile);
|
|
393
|
+
let iteration = 0;
|
|
394
|
+
while (iteration < config.maxIterations) {
|
|
395
|
+
iteration++;
|
|
396
|
+
console.log(`\n### Iteration ${iteration} ###\n`);
|
|
397
|
+
const openIssues = await getOpenIssues(config.issuesDir);
|
|
398
|
+
if (openIssues.length === 0) {
|
|
399
|
+
console.log('No more issues in open/. Exiting.');
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
console.log(`Found ${openIssues.length} open issue(s)`);
|
|
403
|
+
console.log(`Next issue: ${openIssues[0]}`);
|
|
404
|
+
const currentIssue = openIssues[0];
|
|
405
|
+
await runAgent({
|
|
406
|
+
template: promptTemplate,
|
|
407
|
+
issuesDir: config.issuesDir,
|
|
408
|
+
faqDir: config.faqDir,
|
|
409
|
+
issueFile: currentIssue,
|
|
410
|
+
continueMode: config.continueMode,
|
|
411
|
+
continuePrompt: config.continuePrompt,
|
|
412
|
+
});
|
|
413
|
+
// Auto-commit if enabled and there's a current issue
|
|
414
|
+
if (config.gitCommit && currentIssue) {
|
|
415
|
+
// Determine the status based on where the issue ended up
|
|
416
|
+
let isNowInReview = false;
|
|
417
|
+
let isNowInStuck = false;
|
|
418
|
+
let isStillInOpen = false;
|
|
419
|
+
try {
|
|
420
|
+
await fs.access(path.join(config.issuesDir, ISSUE_DIR_REVIEW, currentIssue));
|
|
421
|
+
isNowInReview = true;
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
// File doesn't exist in review
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
await fs.access(path.join(config.issuesDir, ISSUE_DIR_STUCK, currentIssue));
|
|
428
|
+
isNowInStuck = true;
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
// File doesn't exist in stuck
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
await fs.access(path.join(config.issuesDir, ISSUE_DIR_OPEN, currentIssue));
|
|
435
|
+
isStillInOpen = true;
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// File doesn't exist in open
|
|
439
|
+
}
|
|
440
|
+
let status = 'unknown';
|
|
441
|
+
if (isNowInReview) {
|
|
442
|
+
status = 'done';
|
|
443
|
+
}
|
|
444
|
+
else if (isNowInStuck) {
|
|
445
|
+
status = 'stuck';
|
|
446
|
+
}
|
|
447
|
+
else if (isStillInOpen) {
|
|
448
|
+
status = 'in progress';
|
|
449
|
+
}
|
|
450
|
+
gitCommit(currentIssue, status);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (iteration >= config.maxIterations) {
|
|
454
|
+
console.log(`\nReached maximum iterations (${config.maxIterations}). Exiting.`);
|
|
455
|
+
}
|
|
456
|
+
console.log('\nDone!');
|
|
457
|
+
}
|
|
458
|
+
main().catch((error) => {
|
|
459
|
+
console.error('Error:', error);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
});
|
|
462
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bueller-wheel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless Claude Code issue processor - A wrapper that runs Claude Code in a loop to process issues from a directory queue",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bueller-wheel": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "rm -rf dist out && tsc && mkdir -p dist && cp out/src/index.js dist/index.js && 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
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"test": "tsx tests/test-runner.ts",
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"lint": "eslint .",
|
|
17
|
+
"lint:fix": "eslint . --fix",
|
|
18
|
+
"format": "prettier --write ."
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"automation",
|
|
24
|
+
"ai",
|
|
25
|
+
"issue-tracker",
|
|
26
|
+
"task-automation",
|
|
27
|
+
"agent"
|
|
28
|
+
],
|
|
29
|
+
"author": "Andrew Fong <id@andrewfong.com>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/fongandrew/bueller-wheel.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/fongandrew/bueller-wheel/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/fongandrew/bueller-wheel#readme",
|
|
39
|
+
"files": [
|
|
40
|
+
"dist/index.js",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@anthropic-ai/sdk": "^0.71.0",
|
|
49
|
+
"@eslint/js": "^9.30.1",
|
|
50
|
+
"@types/node": "^24.0.10",
|
|
51
|
+
"@typescript-eslint/parser": "^8.47.0",
|
|
52
|
+
"eslint": "^9.30.1",
|
|
53
|
+
"eslint-config-prettier": "^10.1.5",
|
|
54
|
+
"eslint-plugin-import": "^2.32.0",
|
|
55
|
+
"eslint-plugin-prettier": "^5.5.1",
|
|
56
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
57
|
+
"eslint-plugin-unused-imports": "^4.1.4",
|
|
58
|
+
"globals": "^16.3.0",
|
|
59
|
+
"install": "^0.13.0",
|
|
60
|
+
"npm": "^11.6.4",
|
|
61
|
+
"prettier": "^3.6.2",
|
|
62
|
+
"tsx": "^4.19.2",
|
|
63
|
+
"typescript": "^5.8.3",
|
|
64
|
+
"typescript-eslint": "^8.47.0"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.55"
|
|
68
|
+
}
|
|
69
|
+
}
|