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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # Bueller Wheel
2
2
 
3
- A headless Claude Code issue processor that runs in a loop and resolves issues or ticket files written in markdown
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 Code to autonomously work on a large pile of issues while you eat lunch and watch a baseball game. Bueller Wheel helps tackle several issues you might encounter:
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 Code tends to stop processing after completing a few tasks. Bueller Wheel keeps prompting Claude Code to work until all of the issues have been resolved.
21
- - **Claude forgets what it's doing**: As Claude Code uses up its context window, it tends to forget what it was working on. Bueller Wheel runs Claude Code with a fresh context window and prompt for each issue.
22
- - **You forget what Claude was doing**: If you successfully get Claude Code 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.
23
- - **Claude keeps making the same mistakes**: A Claude 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.
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 Code reads the issue and works on the task
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 Code session** - no memory between iterations, which keeps context focused.
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
- npx bueller-wheel --issues-dir ./my-issues --faq-dir ./my-faq --max-iterations 50 --git-commit --prompt ./my-prompt.md
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 --issues-dir ./my-issues --faq-dir ./my-faq --max-iterations 50 --git-commit --prompt ./my-prompt.md
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
- - `--continue [prompt]`: Continue from previous session. Optional prompt defaults to "continue" if not provided
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-commit` is enabled, Bueller Wheel will automatically create a git commit after each iteration where work was done on an issue.
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 live instance of Claude Code!**
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
- --max-iterations N Maximum number of iterations to run (default: 25)
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 --issues-dir ./my-issues --faq-dir ./my-faq
53
- bueller --max-iterations 50 --git-commit
54
- bueller --prompt ./custom-prompt.md
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 = false;
113
+ let gitCommit = true;
70
114
  let promptFile = path.join('./issues', 'prompt.md');
71
115
  let continueMode = false;
72
116
  let continuePrompt = 'continue';
73
- for (let i = 0; i < args.length; i++) {
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-iterations' && i + 1 < args.length) {
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' || args[i] === '--git-commit') {
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. **Append your response**: Add your summary to [ISSUE_FILE_PATH] using this format:
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
- 4. **Decide the outcome**: Choose ONE of the following actions:
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.2.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/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",
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/index.js",
40
+ "dist/*.js",
41
41
  "README.md",
42
42
  "LICENSE"
43
43
  ],