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 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
+ }