bumblebee-cli 0.1.1 → 0.3.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.
Files changed (47) hide show
  1. package/dist/index.js +4214 -0
  2. package/dist/index.js.map +1 -0
  3. package/package.json +42 -28
  4. package/templates/skills/bb-agent/SKILL.md +180 -0
  5. package/templates/skills/bb-agent/references/bb-commands.md +124 -0
  6. package/templates/skills/bb-agent/references/investigate-workflow.md +150 -0
  7. package/templates/skills/bb-agent/references/parallel-workflow.md +105 -0
  8. package/templates/skills/bb-agent/references/prompts.md +144 -0
  9. package/templates/skills/bb-agent/references/status-transitions.md +93 -0
  10. package/templates/skills/bb-agent/references/workflow.md +178 -0
  11. package/README.md +0 -47
  12. package/bin/bb.mjs +0 -132
  13. package/python/bb_cli/__init__.py +0 -0
  14. package/python/bb_cli/api_client.py +0 -38
  15. package/python/bb_cli/bumblebee_cli.egg-info/PKG-INFO +0 -9
  16. package/python/bb_cli/bumblebee_cli.egg-info/SOURCES.txt +0 -21
  17. package/python/bb_cli/bumblebee_cli.egg-info/dependency_links.txt +0 -1
  18. package/python/bb_cli/bumblebee_cli.egg-info/entry_points.txt +0 -2
  19. package/python/bb_cli/bumblebee_cli.egg-info/requires.txt +0 -4
  20. package/python/bb_cli/bumblebee_cli.egg-info/top_level.txt +0 -5
  21. package/python/bb_cli/commands/__init__.py +0 -0
  22. package/python/bb_cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  23. package/python/bb_cli/commands/__pycache__/agent.cpython-313.pyc +0 -0
  24. package/python/bb_cli/commands/__pycache__/auth.cpython-313.pyc +0 -0
  25. package/python/bb_cli/commands/__pycache__/board.cpython-313.pyc +0 -0
  26. package/python/bb_cli/commands/__pycache__/comment.cpython-313.pyc +0 -0
  27. package/python/bb_cli/commands/__pycache__/init.cpython-313.pyc +0 -0
  28. package/python/bb_cli/commands/__pycache__/item.cpython-313.pyc +0 -0
  29. package/python/bb_cli/commands/__pycache__/label.cpython-313.pyc +0 -0
  30. package/python/bb_cli/commands/__pycache__/project.cpython-313.pyc +0 -0
  31. package/python/bb_cli/commands/__pycache__/sprint.cpython-313.pyc +0 -0
  32. package/python/bb_cli/commands/__pycache__/story.cpython-313.pyc +0 -0
  33. package/python/bb_cli/commands/__pycache__/task.cpython-313.pyc +0 -0
  34. package/python/bb_cli/commands/agent.py +0 -1030
  35. package/python/bb_cli/commands/auth.py +0 -79
  36. package/python/bb_cli/commands/board.py +0 -47
  37. package/python/bb_cli/commands/comment.py +0 -34
  38. package/python/bb_cli/commands/init.py +0 -62
  39. package/python/bb_cli/commands/item.py +0 -192
  40. package/python/bb_cli/commands/label.py +0 -43
  41. package/python/bb_cli/commands/project.py +0 -111
  42. package/python/bb_cli/commands/sprint.py +0 -84
  43. package/python/bb_cli/config.py +0 -136
  44. package/python/bb_cli/main.py +0 -44
  45. package/python/pyproject.toml +0 -18
  46. package/scripts/build.sh +0 -20
  47. package/scripts/postinstall.mjs +0 -146
package/dist/index.js ADDED
@@ -0,0 +1,4214 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command24 } from "commander";
5
+
6
+ // src/commands/auth.ts
7
+ import { Command } from "commander";
8
+ import chalk2 from "chalk";
9
+ import * as p from "@clack/prompts";
10
+
11
+ // src/api-client.ts
12
+ import { ofetch } from "ofetch";
13
+
14
+ // src/config.ts
15
+ import fs from "fs";
16
+ import os from "os";
17
+ import path from "path";
18
+ import TOML from "@iarna/toml";
19
+ var GLOBAL_CONFIG_DIR = path.join(os.homedir(), ".bumblebee");
20
+ var GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, "config.toml");
21
+ var LOCAL_DIR_NAME = ".bumblebee";
22
+ var LOCAL_CONFIG_NAME = "config.toml";
23
+ function ensureConfigDir() {
24
+ fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
25
+ }
26
+ function findProjectRoot(start) {
27
+ let current = path.resolve(start ?? process.cwd());
28
+ while (true) {
29
+ if (fs.existsSync(path.join(current, LOCAL_DIR_NAME))) return current;
30
+ if (fs.existsSync(path.join(current, ".git"))) return current;
31
+ const parent = path.dirname(current);
32
+ if (parent === current) break;
33
+ current = parent;
34
+ }
35
+ return null;
36
+ }
37
+ function getLocalConfigDir() {
38
+ const root = findProjectRoot();
39
+ if (root) {
40
+ const dir = path.join(root, LOCAL_DIR_NAME);
41
+ if (fs.existsSync(dir)) return dir;
42
+ }
43
+ return null;
44
+ }
45
+ function isLocalConfigActive() {
46
+ return getLocalConfigDir() !== null;
47
+ }
48
+ function loadToml(filePath) {
49
+ if (!fs.existsSync(filePath)) return {};
50
+ const content = fs.readFileSync(filePath, "utf-8");
51
+ return TOML.parse(content);
52
+ }
53
+ function loadGlobalConfig() {
54
+ return loadToml(GLOBAL_CONFIG_FILE);
55
+ }
56
+ function loadLocalConfig() {
57
+ const dir = getLocalConfigDir();
58
+ if (dir) {
59
+ const file = path.join(dir, LOCAL_CONFIG_NAME);
60
+ return loadToml(file);
61
+ }
62
+ return {};
63
+ }
64
+ function saveLocalConfig(data) {
65
+ const dir = getLocalConfigDir();
66
+ if (!dir) throw new Error("No .bumblebee/ directory found. Run 'bb init' first.");
67
+ fs.mkdirSync(dir, { recursive: true });
68
+ fs.writeFileSync(path.join(dir, LOCAL_CONFIG_NAME), TOML.stringify(data));
69
+ }
70
+ function loadConfig() {
71
+ const globalCfg = loadGlobalConfig();
72
+ const localCfg = loadLocalConfig();
73
+ delete localCfg.token;
74
+ return { ...globalCfg, ...localCfg };
75
+ }
76
+ function saveConfig(data) {
77
+ ensureConfigDir();
78
+ fs.writeFileSync(GLOBAL_CONFIG_FILE, TOML.stringify(data));
79
+ }
80
+ function getApiUrl() {
81
+ const env = process.env.BB_API_URL;
82
+ if (env) return env;
83
+ const cfg = loadConfig();
84
+ return cfg.api_url ?? "http://localhost:8000";
85
+ }
86
+ function getToken() {
87
+ const cfg = loadGlobalConfig();
88
+ return cfg.token ?? null;
89
+ }
90
+ function setToken(token) {
91
+ const cfg = loadGlobalConfig();
92
+ cfg.token = token;
93
+ saveConfig(cfg);
94
+ }
95
+ function clearToken() {
96
+ const cfg = loadGlobalConfig();
97
+ delete cfg.token;
98
+ saveConfig(cfg);
99
+ }
100
+ function getCurrentProject() {
101
+ const cfg = loadConfig();
102
+ return cfg.current_project ?? null;
103
+ }
104
+ function getProjectPath(slug) {
105
+ const cfg = loadConfig();
106
+ const target = slug ?? cfg.current_project;
107
+ if (!target) return null;
108
+ const projects = cfg.projects ?? {};
109
+ return projects[target]?.path ?? null;
110
+ }
111
+ function setProjectPath(slug, projectPath) {
112
+ const cfg = loadGlobalConfig();
113
+ if (!cfg.projects) cfg.projects = {};
114
+ const projects = cfg.projects;
115
+ if (!projects[slug]) projects[slug] = {};
116
+ projects[slug].path = projectPath;
117
+ saveConfig(cfg);
118
+ }
119
+
120
+ // src/api-client.ts
121
+ function createClient() {
122
+ return ofetch.create({
123
+ baseURL: getApiUrl(),
124
+ headers: (() => {
125
+ const h = {};
126
+ const token = getToken();
127
+ if (token) h.Authorization = `Bearer ${token}`;
128
+ return h;
129
+ })(),
130
+ timeout: 3e4
131
+ });
132
+ }
133
+ async function apiGet(path9, params) {
134
+ return createClient()(path9, { method: "GET", query: params });
135
+ }
136
+ async function apiPost(path9, body, params) {
137
+ return createClient()(path9, {
138
+ method: "POST",
139
+ body,
140
+ query: params
141
+ });
142
+ }
143
+ async function apiPut(path9, body) {
144
+ return createClient()(path9, { method: "PUT", body });
145
+ }
146
+ async function apiPatch(path9, body) {
147
+ return createClient()(path9, { method: "PATCH", body });
148
+ }
149
+
150
+ // src/lib/utils.ts
151
+ import chalk from "chalk";
152
+ function requireProject() {
153
+ const slug = getCurrentProject();
154
+ if (!slug) {
155
+ console.error(chalk.red("No project selected. Run 'bb project switch <slug>' first."));
156
+ process.exit(1);
157
+ }
158
+ return slug;
159
+ }
160
+ function requireProjectPath(slug) {
161
+ const p4 = getProjectPath(slug);
162
+ if (!p4) {
163
+ console.error(chalk.red("No source code directory linked to this project."));
164
+ console.error(chalk.yellow("Run 'bb project link <path>' to set it."));
165
+ process.exit(1);
166
+ }
167
+ return p4;
168
+ }
169
+ async function resolveItemId(slug, idOrNumber) {
170
+ if (idOrNumber.length > 8 && idOrNumber.includes("-") && idOrNumber.match(/^[0-9a-f-]+$/i)) {
171
+ return apiGet(`/api/work-items/${idOrNumber}`);
172
+ }
173
+ const match = idOrNumber.match(/^(?:[A-Za-z]+-)?(\d+)$/);
174
+ if (match) {
175
+ const num = parseInt(match[1], 10);
176
+ return apiGet(`/api/projects/${slug}/work-items/by-number/${num}`);
177
+ }
178
+ console.error(
179
+ chalk.red(`Invalid identifier: ${idOrNumber}. Use UUID, number, or KEY-number (e.g. BB-42).`)
180
+ );
181
+ process.exit(1);
182
+ }
183
+ function slugify(text3, maxLen = 48) {
184
+ let s = text3.toLowerCase().trim();
185
+ s = s.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
186
+ if (s.length > maxLen) {
187
+ s = s.slice(0, maxLen);
188
+ const last = s.lastIndexOf("-");
189
+ if (last > 0) s = s.slice(0, last);
190
+ }
191
+ return s;
192
+ }
193
+ function truncate(text3, max) {
194
+ return text3.length <= max ? text3 : text3.slice(0, max - 3) + "...";
195
+ }
196
+ async function withErrorHandling(fn) {
197
+ try {
198
+ await fn();
199
+ } catch (err) {
200
+ if (err?.data?.detail) {
201
+ console.error(chalk.red(`Error: ${err.data.detail}`));
202
+ } else if (err?.statusCode === 401) {
203
+ console.error(chalk.red("Not authenticated. Run 'bb login' first."));
204
+ } else if (err?.message?.includes("fetch failed") || err?.code === "ECONNREFUSED") {
205
+ console.error(chalk.red("Cannot connect to the API server."));
206
+ } else {
207
+ console.error(chalk.red(`Error: ${err?.message ?? err}`));
208
+ }
209
+ process.exit(1);
210
+ }
211
+ }
212
+
213
+ // src/commands/auth.ts
214
+ var authCommand = new Command("auth").description(
215
+ "Authentication commands"
216
+ );
217
+ authCommand.command("login").description("Log in to Bumblebee API").option("-e, --email <email>", "Email address").option("-p, --password <password>", "Password").action(
218
+ (opts) => withErrorHandling(async () => {
219
+ let { email, password: password2 } = opts;
220
+ if (!email) {
221
+ const value = await p.text({
222
+ message: "Email",
223
+ validate: (v) => v.length === 0 ? "Email is required" : void 0
224
+ });
225
+ if (p.isCancel(value)) {
226
+ console.log(chalk2.yellow("Cancelled."));
227
+ process.exit(0);
228
+ }
229
+ email = value;
230
+ }
231
+ if (!password2) {
232
+ const value = await p.password({
233
+ message: "Password",
234
+ validate: (v) => v.length === 0 ? "Password is required" : void 0
235
+ });
236
+ if (p.isCancel(value)) {
237
+ console.log(chalk2.yellow("Cancelled."));
238
+ process.exit(0);
239
+ }
240
+ password2 = value;
241
+ }
242
+ const data = await apiPost("/auth/login", {
243
+ email,
244
+ password: password2
245
+ });
246
+ setToken(data.access_token);
247
+ console.log(chalk2.green("Logged in successfully."));
248
+ })
249
+ );
250
+ authCommand.command("register").description("Register a new Bumblebee account").option("-e, --email <email>", "Email address").option("-u, --username <username>", "Username").option("-p, --password <password>", "Password").action(
251
+ (opts) => withErrorHandling(async () => {
252
+ let { email, username, password: password2 } = opts;
253
+ if (!email) {
254
+ const value = await p.text({
255
+ message: "Email",
256
+ validate: (v) => v.length === 0 ? "Email is required" : void 0
257
+ });
258
+ if (p.isCancel(value)) {
259
+ console.log(chalk2.yellow("Cancelled."));
260
+ process.exit(0);
261
+ }
262
+ email = value;
263
+ }
264
+ if (!username) {
265
+ const value = await p.text({
266
+ message: "Username",
267
+ validate: (v) => v.length === 0 ? "Username is required" : void 0
268
+ });
269
+ if (p.isCancel(value)) {
270
+ console.log(chalk2.yellow("Cancelled."));
271
+ process.exit(0);
272
+ }
273
+ username = value;
274
+ }
275
+ if (!password2) {
276
+ const value = await p.password({
277
+ message: "Password",
278
+ validate: (v) => v.length === 0 ? "Password is required" : void 0
279
+ });
280
+ if (p.isCancel(value)) {
281
+ console.log(chalk2.yellow("Cancelled."));
282
+ process.exit(0);
283
+ }
284
+ password2 = value;
285
+ const confirm4 = await p.password({ message: "Confirm password" });
286
+ if (p.isCancel(confirm4)) {
287
+ console.log(chalk2.yellow("Cancelled."));
288
+ process.exit(0);
289
+ }
290
+ if (confirm4 !== password2) {
291
+ console.error(chalk2.red("Passwords do not match."));
292
+ process.exit(1);
293
+ }
294
+ }
295
+ await apiPost("/auth/register", { email, username, password: password2 });
296
+ console.log(
297
+ chalk2.green(`Registered! You can now run ${chalk2.bold("bb login")}.`)
298
+ );
299
+ })
300
+ );
301
+ authCommand.command("logout").description("Clear saved credentials").action(
302
+ () => withErrorHandling(async () => {
303
+ clearToken();
304
+ console.log(chalk2.yellow("Logged out."));
305
+ })
306
+ );
307
+ authCommand.command("whoami").description("Show current user info").action(
308
+ () => withErrorHandling(async () => {
309
+ const user = await apiGet("/auth/me");
310
+ console.log(`${chalk2.bold(user.username)} (${user.email})`);
311
+ })
312
+ );
313
+ authCommand.command("config").description("Show current CLI configuration").action(
314
+ () => withErrorHandling(async () => {
315
+ const cfg = loadConfig();
316
+ const slug = cfg.current_project;
317
+ const configSource = isLocalConfigActive() ? "local (.bumblebee/)" : "global (~/.bumblebee/)";
318
+ console.log(`Config source: ${configSource}`);
319
+ console.log(`API URL: ${getApiUrl()}`);
320
+ console.log(`Logged in: ${cfg.token ? "Yes" : "No"}`);
321
+ console.log(`Current project: ${slug ?? "None"}`);
322
+ if (slug) {
323
+ const projectPath = getProjectPath(slug);
324
+ console.log(`Source path: ${projectPath ?? chalk2.dim("not linked")}`);
325
+ }
326
+ const projects = cfg.projects ?? {};
327
+ const projectKeys = Object.keys(projects);
328
+ if (projectKeys.length > 0) {
329
+ console.log(`
330
+ ${chalk2.bold("Project paths:")}`);
331
+ for (const name of projectKeys) {
332
+ console.log(` ${name} -> ${projects[name]?.path ?? "?"}`);
333
+ }
334
+ }
335
+ })
336
+ );
337
+
338
+ // src/commands/project.ts
339
+ import { Command as Command2 } from "commander";
340
+ import chalk3 from "chalk";
341
+ import path2 from "path";
342
+ import fs2 from "fs";
343
+ import Table from "cli-table3";
344
+ import * as p2 from "@clack/prompts";
345
+ var projectCommand = new Command2("project").description(
346
+ "Project management"
347
+ );
348
+ async function selectProjectInteractive() {
349
+ const projects = await apiGet("/api/projects");
350
+ if (!projects || projects.length === 0) {
351
+ console.log(
352
+ chalk3.yellow(
353
+ `No projects found. Create one with ${chalk3.bold("bb project create <name>")}.`
354
+ )
355
+ );
356
+ process.exit(1);
357
+ }
358
+ const choices = projects.map((proj) => ({
359
+ value: proj,
360
+ label: `${proj.slug} - ${proj.name}`
361
+ }));
362
+ const selected = await p2.select({
363
+ message: "Select a project",
364
+ options: choices
365
+ });
366
+ if (p2.isCancel(selected)) {
367
+ console.log(chalk3.yellow("Cancelled."));
368
+ process.exit(0);
369
+ }
370
+ return selected;
371
+ }
372
+ function setCurrentProject(slug) {
373
+ if (isLocalConfigActive()) {
374
+ const localCfg = loadLocalConfig();
375
+ localCfg.current_project = slug;
376
+ saveLocalConfig(localCfg);
377
+ } else {
378
+ const cfg = loadConfig();
379
+ cfg.current_project = slug;
380
+ saveConfig(cfg);
381
+ }
382
+ }
383
+ projectCommand.command("list").description("List all projects").action(
384
+ () => withErrorHandling(async () => {
385
+ const projects = await apiGet("/api/projects");
386
+ const table = new Table({
387
+ head: ["Slug", "Name", "Local Path", "Repo"],
388
+ style: { head: ["cyan"] }
389
+ });
390
+ for (const proj of projects) {
391
+ const localPath = getProjectPath(proj.slug) ?? "-";
392
+ table.push([
393
+ proj.slug,
394
+ proj.name,
395
+ localPath,
396
+ proj.repo_url ?? "-"
397
+ ]);
398
+ }
399
+ console.log(table.toString());
400
+ })
401
+ );
402
+ projectCommand.command("create").description("Create a new project").argument("<name>", "Project name").option("--slug <slug>", "Project slug (defaults to name kebab-cased)").option("--repo-url <url>", "Repository URL").action(
403
+ (name, opts) => withErrorHandling(async () => {
404
+ const projectSlug = opts.slug ?? slugify(name);
405
+ const project = await apiPost("/api/projects", {
406
+ name,
407
+ slug: projectSlug,
408
+ repo_url: opts.repoUrl ?? null
409
+ });
410
+ console.log(
411
+ chalk3.green(`Created project ${chalk3.bold(project.slug)}`)
412
+ );
413
+ })
414
+ );
415
+ projectCommand.command("switch").description("Switch to a project as the active context").argument("[slug]", "Project slug (interactive picker if omitted)").option("-p, --path <path>", "Local source code directory").action(
416
+ (slug, opts) => withErrorHandling(async () => {
417
+ if (!slug) {
418
+ const selected = await selectProjectInteractive();
419
+ slug = selected.slug;
420
+ } else {
421
+ await apiGet(`/api/projects/${slug}`);
422
+ }
423
+ setCurrentProject(slug);
424
+ if (opts.path) {
425
+ const resolved = path2.resolve(opts.path);
426
+ setProjectPath(slug, resolved);
427
+ console.log(
428
+ chalk3.green(`Switched to ${chalk3.bold(slug)} -> ${resolved}`)
429
+ );
430
+ } else {
431
+ const existing = getProjectPath(slug);
432
+ console.log(chalk3.green(`Switched to project ${chalk3.bold(slug)}`));
433
+ if (!existing) {
434
+ console.log(
435
+ chalk3.dim(
436
+ `Tip: use ${chalk3.bold("bb project link <path>")} to set the source code directory.`
437
+ )
438
+ );
439
+ }
440
+ }
441
+ })
442
+ );
443
+ projectCommand.command("link").description("Link current project to a local source code directory").argument("[path]", "Path to source directory (default: current dir)", ".").action(
444
+ (linkPath) => withErrorHandling(async () => {
445
+ const slug = getCurrentProject();
446
+ if (!slug) {
447
+ console.error(
448
+ chalk3.red(
449
+ `No project selected. Run ${chalk3.bold("bb project switch <slug>")} first.`
450
+ )
451
+ );
452
+ process.exit(1);
453
+ }
454
+ const resolved = path2.resolve(linkPath);
455
+ if (!fs2.existsSync(resolved) || !fs2.statSync(resolved).isDirectory()) {
456
+ console.error(chalk3.red(`Directory not found: ${resolved}`));
457
+ process.exit(1);
458
+ }
459
+ setProjectPath(slug, resolved);
460
+ console.log(
461
+ chalk3.green(`Linked ${chalk3.bold(slug)} -> ${resolved}`)
462
+ );
463
+ })
464
+ );
465
+ projectCommand.command("current").description("Show the current active project").action(
466
+ () => withErrorHandling(async () => {
467
+ const slug = getCurrentProject();
468
+ if (!slug) {
469
+ console.log(
470
+ chalk3.yellow(
471
+ `No project selected. Use ${chalk3.bold("bb project switch <slug>")}.`
472
+ )
473
+ );
474
+ process.exit(1);
475
+ }
476
+ const project = await apiGet(`/api/projects/${slug}`);
477
+ console.log(`${chalk3.bold(project.name)} (${project.slug})`);
478
+ const localPath = getProjectPath(slug);
479
+ if (localPath) {
480
+ console.log(`Path: ${localPath}`);
481
+ } else {
482
+ console.log(
483
+ chalk3.dim(
484
+ `Path: not linked - use ${chalk3.bold("bb project link <path>")}`
485
+ )
486
+ );
487
+ }
488
+ if (project.repo_url) {
489
+ console.log(`Repo: ${project.repo_url}`);
490
+ }
491
+ })
492
+ );
493
+ projectCommand.command("refresh").description(
494
+ "Re-pick the active project from the API and link the current directory"
495
+ ).action(
496
+ () => withErrorHandling(async () => {
497
+ const selected = await selectProjectInteractive();
498
+ const slug = selected.slug;
499
+ setCurrentProject(slug);
500
+ const resolved = path2.resolve(process.cwd());
501
+ setProjectPath(slug, resolved);
502
+ console.log(
503
+ chalk3.green(
504
+ `Switched to ${chalk3.bold(selected.name)} (${slug}) -> ${resolved}`
505
+ )
506
+ );
507
+ })
508
+ );
509
+
510
+ // src/commands/item.ts
511
+ import { Command as Command3 } from "commander";
512
+ import chalk4 from "chalk";
513
+ import Table2 from "cli-table3";
514
+ var itemCommand = new Command3("item").description(
515
+ "Work item management"
516
+ );
517
+ itemCommand.command("list").description("List work items in the current project").option("-t, --type <type>", "Filter by type (story, task, epic, bug...)").option("-s, --status <status>", "Filter by status").option("--parent <parent>", "Filter by parent ID").option("-a, --assignee <assignee>", "Filter by assignee").action(
518
+ (opts) => withErrorHandling(async () => {
519
+ const slug = requireProject();
520
+ const params = {};
521
+ if (opts.type) params.type = opts.type;
522
+ if (opts.status) params.status = opts.status;
523
+ if (opts.parent) params.parent_id = opts.parent;
524
+ if (opts.assignee) params.assignee = opts.assignee;
525
+ const items = await apiGet(
526
+ `/api/projects/${slug}/work-items`,
527
+ params
528
+ );
529
+ const table = new Table2({
530
+ head: ["#", "Type", "Title", "Status", "Priority", "Assignee"],
531
+ style: { head: ["cyan"] }
532
+ });
533
+ for (const item of items) {
534
+ const key = item.key ?? String(item.number);
535
+ table.push([
536
+ key,
537
+ item.type,
538
+ item.title,
539
+ item.status,
540
+ item.priority,
541
+ item.assignee ?? "-"
542
+ ]);
543
+ }
544
+ console.log(table.toString());
545
+ })
546
+ );
547
+ itemCommand.command("create").description("Create a new work item").argument("<title>", "Item title").option(
548
+ "-t, --type <type>",
549
+ "Item type: story, task, epic, bug, feature, chore, spike",
550
+ "story"
551
+ ).option("-d, --description <description>", "Description").option("-p, --priority <priority>", "Priority", "medium").option("--parent <parent>", "Parent item ID or number").action(
552
+ (title, opts) => withErrorHandling(async () => {
553
+ const slug = requireProject();
554
+ const data = {
555
+ title,
556
+ type: opts.type,
557
+ priority: opts.priority
558
+ };
559
+ if (opts.description) {
560
+ data.description = opts.description;
561
+ }
562
+ if (opts.parent) {
563
+ const parentItem = await resolveItemId(slug, opts.parent);
564
+ data.parent_id = parentItem.id;
565
+ }
566
+ const item = await apiPost(
567
+ `/api/projects/${slug}/work-items`,
568
+ data
569
+ );
570
+ const key = item.key ?? `#${item.number}`;
571
+ console.log(chalk4.green(`Created ${item.type} ${key}: ${item.title}`));
572
+ })
573
+ );
574
+ itemCommand.command("show").description("Show work item details").argument("<id>", "UUID, number, or KEY-number (e.g. BB-42)").action(
575
+ (idOrNumber) => withErrorHandling(async () => {
576
+ const slug = requireProject();
577
+ const item = await resolveItemId(slug, idOrNumber);
578
+ const key = item.key ?? `#${item.number}`;
579
+ console.log(
580
+ `${chalk4.bold(key)} [${item.type}] ${item.title}`
581
+ );
582
+ console.log(
583
+ `Status: ${item.status} | Priority: ${item.priority}`
584
+ );
585
+ if (item.assignee) {
586
+ console.log(`Assignee: ${item.assignee}`);
587
+ }
588
+ if (item.parent_id) {
589
+ console.log(`Parent: ${item.parent_id}`);
590
+ }
591
+ if (item.sprint_id) {
592
+ console.log(`Sprint: ${item.sprint_id}`);
593
+ }
594
+ if (item.story_points) {
595
+ console.log(`Points: ${item.story_points}`);
596
+ }
597
+ if (item.description) {
598
+ console.log(`
599
+ ${item.description}`);
600
+ }
601
+ if (item.acceptance_criteria) {
602
+ console.log(
603
+ `
604
+ ${chalk4.bold("Acceptance Criteria:")}
605
+ ${item.acceptance_criteria}`
606
+ );
607
+ }
608
+ if (item.plan) {
609
+ console.log(`
610
+ ${chalk4.bold("Plan:")}
611
+ ${item.plan}`);
612
+ }
613
+ if (item.ai_summary) {
614
+ console.log(`
615
+ ${chalk4.bold("AI Summary:")}
616
+ ${item.ai_summary}`);
617
+ }
618
+ })
619
+ );
620
+ itemCommand.command("update").description("Update a work item").argument("<id>", "UUID, number, or KEY-number").option("-s, --status <status>", "New status").option("-a, --assignee <assignee>", "New assignee").option("-p, --priority <priority>", "New priority").option("--title <title>", "New title").option("-t, --type <type>", "New type").action(
621
+ (idOrNumber, opts) => withErrorHandling(async () => {
622
+ const slug = requireProject();
623
+ const item = await resolveItemId(slug, idOrNumber);
624
+ const data = {};
625
+ if (opts.status) data.status = opts.status;
626
+ if (opts.assignee) data.assignee = opts.assignee;
627
+ if (opts.priority) data.priority = opts.priority;
628
+ if (opts.title) data.title = opts.title;
629
+ if (opts.type) data.type = opts.type;
630
+ if (Object.keys(data).length === 0) {
631
+ console.log(chalk4.yellow("Nothing to update."));
632
+ return;
633
+ }
634
+ const updated = await apiPut(
635
+ `/api/work-items/${item.id}`,
636
+ data
637
+ );
638
+ const key = updated.key ?? `#${updated.number}`;
639
+ console.log(chalk4.green(`Updated ${key}`));
640
+ })
641
+ );
642
+ itemCommand.command("assign").description("Assign a work item to someone").argument("<id>", "UUID, number, or KEY-number").argument("<assignee>", "Assignee name").action(
643
+ (idOrNumber, assignee) => withErrorHandling(async () => {
644
+ const slug = requireProject();
645
+ const item = await resolveItemId(slug, idOrNumber);
646
+ const updated = await apiPut(`/api/work-items/${item.id}`, {
647
+ assignee
648
+ });
649
+ const key = updated.key ?? `#${updated.number}`;
650
+ console.log(chalk4.green(`${key} assigned to ${assignee}`));
651
+ })
652
+ );
653
+ itemCommand.command("children").description("List children of a work item").argument("<id>", "UUID, number, or KEY-number").action(
654
+ (idOrNumber) => withErrorHandling(async () => {
655
+ const slug = requireProject();
656
+ const item = await resolveItemId(slug, idOrNumber);
657
+ const kids = await apiGet(
658
+ `/api/work-items/${item.id}/children`
659
+ );
660
+ if (!kids || kids.length === 0) {
661
+ console.log(chalk4.dim("No children."));
662
+ return;
663
+ }
664
+ const parentKey = item.key ?? `#${item.number}`;
665
+ const table = new Table2({
666
+ head: ["#", "Type", "Title", "Status", "Assignee"],
667
+ style: { head: ["cyan"] }
668
+ });
669
+ for (const kid of kids) {
670
+ const key = kid.key ?? String(kid.number);
671
+ table.push([
672
+ key,
673
+ kid.type,
674
+ kid.title,
675
+ kid.status,
676
+ kid.assignee ?? "-"
677
+ ]);
678
+ }
679
+ console.log(`Children of ${parentKey}`);
680
+ console.log(table.toString());
681
+ })
682
+ );
683
+
684
+ // src/commands/comment.ts
685
+ import { Command as Command4 } from "commander";
686
+ import chalk5 from "chalk";
687
+ var commentCommand = new Command4("comment").description(
688
+ "Comment management"
689
+ );
690
+ commentCommand.command("list").description("List comments on a work item").argument("<id>", "Work item UUID, number, or KEY-number").action(
691
+ (idOrNumber) => withErrorHandling(async () => {
692
+ const slug = requireProject();
693
+ const item = await resolveItemId(slug, idOrNumber);
694
+ const comments = await apiGet(
695
+ `/api/work-items/${item.id}/comments`
696
+ );
697
+ if (!comments || comments.length === 0) {
698
+ console.log(chalk5.dim("No comments yet."));
699
+ return;
700
+ }
701
+ for (const c of comments) {
702
+ const typeTag = c.type && c.type !== "discussion" ? ` [${c.type}]` : "";
703
+ console.log(`${chalk5.bold(c.author)}${typeTag}: ${c.body}`);
704
+ }
705
+ })
706
+ );
707
+ commentCommand.command("add").description("Add a comment to a work item").argument("<id>", "Work item UUID, number, or KEY-number").argument("<body>", "Comment body").option(
708
+ "-t, --type <type>",
709
+ "Comment type: discussion, investigation, proposal, review, agent_output",
710
+ "discussion"
711
+ ).action(
712
+ (idOrNumber, body, opts) => withErrorHandling(async () => {
713
+ const slug = requireProject();
714
+ const item = await resolveItemId(slug, idOrNumber);
715
+ await apiPost(`/api/work-items/${item.id}/comments`, {
716
+ body,
717
+ type: opts.type
718
+ });
719
+ console.log(chalk5.green("Comment added."));
720
+ })
721
+ );
722
+
723
+ // src/commands/sprint.ts
724
+ import { Command as Command5 } from "commander";
725
+ import chalk6 from "chalk";
726
+ import Table3 from "cli-table3";
727
+ var sprintCommand = new Command5("sprint").description(
728
+ "Sprint management"
729
+ );
730
+ sprintCommand.command("list").description("List sprints in the current project").action(
731
+ () => withErrorHandling(async () => {
732
+ const slug = requireProject();
733
+ const sprints = await apiGet(
734
+ `/api/projects/${slug}/sprints`
735
+ );
736
+ const table = new Table3({
737
+ head: ["ID", "Name", "Status", "Start", "End"],
738
+ style: { head: ["cyan"] }
739
+ });
740
+ for (const s of sprints) {
741
+ table.push([
742
+ s.id.slice(0, 8),
743
+ s.name,
744
+ s.status,
745
+ s.start_date ?? "-",
746
+ s.end_date ?? "-"
747
+ ]);
748
+ }
749
+ console.log(table.toString());
750
+ })
751
+ );
752
+ sprintCommand.command("create").description("Create a new sprint").argument("<name>", "Sprint name").option("-g, --goal <goal>", "Sprint goal").action(
753
+ (name, opts) => withErrorHandling(async () => {
754
+ const slug = requireProject();
755
+ const data = { name };
756
+ if (opts.goal) {
757
+ data.goal = opts.goal;
758
+ }
759
+ const sprint = await apiPost(
760
+ `/api/projects/${slug}/sprints`,
761
+ data
762
+ );
763
+ console.log(chalk6.green(`Created sprint: ${sprint.name}`));
764
+ })
765
+ );
766
+ sprintCommand.command("start").description("Start a sprint (set status to active)").argument("<id>", "Sprint ID").action(
767
+ (sprintId) => withErrorHandling(async () => {
768
+ const slug = requireProject();
769
+ const sprint = await apiPost(
770
+ `/api/projects/${slug}/sprints/${sprintId}/start`
771
+ );
772
+ console.log(
773
+ chalk6.green(`Sprint ${chalk6.bold(sprint.name)} is now active.`)
774
+ );
775
+ })
776
+ );
777
+ sprintCommand.command("close").description("Close an active sprint").argument("<id>", "Sprint ID").action(
778
+ (sprintId) => withErrorHandling(async () => {
779
+ const slug = requireProject();
780
+ const sprint = await apiPost(
781
+ `/api/projects/${slug}/sprints/${sprintId}/close`
782
+ );
783
+ console.log(
784
+ chalk6.green(`Sprint ${chalk6.bold(sprint.name)} completed.`)
785
+ );
786
+ })
787
+ );
788
+ sprintCommand.command("current").description("Show the currently active sprint").action(
789
+ () => withErrorHandling(async () => {
790
+ const slug = requireProject();
791
+ const sprints = await apiGet(
792
+ `/api/projects/${slug}/sprints`
793
+ );
794
+ const active = sprints.filter((s2) => s2.status === "active");
795
+ if (active.length === 0) {
796
+ console.log(chalk6.yellow("No active sprint."));
797
+ return;
798
+ }
799
+ const s = active[0];
800
+ console.log(`${chalk6.bold(s.name)} (${s.status})`);
801
+ if (s.goal) {
802
+ console.log(`Goal: ${s.goal}`);
803
+ }
804
+ console.log(
805
+ `Period: ${s.start_date ?? "?"} -> ${s.end_date ?? "?"}`
806
+ );
807
+ })
808
+ );
809
+
810
+ // src/commands/label.ts
811
+ import { Command as Command6 } from "commander";
812
+ import chalk7 from "chalk";
813
+ import Table4 from "cli-table3";
814
+ var labelCommand = new Command6("label").description(
815
+ "Label management"
816
+ );
817
+ labelCommand.command("list").description("List labels in the current project").action(
818
+ () => withErrorHandling(async () => {
819
+ const slug = requireProject();
820
+ const labels = await apiGet(
821
+ `/api/projects/${slug}/labels`
822
+ );
823
+ const table = new Table4({
824
+ head: ["Name", "Color"],
825
+ style: { head: ["cyan"] }
826
+ });
827
+ for (const label of labels) {
828
+ table.push([label.name, label.color ?? "-"]);
829
+ }
830
+ console.log(table.toString());
831
+ })
832
+ );
833
+ labelCommand.command("create").description("Create a label").argument("<name>", "Label name").option("-c, --color <color>", "Hex color e.g. #ff0000").action(
834
+ (name, opts) => withErrorHandling(async () => {
835
+ const slug = requireProject();
836
+ const data = { name };
837
+ if (opts.color) {
838
+ data.color = opts.color;
839
+ }
840
+ const label = await apiPost(
841
+ `/api/projects/${slug}/labels`,
842
+ data
843
+ );
844
+ console.log(chalk7.green(`Created label: ${label.name}`));
845
+ })
846
+ );
847
+
848
+ // src/commands/board.ts
849
+ import { Command as Command7 } from "commander";
850
+ import chalk8 from "chalk";
851
+ import Table5 from "cli-table3";
852
+ var COLUMNS = ["open", "in_progress", "in_review", "resolved", "closed"];
853
+ var COLUMN_WIDTH = 24;
854
+ var boardCommand = new Command7("board").description("Show kanban board for current project work items").option("-t, --type <type>", "Filter by type (story, task, etc.)").action(
855
+ (opts) => withErrorHandling(async () => {
856
+ const slug = requireProject();
857
+ const params = {};
858
+ if (opts.type) params.type = opts.type;
859
+ const items = await apiGet(
860
+ `/api/projects/${slug}/work-items`,
861
+ params
862
+ );
863
+ const grouped = {};
864
+ for (const col of COLUMNS) {
865
+ grouped[col] = [];
866
+ }
867
+ for (const item of items) {
868
+ if (grouped[item.status]) {
869
+ grouped[item.status].push(item);
870
+ }
871
+ }
872
+ const columnContents = COLUMNS.map((col) => {
873
+ const colItems = grouped[col];
874
+ if (colItems.length === 0) {
875
+ return [chalk8.dim(" (empty)")];
876
+ }
877
+ const lines = [];
878
+ for (const item of colItems) {
879
+ const key = item.key ?? `#${item.number}`;
880
+ lines.push(
881
+ ` ${chalk8.bold(key)} ${chalk8.dim(`[${item.type}]`)}`
882
+ );
883
+ lines.push(` ${truncate(item.title, COLUMN_WIDTH - 2)}`);
884
+ const meta = [];
885
+ if (item.priority) meta.push(item.priority);
886
+ if (item.assignee) meta.push(`-> ${item.assignee}`);
887
+ if (meta.length > 0) {
888
+ lines.push(` ${chalk8.dim(meta.join(" "))}`);
889
+ }
890
+ lines.push("");
891
+ }
892
+ return lines;
893
+ });
894
+ const maxLines = Math.max(...columnContents.map((c) => c.length));
895
+ for (const col of columnContents) {
896
+ while (col.length < maxLines) {
897
+ col.push("");
898
+ }
899
+ }
900
+ const headers = COLUMNS.map(
901
+ (col) => col.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
902
+ );
903
+ const table = new Table5({
904
+ head: headers,
905
+ style: { head: ["cyan"] },
906
+ colWidths: COLUMNS.map(() => COLUMN_WIDTH),
907
+ wordWrap: true
908
+ });
909
+ for (let i = 0; i < maxLines; i++) {
910
+ const row = columnContents.map((col) => col[i] ?? "");
911
+ table.push(row);
912
+ }
913
+ console.log(`
914
+ ${chalk8.bold(`Board - ${slug}`)}
915
+ `);
916
+ console.log(table.toString());
917
+ })
918
+ );
919
+
920
+ // src/commands/doctor.ts
921
+ import { Command as Command8 } from "commander";
922
+ import chalk9 from "chalk";
923
+ import Table6 from "cli-table3";
924
+ import { execa } from "execa";
925
+ async function checkBinary(name) {
926
+ try {
927
+ const { stdout } = await execa(name, ["--version"]);
928
+ const version = stdout.trim().split("\n")[0];
929
+ return { name, status: "pass", detail: version };
930
+ } catch {
931
+ return { name, status: "fail", detail: "Not found in PATH" };
932
+ }
933
+ }
934
+ async function checkNodeVersion() {
935
+ const version = process.version;
936
+ const major = parseInt(version.slice(1).split(".")[0], 10);
937
+ if (major >= 20) {
938
+ return { name: "Node.js", status: "pass", detail: version };
939
+ }
940
+ return {
941
+ name: "Node.js",
942
+ status: "warn",
943
+ detail: `${version} (>= 20 recommended)`
944
+ };
945
+ }
946
+ async function checkApi() {
947
+ try {
948
+ const health = await apiGet("/health");
949
+ const parts = [health.status];
950
+ if (health.version) parts.push(`v${health.version}`);
951
+ if (health.db) parts.push(`db: ${health.db}`);
952
+ return { name: "API Server", status: "pass", detail: parts.join(", ") };
953
+ } catch {
954
+ return {
955
+ name: "API Server",
956
+ status: "fail",
957
+ detail: `Cannot reach ${getApiUrl()}`
958
+ };
959
+ }
960
+ }
961
+ async function checkAuth() {
962
+ const token = getToken();
963
+ if (!token) {
964
+ return { name: "Authentication", status: "warn", detail: "Not logged in" };
965
+ }
966
+ try {
967
+ const user = await apiGet("/auth/me");
968
+ return {
969
+ name: "Authentication",
970
+ status: "pass",
971
+ detail: `${user.username} (${user.email})`
972
+ };
973
+ } catch {
974
+ return {
975
+ name: "Authentication",
976
+ status: "fail",
977
+ detail: "Token is invalid or expired"
978
+ };
979
+ }
980
+ }
981
+ function statusIcon(status) {
982
+ switch (status) {
983
+ case "pass":
984
+ return chalk9.green("PASS");
985
+ case "fail":
986
+ return chalk9.red("FAIL");
987
+ case "warn":
988
+ return chalk9.yellow("WARN");
989
+ }
990
+ }
991
+ var doctorCommand = new Command8("doctor").description("Check prerequisites and connectivity").action(
992
+ () => withErrorHandling(async () => {
993
+ console.log(chalk9.bold("\nBumblebee Doctor\n"));
994
+ console.log("Checking prerequisites...\n");
995
+ const results = await Promise.all([
996
+ checkBinary("claude"),
997
+ checkBinary("docker"),
998
+ checkBinary("git"),
999
+ checkNodeVersion(),
1000
+ checkApi(),
1001
+ checkAuth()
1002
+ ]);
1003
+ const table = new Table6({
1004
+ head: ["Check", "Status", "Details"],
1005
+ style: { head: ["cyan"] },
1006
+ colWidths: [18, 10, 50]
1007
+ });
1008
+ for (const result of results) {
1009
+ table.push([result.name, statusIcon(result.status), result.detail]);
1010
+ }
1011
+ console.log(table.toString());
1012
+ const failures = results.filter((r) => r.status === "fail");
1013
+ const warnings = results.filter((r) => r.status === "warn");
1014
+ console.log("");
1015
+ if (failures.length > 0) {
1016
+ console.log(
1017
+ chalk9.red(
1018
+ `${failures.length} check(s) failed. Please resolve before continuing.`
1019
+ )
1020
+ );
1021
+ } else if (warnings.length > 0) {
1022
+ console.log(
1023
+ chalk9.yellow(
1024
+ `All checks passed with ${warnings.length} warning(s).`
1025
+ )
1026
+ );
1027
+ } else {
1028
+ console.log(chalk9.green("All checks passed!"));
1029
+ }
1030
+ })
1031
+ );
1032
+
1033
+ // src/commands/init.ts
1034
+ import { Command as Command9 } from "commander";
1035
+ import chalk11 from "chalk";
1036
+ import * as p3 from "@clack/prompts";
1037
+ import fs4 from "fs";
1038
+ import path4 from "path";
1039
+ import TOML2 from "@iarna/toml";
1040
+
1041
+ // src/lib/ide-setup.ts
1042
+ import fs3 from "fs";
1043
+ import path3 from "path";
1044
+ import { fileURLToPath } from "url";
1045
+ import chalk10 from "chalk";
1046
+ var __dirname = path3.dirname(fileURLToPath(import.meta.url));
1047
+ function templatesDir() {
1048
+ let dir = __dirname;
1049
+ for (let i = 0; i < 5; i++) {
1050
+ const candidate = path3.join(dir, "templates", "skills", "bb-agent");
1051
+ if (fs3.existsSync(candidate)) return candidate;
1052
+ dir = path3.dirname(dir);
1053
+ }
1054
+ return path3.join(__dirname, "..", "..", "templates", "skills", "bb-agent");
1055
+ }
1056
+ var SKILL_FILES = [
1057
+ "SKILL.md",
1058
+ "references/workflow.md",
1059
+ "references/investigate-workflow.md",
1060
+ "references/bb-commands.md",
1061
+ "references/prompts.md",
1062
+ "references/status-transitions.md",
1063
+ "references/parallel-workflow.md"
1064
+ ];
1065
+ function getTemplateContent(relativePath) {
1066
+ const fullPath = path3.join(templatesDir(), relativePath);
1067
+ return fs3.readFileSync(fullPath, "utf-8");
1068
+ }
1069
+ function buildMcpConfig(apiUrl, token) {
1070
+ return {
1071
+ type: "http",
1072
+ url: `${apiUrl}/mcp`,
1073
+ headers: {
1074
+ Authorization: `Bearer ${token}`
1075
+ }
1076
+ };
1077
+ }
1078
+ function installMcpConfig(projectRoot, targetDir) {
1079
+ const apiUrl = getApiUrl();
1080
+ const token = getToken();
1081
+ if (!token) {
1082
+ console.log(
1083
+ ` ${chalk10.yellow("!")} No auth token - skipping MCP config (run ${chalk10.bold("bb login")} first)`
1084
+ );
1085
+ return false;
1086
+ }
1087
+ const serverEntry = buildMcpConfig(apiUrl, token);
1088
+ let configPath;
1089
+ if (targetDir) {
1090
+ configPath = path3.join(projectRoot, targetDir, "mcp.json");
1091
+ fs3.mkdirSync(path3.dirname(configPath), { recursive: true });
1092
+ } else {
1093
+ configPath = path3.join(projectRoot, ".mcp.json");
1094
+ }
1095
+ let config;
1096
+ if (fs3.existsSync(configPath)) {
1097
+ const existing = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
1098
+ if (!existing.mcpServers) existing.mcpServers = {};
1099
+ existing.mcpServers.bumblebee = serverEntry;
1100
+ config = existing;
1101
+ } else {
1102
+ config = { mcpServers: { bumblebee: serverEntry } };
1103
+ }
1104
+ fs3.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1105
+ return true;
1106
+ }
1107
+ function installClaudeSkills(projectRoot) {
1108
+ const target = path3.join(projectRoot, ".claude", "skills", "bb-agent");
1109
+ fs3.mkdirSync(path3.join(target, "references"), { recursive: true });
1110
+ for (const relPath of SKILL_FILES) {
1111
+ const content = getTemplateContent(relPath);
1112
+ const dest = path3.join(target, relPath);
1113
+ fs3.mkdirSync(path3.dirname(dest), { recursive: true });
1114
+ fs3.writeFileSync(dest, content);
1115
+ }
1116
+ installMcpConfig(projectRoot);
1117
+ console.log(` ${chalk10.green("\u2713")} Installed Claude Code skills + MCP config`);
1118
+ }
1119
+ function flattenSkillToMarkdown() {
1120
+ const sections = [];
1121
+ let skillContent = getTemplateContent("SKILL.md");
1122
+ if (skillContent.startsWith("---")) {
1123
+ const end = skillContent.indexOf("---", 3);
1124
+ if (end !== -1) {
1125
+ skillContent = skillContent.slice(end + 3).trim();
1126
+ }
1127
+ }
1128
+ sections.push(skillContent);
1129
+ const refFiles = SKILL_FILES.filter((f) => f.startsWith("references/"));
1130
+ for (const relPath of refFiles) {
1131
+ const content = getTemplateContent(relPath);
1132
+ sections.push(`
1133
+ ---
1134
+
1135
+ ${content}`);
1136
+ }
1137
+ return sections.join("\n");
1138
+ }
1139
+ function installCursorRules(projectRoot) {
1140
+ const targetDir = path3.join(projectRoot, ".cursor", "rules");
1141
+ fs3.mkdirSync(targetDir, { recursive: true });
1142
+ const content = flattenSkillToMarkdown();
1143
+ fs3.writeFileSync(path3.join(targetDir, "bb-agent.md"), content);
1144
+ installMcpConfig(projectRoot, ".cursor");
1145
+ console.log(` ${chalk10.green("\u2713")} Installed Cursor rules + MCP config`);
1146
+ }
1147
+ function installAntigravityRules(projectRoot) {
1148
+ const targetDir = path3.join(projectRoot, ".antigravity", "rules");
1149
+ fs3.mkdirSync(targetDir, { recursive: true });
1150
+ const content = flattenSkillToMarkdown();
1151
+ fs3.writeFileSync(path3.join(targetDir, "bb-agent.md"), content);
1152
+ installMcpConfig(projectRoot, ".antigravity");
1153
+ console.log(` ${chalk10.green("\u2713")} Installed Antigravity rules + MCP config`);
1154
+ }
1155
+ function installSkills(projectRoot, ideList) {
1156
+ const installers = {
1157
+ claude: installClaudeSkills,
1158
+ cursor: installCursorRules,
1159
+ antigravity: installAntigravityRules
1160
+ };
1161
+ for (const ide of ideList) {
1162
+ const installer = installers[ide];
1163
+ if (installer) installer(projectRoot);
1164
+ }
1165
+ }
1166
+
1167
+ // src/commands/init.ts
1168
+ function parseIdesOption(value) {
1169
+ if (value === "none") return [];
1170
+ if (value === "all") return ["claude", "cursor", "antigravity"];
1171
+ const valid = /* @__PURE__ */ new Set(["claude", "cursor", "antigravity"]);
1172
+ const ides = value.split(",").map((s) => s.trim());
1173
+ for (const ide of ides) {
1174
+ if (!valid.has(ide)) {
1175
+ console.error(
1176
+ chalk11.red(
1177
+ `Unknown IDE: ${ide}. Valid: claude, cursor, antigravity, all, none`
1178
+ )
1179
+ );
1180
+ process.exit(1);
1181
+ }
1182
+ }
1183
+ return ides;
1184
+ }
1185
+ async function promptIdeSelection() {
1186
+ const result = await p3.select({
1187
+ message: "Install AI agent skills for your IDE?",
1188
+ options: [
1189
+ { value: "claude", label: "Claude Code (.claude/skills/)" },
1190
+ { value: "cursor", label: "Cursor (.cursor/rules/)" },
1191
+ { value: "antigravity", label: "Antigravity (.antigravity/rules/)" },
1192
+ { value: "all", label: "All of the above" },
1193
+ { value: "none", label: "Skip" }
1194
+ ],
1195
+ initialValue: "all"
1196
+ });
1197
+ if (p3.isCancel(result) || result === "none") return [];
1198
+ if (result === "all") return ["claude", "cursor", "antigravity"];
1199
+ return [result];
1200
+ }
1201
+ var initCommand = new Command9("init").description(
1202
+ "Initialize a .bumblebee/ config directory in the project root"
1203
+ ).option(
1204
+ "-p, --project <slug>",
1205
+ "Set current_project in local config"
1206
+ ).option("--api-url <url>", "Set api_url in local config").option(
1207
+ "--ides <ides>",
1208
+ "IDEs to install skills for (claude,cursor,antigravity,all,none)"
1209
+ ).option("-f, --force", "Overwrite existing config").action(
1210
+ (opts) => withErrorHandling(async () => {
1211
+ const root = findProjectRoot() ?? process.cwd();
1212
+ const bbDir = path4.join(root, LOCAL_DIR_NAME);
1213
+ const configFile = path4.join(bbDir, LOCAL_CONFIG_NAME);
1214
+ const gitignoreFile = path4.join(bbDir, ".gitignore");
1215
+ if (fs4.existsSync(configFile) && !opts.force) {
1216
+ console.log(
1217
+ chalk11.yellow(`.bumblebee/ already exists at ${root}`)
1218
+ );
1219
+ console.log(chalk11.dim("Use --force to overwrite."));
1220
+ process.exit(1);
1221
+ }
1222
+ fs4.mkdirSync(bbDir, { recursive: true });
1223
+ let project = opts.project;
1224
+ let selectedName;
1225
+ if (!project) {
1226
+ try {
1227
+ const projects = await apiGet("/api/projects");
1228
+ if (projects && projects.length > 0) {
1229
+ const options = [
1230
+ ...projects.map((proj) => ({
1231
+ value: proj.slug,
1232
+ label: `${proj.slug} - ${proj.name}`
1233
+ })),
1234
+ { value: "__new__", label: "+ Create new project" }
1235
+ ];
1236
+ const selected = await p3.select({
1237
+ message: "Select a project",
1238
+ options
1239
+ });
1240
+ if (p3.isCancel(selected)) {
1241
+ } else if (selected === "__new__") {
1242
+ const name = await p3.text({ message: "Project name" });
1243
+ if (!p3.isCancel(name)) {
1244
+ const defaultSlug = name.toLowerCase().replace(/\s+/g, "-");
1245
+ const slug = await p3.text({
1246
+ message: "Project slug",
1247
+ initialValue: defaultSlug
1248
+ });
1249
+ if (!p3.isCancel(slug)) {
1250
+ const created = await apiPost(
1251
+ "/api/projects",
1252
+ { name, slug }
1253
+ );
1254
+ project = created.slug;
1255
+ selectedName = created.name;
1256
+ console.log(
1257
+ chalk11.green(
1258
+ `Created project ${chalk11.bold(project)}`
1259
+ )
1260
+ );
1261
+ }
1262
+ }
1263
+ } else {
1264
+ project = selected;
1265
+ selectedName = projects.find(
1266
+ (proj) => proj.slug === project
1267
+ )?.name;
1268
+ }
1269
+ }
1270
+ } catch {
1271
+ }
1272
+ }
1273
+ const cfg = {};
1274
+ if (project) cfg.current_project = project;
1275
+ if (opts.apiUrl) cfg.api_url = opts.apiUrl;
1276
+ if (Object.keys(cfg).length > 0) {
1277
+ fs4.writeFileSync(configFile, TOML2.stringify(cfg));
1278
+ } else {
1279
+ fs4.writeFileSync(
1280
+ configFile,
1281
+ "# Bumblebee project-local config\n# Settings here override ~/.bumblebee/config.toml\n"
1282
+ );
1283
+ }
1284
+ if (!fs4.existsSync(gitignoreFile) || opts.force) {
1285
+ fs4.writeFileSync(gitignoreFile, "*.local.toml\n");
1286
+ }
1287
+ if (project) {
1288
+ setProjectPath(project, path4.resolve(root));
1289
+ }
1290
+ console.log(chalk11.green(`Initialized .bumblebee/ at ${root}`));
1291
+ if (project) {
1292
+ const display = selectedName ? `${selectedName} (${project})` : project;
1293
+ console.log(` current_project = ${display}`);
1294
+ console.log(` linked path = ${path4.resolve(root)}`);
1295
+ }
1296
+ if (opts.apiUrl) {
1297
+ console.log(` api_url = ${opts.apiUrl}`);
1298
+ }
1299
+ if (!project) {
1300
+ console.log(
1301
+ chalk11.dim(
1302
+ "Tip: Add project-specific settings to .bumblebee/config.toml"
1303
+ )
1304
+ );
1305
+ }
1306
+ let ideList;
1307
+ if (opts.ides) {
1308
+ ideList = parseIdesOption(opts.ides);
1309
+ } else {
1310
+ ideList = await promptIdeSelection();
1311
+ }
1312
+ if (ideList.length > 0) {
1313
+ installSkills(root, ideList);
1314
+ }
1315
+ console.log(chalk11.green.bold("\nDone!"));
1316
+ })
1317
+ );
1318
+
1319
+ // src/commands/agent/index.ts
1320
+ import { Command as Command23 } from "commander";
1321
+
1322
+ // src/commands/agent/suggest.ts
1323
+ import { Command as Command10 } from "commander";
1324
+ import chalk13 from "chalk";
1325
+ import pLimit from "p-limit";
1326
+
1327
+ // src/lib/claude-runner.ts
1328
+ import { execa as execa2 } from "execa";
1329
+ import fs5 from "fs";
1330
+ import { execSync } from "child_process";
1331
+ import { createInterface } from "readline";
1332
+ var CLAUDE_PROCESS_TIMEOUT = 30 * 60 * 1e3;
1333
+ var CLAUDE_STALL_TIMEOUT = 5 * 60 * 1e3;
1334
+ function claudeBin() {
1335
+ try {
1336
+ const cmd = process.platform === "win32" ? "where claude" : "which claude";
1337
+ const resolved = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).split("\n")[0].trim();
1338
+ if (resolved) return resolved;
1339
+ } catch {
1340
+ }
1341
+ if (process.platform === "win32") {
1342
+ const home = process.env.USERPROFILE ?? process.env.HOME ?? "";
1343
+ const candidates = [
1344
+ `${home}\\.local\\bin\\claude.exe`,
1345
+ `${home}\\AppData\\Roaming\\npm\\claude.cmd`
1346
+ ];
1347
+ for (const candidate of candidates) {
1348
+ if (fs5.existsSync(candidate)) return candidate;
1349
+ }
1350
+ }
1351
+ return "claude";
1352
+ }
1353
+ function claudeEnv() {
1354
+ const env = { ...process.env };
1355
+ delete env.CLAUDECODE;
1356
+ return env;
1357
+ }
1358
+ async function runClaude(opts) {
1359
+ const resolvedCmd = [...opts.cmd];
1360
+ if (resolvedCmd[0] === "claude") {
1361
+ resolvedCmd[0] = claudeBin();
1362
+ }
1363
+ const timeout = opts.timeout ?? CLAUDE_PROCESS_TIMEOUT;
1364
+ const stallTimeout = opts.stallTimeout ?? CLAUDE_STALL_TIMEOUT;
1365
+ const stderrChunks = [];
1366
+ let lastOutputTime = Date.now();
1367
+ let timedOut = false;
1368
+ let stalled = false;
1369
+ const controller = new AbortController();
1370
+ const totalTimer = setTimeout(() => {
1371
+ timedOut = true;
1372
+ controller.abort();
1373
+ }, timeout);
1374
+ const stallChecker = setInterval(() => {
1375
+ if (Date.now() - lastOutputTime >= stallTimeout) {
1376
+ stalled = true;
1377
+ controller.abort();
1378
+ }
1379
+ }, 3e4);
1380
+ try {
1381
+ const proc = execa2(resolvedCmd[0], resolvedCmd.slice(1), {
1382
+ cwd: opts.cwd,
1383
+ env: claudeEnv(),
1384
+ stdin: opts.stdinData ? "pipe" : void 0,
1385
+ stdout: "pipe",
1386
+ stderr: "pipe",
1387
+ cancelSignal: controller.signal,
1388
+ gracefulCancel: true
1389
+ });
1390
+ if (opts.stdinData && proc.stdin) {
1391
+ proc.stdin.write(opts.stdinData);
1392
+ proc.stdin.end();
1393
+ }
1394
+ proc.stderr?.on("data", (chunk) => {
1395
+ stderrChunks.push(chunk.toString());
1396
+ });
1397
+ if (proc.stdout) {
1398
+ const rl = createInterface({ input: proc.stdout });
1399
+ for await (const line of rl) {
1400
+ lastOutputTime = Date.now();
1401
+ if (opts.onLine) opts.onLine(line);
1402
+ }
1403
+ }
1404
+ const result = await proc;
1405
+ return {
1406
+ returncode: result.exitCode ?? 0,
1407
+ textBlocks: [],
1408
+ stderr: stderrChunks.join("").trim(),
1409
+ timedOut,
1410
+ stalled
1411
+ };
1412
+ } catch (err) {
1413
+ if (!timedOut && (err.exitCode === 137 || err.signal === "SIGKILL")) {
1414
+ stalled = true;
1415
+ }
1416
+ return {
1417
+ returncode: err.exitCode ?? 1,
1418
+ textBlocks: [],
1419
+ stderr: stderrChunks.join("").trim() || err.stderr || err.message,
1420
+ timedOut,
1421
+ stalled
1422
+ };
1423
+ } finally {
1424
+ clearTimeout(totalTimer);
1425
+ clearInterval(stallChecker);
1426
+ }
1427
+ }
1428
+
1429
+ // src/lib/agent-streamer.ts
1430
+ import fs6 from "fs";
1431
+ import path5 from "path";
1432
+ import os2 from "os";
1433
+ function logsDir() {
1434
+ const d = path5.join(os2.homedir(), ".bumblebee", "logs");
1435
+ fs6.mkdirSync(d, { recursive: true });
1436
+ return d;
1437
+ }
1438
+ var AgentStreamer = class _AgentStreamer {
1439
+ sessionId;
1440
+ textBlocks = [];
1441
+ logPath;
1442
+ logStream;
1443
+ buffer = [];
1444
+ flushTimer = null;
1445
+ callback;
1446
+ static BATCH_SIZE = 20;
1447
+ static FLUSH_INTERVAL = 500;
1448
+ // ms
1449
+ constructor(sessionId) {
1450
+ this.sessionId = sessionId;
1451
+ this.logPath = path5.join(logsDir(), `session-${sessionId}.jsonl`);
1452
+ this.logStream = fs6.createWriteStream(this.logPath, { flags: "a" });
1453
+ }
1454
+ /** All extracted text blocks from assistant messages. */
1455
+ get texts() {
1456
+ return this.textBlocks;
1457
+ }
1458
+ /** Path to the local log file. */
1459
+ get logFile() {
1460
+ return this.logPath;
1461
+ }
1462
+ /**
1463
+ * Set a callback invoked for each parsed event.
1464
+ * Signature: (payload: any, text: string | null) => void
1465
+ */
1466
+ setCallback(cb) {
1467
+ this.callback = cb;
1468
+ }
1469
+ /** Start the periodic flush timer. */
1470
+ start() {
1471
+ this.flushTimer = setInterval(() => this.flush(), _AgentStreamer.FLUSH_INTERVAL);
1472
+ }
1473
+ /**
1474
+ * Feed a raw JSON line from Claude CLI stdout.
1475
+ * Parses JSON, writes to log, extracts text blocks, buffers for relay.
1476
+ */
1477
+ feed(line) {
1478
+ line = line.trim();
1479
+ if (!line) return;
1480
+ let payload;
1481
+ try {
1482
+ payload = JSON.parse(line);
1483
+ } catch {
1484
+ return;
1485
+ }
1486
+ this.logStream.write(line + "\n");
1487
+ let text3 = null;
1488
+ if (payload.type === "assistant") {
1489
+ for (const block of payload.content ?? []) {
1490
+ if (block.type === "text") {
1491
+ text3 = block.text;
1492
+ if (text3) this.textBlocks.push(text3);
1493
+ }
1494
+ }
1495
+ }
1496
+ if (this.callback) {
1497
+ try {
1498
+ this.callback(payload, text3);
1499
+ } catch {
1500
+ }
1501
+ }
1502
+ this.buffer.push(payload);
1503
+ if (this.buffer.length >= _AgentStreamer.BATCH_SIZE) {
1504
+ this.flush();
1505
+ }
1506
+ }
1507
+ /**
1508
+ * Stop streaming, flush remaining events, close log file.
1509
+ * Returns all extracted text blocks.
1510
+ */
1511
+ stop() {
1512
+ if (this.flushTimer) {
1513
+ clearInterval(this.flushTimer);
1514
+ this.flushTimer = null;
1515
+ }
1516
+ this.flush();
1517
+ this.logStream.end();
1518
+ return this.textBlocks;
1519
+ }
1520
+ /** Flush buffered events to the API (fire-and-forget). */
1521
+ flush() {
1522
+ if (this.buffer.length === 0) return;
1523
+ const events = [...this.buffer];
1524
+ this.buffer = [];
1525
+ apiPost(
1526
+ `/api/agent-sessions/${this.sessionId}/relay-batch`,
1527
+ { events }
1528
+ ).catch(() => {
1529
+ });
1530
+ }
1531
+ };
1532
+ async function updatePhase(sessionId, phase, extra) {
1533
+ try {
1534
+ await apiPatch(`/api/agent-sessions/${sessionId}/phase`, {
1535
+ phase,
1536
+ ...extra
1537
+ });
1538
+ } catch {
1539
+ }
1540
+ }
1541
+ async function completeSession(sessionId, status, extra) {
1542
+ try {
1543
+ await apiPost(`/api/agent-sessions/${sessionId}/complete`, {
1544
+ status,
1545
+ ...extra
1546
+ });
1547
+ } catch {
1548
+ }
1549
+ }
1550
+
1551
+ // src/lib/prompt-builder.ts
1552
+ import fs7 from "fs";
1553
+ import path6 from "path";
1554
+ function readKnowledge(projectPath) {
1555
+ const candidates = [
1556
+ "CLAUDE.md",
1557
+ "docs/knowledge.md",
1558
+ ".claude/lessons-learned.md"
1559
+ ];
1560
+ const parts = [];
1561
+ for (const rel of candidates) {
1562
+ const fp = path6.join(projectPath, rel);
1563
+ if (fs7.existsSync(fp)) {
1564
+ try {
1565
+ const text3 = fs7.readFileSync(fp, "utf-8").trim();
1566
+ if (text3) parts.push(`### ${rel}
1567
+
1568
+ ${text3}`);
1569
+ } catch {
1570
+ }
1571
+ }
1572
+ }
1573
+ return parts.join("\n\n---\n\n");
1574
+ }
1575
+ async function getItemComments(itemId) {
1576
+ try {
1577
+ return await apiGet(`/api/work-items/${itemId}/comments`);
1578
+ } catch {
1579
+ return [];
1580
+ }
1581
+ }
1582
+ function formatCommentsContext(comments) {
1583
+ if (!comments.length) return "";
1584
+ const parts = ["## Previous Comments / Progress"];
1585
+ for (const c of comments) {
1586
+ const tag = c.type !== "discussion" ? ` [${c.type}]` : "";
1587
+ parts.push(`
1588
+ ### ${c.author}${tag} -- ${c.created_at}
1589
+ ${c.body}`);
1590
+ }
1591
+ return parts.join("\n");
1592
+ }
1593
+ function truncateOutput(output, maxChars = 4e3) {
1594
+ if (output.length <= maxChars) return output;
1595
+ return `... (truncated, showing last ${maxChars} chars)
1596
+ ` + output.slice(-maxChars);
1597
+ }
1598
+ function itemHeader(item, verb, includeStatus = false) {
1599
+ const key = item.key ?? `#${item.number}`;
1600
+ const parts = [`You are ${verb} ${item.type} ${key}: ${item.title}`, ""];
1601
+ if (includeStatus) {
1602
+ parts.push(
1603
+ `Type: ${item.type} | Priority: ${item.priority} | Status: ${item.status}`
1604
+ );
1605
+ } else {
1606
+ parts.push(`Type: ${item.type} | Priority: ${item.priority}`);
1607
+ }
1608
+ return parts;
1609
+ }
1610
+ function appendContext(parts, item, knowledge, commentsCtx, planHeading = "## Existing Plan") {
1611
+ if (item.description) {
1612
+ parts.push("", "## Description", item.description);
1613
+ }
1614
+ if (item.acceptance_criteria) {
1615
+ parts.push("", "## Acceptance Criteria", item.acceptance_criteria);
1616
+ }
1617
+ if (item.plan) {
1618
+ parts.push("", planHeading, item.plan);
1619
+ }
1620
+ if (commentsCtx) {
1621
+ parts.push("", commentsCtx);
1622
+ }
1623
+ if (knowledge) {
1624
+ parts.push("", "## Project Knowledge Base", knowledge);
1625
+ }
1626
+ }
1627
+ function buildSuggestPrompt(item, knowledge, commentsCtx) {
1628
+ const parts = itemHeader(
1629
+ item,
1630
+ "analysing",
1631
+ /* includeStatus */
1632
+ true
1633
+ );
1634
+ appendContext(parts, item, knowledge, commentsCtx, "## Existing Plan");
1635
+ parts.push(
1636
+ "",
1637
+ "## Your Task",
1638
+ "",
1639
+ "Analyse this work item **and** the project source code. Return a Markdown plan:",
1640
+ "",
1641
+ "1. **Root Cause / Analysis** -- what needs to change and why",
1642
+ "2. **Files to Modify** -- list every file with a short description of the change",
1643
+ "3. **Implementation Steps** -- numbered, concrete steps",
1644
+ "4. **Testing Strategy** -- how to verify the changes",
1645
+ "5. **Risks & Considerations** -- edge cases, breaking changes",
1646
+ "",
1647
+ "IMPORTANT: Do NOT modify any files. Only analyse and produce the plan."
1648
+ );
1649
+ parts.push(
1650
+ "",
1651
+ "## Split Analysis (Required)",
1652
+ "",
1653
+ "Analyze which packages (api/, web/, cli/) this item affects.",
1654
+ "Output in exact format at the end of your response:",
1655
+ "",
1656
+ "### SPLIT_RESULT",
1657
+ "NEEDS_SPLIT: true|false",
1658
+ "ITEMS:",
1659
+ "- SCOPE: <package>",
1660
+ " TITLE: <task title>",
1661
+ " DESCRIPTION: <what to do>",
1662
+ " ACCEPTANCE_CRITERIA: <testable criteria>",
1663
+ "",
1664
+ "If the item only affects one package, set NEEDS_SPLIT: false and omit ITEMS."
1665
+ );
1666
+ return parts.join("\n");
1667
+ }
1668
+ function buildExecutePrompt(item, knowledge, commentsCtx) {
1669
+ const parts = itemHeader(item, "implementing");
1670
+ appendContext(parts, item, knowledge, commentsCtx, "## Implementation Plan");
1671
+ parts.push(
1672
+ "",
1673
+ "## Instructions",
1674
+ "",
1675
+ "Implement the changes described in the plan / comments above.",
1676
+ "",
1677
+ "1. Follow the project's existing coding conventions and patterns",
1678
+ "2. Work through changes one file at a time",
1679
+ "3. Run existing tests after your changes and fix any failures",
1680
+ "4. Add new tests where appropriate",
1681
+ "5. Commit your work with a clear, descriptive commit message",
1682
+ "6. If you hit a blocker, document it clearly so the next run can continue"
1683
+ );
1684
+ return parts.join("\n");
1685
+ }
1686
+ function buildTestPrompt(item, knowledge, commentsCtx) {
1687
+ const parts = itemHeader(item, "verifying the implementation of");
1688
+ if (item.description) {
1689
+ parts.push("", "## Description", item.description);
1690
+ }
1691
+ if (item.acceptance_criteria) {
1692
+ parts.push("", "## Acceptance Criteria", item.acceptance_criteria);
1693
+ }
1694
+ if (commentsCtx) {
1695
+ parts.push("", commentsCtx);
1696
+ }
1697
+ if (knowledge) {
1698
+ parts.push("", "## Project Knowledge Base", knowledge);
1699
+ }
1700
+ parts.push(
1701
+ "",
1702
+ "## Your Task",
1703
+ "",
1704
+ "Run ALL relevant tests and verify the implementation. Follow these steps:",
1705
+ "",
1706
+ "1. Identify test commands from CLAUDE.md or project config (pytest, vitest, npm test, etc.)",
1707
+ "2. Run all relevant test suites",
1708
+ "3. Check acceptance criteria from the work item (if any)",
1709
+ "4. Review the git diff for obvious issues",
1710
+ "",
1711
+ "Return a structured test report in this format:",
1712
+ "",
1713
+ "## Test Report",
1714
+ "",
1715
+ "### Results",
1716
+ "- **Status**: PASS or FAIL",
1717
+ "- **Tests run**: <count>",
1718
+ "- **Passed**: <count>",
1719
+ "- **Failed**: <count>",
1720
+ "",
1721
+ "### Failing Tests (if any)",
1722
+ "- Test name: reason for failure",
1723
+ "",
1724
+ "### Acceptance Criteria Check",
1725
+ "- [x] or [ ] for each criterion",
1726
+ "",
1727
+ "### Root Cause Analysis (if failures)",
1728
+ "Brief analysis of why tests are failing.",
1729
+ "",
1730
+ "IMPORTANT: Do NOT fix any code. Only run tests and report results."
1731
+ );
1732
+ return parts.join("\n");
1733
+ }
1734
+ function buildReimplementPrompt(item, knowledge, commentsCtx, dockerOutput = "") {
1735
+ const key = item.key ?? `#${item.number}`;
1736
+ const parts = [
1737
+ `You are RE-IMPLEMENTING ${item.type} ${key}: ${item.title}`,
1738
+ "",
1739
+ `Type: ${item.type} | Priority: ${item.priority}`,
1740
+ "",
1741
+ "**IMPORTANT: A previous implementation attempt had test failures.**",
1742
+ "Read the comments below carefully \u2014 they contain the original plan,",
1743
+ "execution report, and test failure details. Fix the issues identified",
1744
+ "in the test report."
1745
+ ];
1746
+ appendContext(parts, item, knowledge, commentsCtx, "## Implementation Plan");
1747
+ if (dockerOutput) {
1748
+ const truncated = truncateOutput(dockerOutput, 3e3);
1749
+ parts.push(
1750
+ "",
1751
+ "## Docker Test Output (from last run)",
1752
+ "",
1753
+ "The following is the raw Docker test output. Use this to identify exact failures:",
1754
+ "",
1755
+ `\`\`\`
1756
+ ${truncated}
1757
+ \`\`\``
1758
+ );
1759
+ }
1760
+ parts.push(
1761
+ "",
1762
+ "## Instructions",
1763
+ "",
1764
+ "1. Read the test report and failure reasons from previous comments AND the Docker output above",
1765
+ "2. Identify what went wrong in the previous implementation",
1766
+ "3. Fix the issues \u2014 focus on the root causes identified in the test report",
1767
+ "4. Ensure all tests pass after your changes",
1768
+ "5. Run the full test suite to verify no regressions",
1769
+ "6. Commit your fixes with a clear message referencing the re-implementation",
1770
+ "7. If you hit a blocker, document it clearly"
1771
+ );
1772
+ return parts.join("\n");
1773
+ }
1774
+ function buildVerifyPrompt(item, knowledge, commentsCtx) {
1775
+ const parts = itemHeader(
1776
+ item,
1777
+ "verifying requirements for",
1778
+ /* includeStatus */
1779
+ true
1780
+ );
1781
+ appendContext(parts, item, knowledge, commentsCtx, "## Existing Plan");
1782
+ parts.push(
1783
+ "",
1784
+ "## Your Task",
1785
+ "",
1786
+ "Analyse this work item's requirements AND the project source code. Determine if the item is ready for implementation.",
1787
+ "",
1788
+ "Check the following:",
1789
+ "1. Are the requirements clear and specific enough to implement?",
1790
+ "2. Do acceptance criteria exist and are they testable?",
1791
+ "3. Is the requested change feasible given the current codebase?",
1792
+ "4. Are there any blockers, missing dependencies, or unclear areas?",
1793
+ "",
1794
+ "Return a structured analysis:",
1795
+ "",
1796
+ "## Requirement Analysis",
1797
+ "",
1798
+ "### Clarity",
1799
+ "- Are requirements clear? What's ambiguous?",
1800
+ "",
1801
+ "### Feasibility",
1802
+ "- What files need to change?",
1803
+ "- Is the architecture compatible?",
1804
+ "- Any technical blockers?",
1805
+ "",
1806
+ "### Solution Approach",
1807
+ "- Proposed approach (high-level)",
1808
+ "- Files to modify with brief description",
1809
+ "- Estimated complexity (low/medium/high)",
1810
+ "",
1811
+ "### Blockers & Missing Information",
1812
+ "- List anything that would prevent implementation",
1813
+ "",
1814
+ "### VERDICT: READY",
1815
+ "or",
1816
+ "### VERDICT: NEEDS_INFO",
1817
+ "- If NEEDS_INFO, list exactly what information is missing",
1818
+ "",
1819
+ "IMPORTANT:",
1820
+ "- You MUST include exactly one verdict line: `VERDICT: READY` or `VERDICT: NEEDS_INFO`",
1821
+ "- Do NOT modify any files. Only analyse and produce the assessment."
1822
+ );
1823
+ return parts.join("\n");
1824
+ }
1825
+
1826
+ // src/lib/progress-tracker.ts
1827
+ import logUpdate from "log-update";
1828
+ import chalk12 from "chalk";
1829
+ var AgentProgressTracker = class {
1830
+ data = /* @__PURE__ */ new Map();
1831
+ startTime = Date.now();
1832
+ completed = 0;
1833
+ total = 0;
1834
+ timer = null;
1835
+ /** Register an item before work starts. */
1836
+ register(itemKey) {
1837
+ this.data.set(itemKey, {
1838
+ phase: "pending",
1839
+ status: "waiting",
1840
+ lastLine: "",
1841
+ start: Date.now()
1842
+ });
1843
+ this.total++;
1844
+ }
1845
+ /** Update progress for an item. */
1846
+ update(itemKey, phase, status, lastLine) {
1847
+ const existing = this.data.get(itemKey);
1848
+ const start = existing?.start ?? Date.now();
1849
+ this.data.set(itemKey, { phase, status, lastLine, start });
1850
+ this.render();
1851
+ }
1852
+ /** Mark an item as completed or failed. */
1853
+ complete(itemKey, success, message = "") {
1854
+ const existing = this.data.get(itemKey);
1855
+ if (existing) {
1856
+ existing.status = success ? "done" : "failed";
1857
+ existing.lastLine = message.slice(0, 60);
1858
+ }
1859
+ this.completed++;
1860
+ this.render();
1861
+ }
1862
+ /** Start periodic render updates (every 500ms). */
1863
+ start() {
1864
+ this.timer = setInterval(() => this.render(), 500);
1865
+ }
1866
+ /** Stop updates and persist final output. */
1867
+ stop() {
1868
+ if (this.timer) {
1869
+ clearInterval(this.timer);
1870
+ this.timer = null;
1871
+ }
1872
+ this.render();
1873
+ logUpdate.done();
1874
+ }
1875
+ /** Render the progress table to the terminal using log-update. */
1876
+ render() {
1877
+ const lines = [];
1878
+ lines.push(
1879
+ chalk12.bold.cyan(
1880
+ " Item Phase Status Last Output"
1881
+ )
1882
+ );
1883
+ lines.push(chalk12.dim(" " + "\u2500".repeat(70)));
1884
+ for (const [key, info] of this.data) {
1885
+ const statusColor = info.status === "running" ? chalk12.yellow : info.status === "done" ? chalk12.green : info.status === "failed" ? chalk12.red : chalk12.dim;
1886
+ lines.push(
1887
+ ` ${chalk12.bold(key.padEnd(12))} ${info.phase.padEnd(14)} ${statusColor(info.status.padEnd(10))} ${chalk12.dim(info.lastLine.slice(0, 50))}`
1888
+ );
1889
+ }
1890
+ const elapsed = Math.floor((Date.now() - this.startTime) / 1e3);
1891
+ const mins = Math.floor(elapsed / 60);
1892
+ const secs = elapsed % 60;
1893
+ lines.push("");
1894
+ lines.push(
1895
+ chalk12.dim(
1896
+ ` Elapsed: ${mins}m ${secs.toString().padStart(2, "0")}s | ${this.completed}/${this.total} complete | Ctrl+C to abort`
1897
+ )
1898
+ );
1899
+ logUpdate(lines.join("\n"));
1900
+ }
1901
+ };
1902
+
1903
+ // src/commands/agent/suggest.ts
1904
+ async function suggestOne(slug, projectPath, idOrNumber, tracker) {
1905
+ let item;
1906
+ try {
1907
+ item = await resolveItemId(slug, idOrNumber);
1908
+ } catch (e) {
1909
+ return { key: idOrNumber, status: "failed", error: `Resolve failed: ${e.message}` };
1910
+ }
1911
+ const itemId = item.id;
1912
+ const key = item.key ?? `#${item.number}`;
1913
+ let sessionId = null;
1914
+ try {
1915
+ const session = await apiPost("/api/agent-sessions/start", {
1916
+ work_item_id: itemId,
1917
+ origin: "cli",
1918
+ phase: "suggest"
1919
+ }, { project_slug: slug });
1920
+ sessionId = session.id;
1921
+ } catch {
1922
+ }
1923
+ if (tracker && sessionId) {
1924
+ tracker.update(key, "suggest", "running", "Starting analysis...");
1925
+ }
1926
+ const knowledge = readKnowledge(projectPath);
1927
+ const comments = await getItemComments(itemId);
1928
+ const commentsCtx = formatCommentsContext(comments);
1929
+ const prompt = buildSuggestPrompt(item, knowledge, commentsCtx);
1930
+ let streamer = null;
1931
+ if (sessionId) {
1932
+ streamer = new AgentStreamer(sessionId);
1933
+ if (tracker) {
1934
+ streamer.setCallback((_p, t) => {
1935
+ if (t) tracker.update(key, "suggest", "running", t.slice(0, 60));
1936
+ });
1937
+ }
1938
+ streamer.start();
1939
+ await updatePhase(sessionId, "suggest");
1940
+ }
1941
+ const result = await runClaude({
1942
+ cmd: ["claude", "-p", prompt, "--output-format", "stream-json"],
1943
+ cwd: projectPath,
1944
+ onLine: (line) => streamer?.feed(line)
1945
+ });
1946
+ const textBlocks = streamer?.stop() ?? [];
1947
+ if (result.returncode !== 0) {
1948
+ if (sessionId) await completeSession(sessionId, "failed", { error: result.stderr.slice(0, 500) });
1949
+ return { key, status: "failed", error: result.stderr.slice(0, 500) || `Exit code ${result.returncode}` };
1950
+ }
1951
+ const suggestion = textBlocks.join("\n\n");
1952
+ if (!suggestion) {
1953
+ if (sessionId) await completeSession(sessionId, "failed", { error: "Empty response" });
1954
+ return { key, status: "failed", error: "Empty response" };
1955
+ }
1956
+ await apiPost(`/api/work-items/${itemId}/comments`, {
1957
+ body: suggestion,
1958
+ author: "bb-agent",
1959
+ type: "proposal"
1960
+ });
1961
+ if (item.status === "open") {
1962
+ await apiPut(`/api/work-items/${itemId}`, { status: "confirmed" });
1963
+ }
1964
+ if (sessionId) await completeSession(sessionId, "completed");
1965
+ return { key, status: "ok", suggestion: suggestion.slice(0, 200) };
1966
+ }
1967
+ var suggestCommand = new Command10("suggest").description("Phase 1: Analyse a work item and post a solution plan").argument("<id>", "Work item ID, number, or KEY-number").action(async (id) => {
1968
+ await withErrorHandling(async () => {
1969
+ const slug = requireProject();
1970
+ const projectPath = requireProjectPath(slug);
1971
+ console.log(chalk13.cyan(`Fetching work item ${id}...`));
1972
+ const item = await resolveItemId(slug, id);
1973
+ const itemId = item.id;
1974
+ const key = item.key ?? `#${item.number}`;
1975
+ const knowledge = readKnowledge(projectPath);
1976
+ const comments = await getItemComments(itemId);
1977
+ const commentsCtx = formatCommentsContext(comments);
1978
+ const prompt = buildSuggestPrompt(item, knowledge, commentsCtx);
1979
+ console.log(chalk13.cyan(`Running Claude Code analysis in ${projectPath}...`));
1980
+ const result = await runClaude({
1981
+ cmd: ["claude", "-p", prompt, "--output-format", "stream-json"],
1982
+ cwd: projectPath,
1983
+ onLine: (line) => {
1984
+ try {
1985
+ const payload = JSON.parse(line);
1986
+ if (payload.type === "assistant") {
1987
+ for (const block of payload.content ?? []) {
1988
+ if (block.type === "text") process.stdout.write(block.text);
1989
+ }
1990
+ }
1991
+ } catch {
1992
+ }
1993
+ }
1994
+ });
1995
+ if (result.returncode !== 0) {
1996
+ console.error(chalk13.red(`Claude analysis failed:
1997
+ ${result.stderr}`));
1998
+ process.exit(1);
1999
+ }
2000
+ const suggestion = result.textBlocks.join("\n\n");
2001
+ if (!suggestion) {
2002
+ console.error(chalk13.red("Claude returned an empty response."));
2003
+ process.exit(1);
2004
+ }
2005
+ await apiPost(`/api/work-items/${itemId}/comments`, {
2006
+ body: suggestion,
2007
+ author: "bb-agent",
2008
+ type: "proposal"
2009
+ });
2010
+ console.log(chalk13.green("Suggestion posted as comment on the work item."));
2011
+ await apiPut(`/api/work-items/${itemId}`, { plan: suggestion });
2012
+ console.log(chalk13.green("Plan saved to work item."));
2013
+ if (item.status === "open") {
2014
+ await apiPut(`/api/work-items/${itemId}`, { status: "confirmed" });
2015
+ console.log(chalk13.dim("Status -> confirmed"));
2016
+ }
2017
+ });
2018
+ });
2019
+ var batchSuggestCommand = new Command10("batch-suggest").description("Analyse multiple work items in parallel").argument("[items...]", "Work item IDs/numbers").option("-A, --all", "Suggest all open items").option("-P, --parallel <n>", "Max parallel analyses", "3").action(async (items, opts) => {
2020
+ await withErrorHandling(async () => {
2021
+ const slug = requireProject();
2022
+ const projectPath = requireProjectPath(slug);
2023
+ const maxParallel = parseInt(opts.parallel, 10);
2024
+ if (opts.all) {
2025
+ const openItems = await apiGet(`/api/projects/${slug}/work-items`, { status: "open" });
2026
+ const filtered = openItems.filter((i) => i.type !== "epic");
2027
+ if (!filtered.length) {
2028
+ console.log(chalk13.yellow("No open items found."));
2029
+ return;
2030
+ }
2031
+ items = filtered.map((i) => i.key ?? String(i.number));
2032
+ console.log(chalk13.cyan(`Found ${items.length} open items: ${items.join(", ")}`));
2033
+ } else if (!items.length) {
2034
+ console.error(chalk13.red("Provide item IDs or use --all flag."));
2035
+ process.exit(1);
2036
+ }
2037
+ console.log(chalk13.cyan(`Suggesting ${items.length} items (max ${maxParallel} parallel)...
2038
+ `));
2039
+ const tracker = new AgentProgressTracker();
2040
+ for (const ref of items) tracker.register(ref);
2041
+ tracker.start();
2042
+ const limit = pLimit(maxParallel);
2043
+ const results = await Promise.all(
2044
+ items.map(
2045
+ (ref) => limit(async () => {
2046
+ const r = await suggestOne(slug, projectPath, ref, tracker);
2047
+ tracker.complete(ref, r.status === "ok", r.error ?? "Done");
2048
+ return r;
2049
+ })
2050
+ )
2051
+ );
2052
+ tracker.stop();
2053
+ const ok = results.filter((r) => r.status === "ok").length;
2054
+ console.log(chalk13.bold(`
2055
+ Done: ${ok}/${items.length} succeeded.`));
2056
+ if (ok > 0) {
2057
+ const suggested = results.filter((r) => r.status === "ok").map((r) => r.key);
2058
+ console.log(chalk13.dim(`
2059
+ Review suggestions, then run:`));
2060
+ console.log(chalk13.dim(` bb agent batch-execute ${suggested.join(" ")}`));
2061
+ }
2062
+ });
2063
+ });
2064
+
2065
+ // src/commands/agent/execute.ts
2066
+ import { Command as Command11 } from "commander";
2067
+ import chalk14 from "chalk";
2068
+ import pLimit2 from "p-limit";
2069
+
2070
+ // src/lib/git-worktree.ts
2071
+ import fs8 from "fs";
2072
+ import path7 from "path";
2073
+ import os3 from "os";
2074
+ import { execa as execa3 } from "execa";
2075
+ var WORKTREES_DIR = path7.join(os3.homedir(), ".bumblebee", "worktrees");
2076
+ var TYPE_BRANCH_PREFIX = {
2077
+ epic: "epic",
2078
+ story: "feat",
2079
+ task: "task",
2080
+ bug: "fix",
2081
+ feature: "feat",
2082
+ chore: "chore",
2083
+ spike: "spike"
2084
+ };
2085
+ function buildBranchName(item) {
2086
+ const prefix = TYPE_BRANCH_PREFIX[item.type] ?? "task";
2087
+ const key = (item.key ?? `item-${item.number}`).toLowerCase();
2088
+ const titleSlug = slugify(item.title);
2089
+ return `${prefix}/${key}_${titleSlug}`;
2090
+ }
2091
+ function worktreePath(slug, itemNumber, title) {
2092
+ if (title) {
2093
+ const shortSlug = slugify(title, 40);
2094
+ return path7.join(WORKTREES_DIR, slug, `item-${itemNumber}-${shortSlug}`);
2095
+ }
2096
+ return path7.join(WORKTREES_DIR, slug, `item-${itemNumber}`);
2097
+ }
2098
+ function findWorktree(slug, itemNumber) {
2099
+ const parent = path7.join(WORKTREES_DIR, slug);
2100
+ if (fs8.existsSync(parent)) {
2101
+ const entries = fs8.readdirSync(parent).sort().reverse();
2102
+ for (const name of entries) {
2103
+ const full = path7.join(parent, name);
2104
+ if (fs8.statSync(full).isDirectory() && (name === `item-${itemNumber}` || name.startsWith(`item-${itemNumber}-`))) {
2105
+ return full;
2106
+ }
2107
+ }
2108
+ }
2109
+ return path7.join(WORKTREES_DIR, slug, `item-${itemNumber}`);
2110
+ }
2111
+ async function createWorktree(projectPath, slug, item) {
2112
+ const branch = buildBranchName(item);
2113
+ const itemNumber = item.number;
2114
+ let wt = findWorktree(slug, itemNumber);
2115
+ if (!fs8.existsSync(wt)) {
2116
+ wt = worktreePath(slug, itemNumber, item.title);
2117
+ }
2118
+ fs8.mkdirSync(path7.dirname(wt), { recursive: true });
2119
+ if (fs8.existsSync(wt)) {
2120
+ const { stdout } = await execa3("git", ["worktree", "list", "--porcelain"], {
2121
+ cwd: projectPath
2122
+ });
2123
+ const wtNorm = wt.replace(/\\/g, "/");
2124
+ if (stdout.split("\n").some((ln) => ln.replace(/\\/g, "/").includes(wtNorm))) {
2125
+ return { worktreePath: wt, branchName: branch };
2126
+ }
2127
+ await execa3("git", ["worktree", "prune"], {
2128
+ cwd: projectPath,
2129
+ reject: false
2130
+ });
2131
+ if (fs8.existsSync(wt)) {
2132
+ fs8.rmSync(wt, { recursive: true, force: true });
2133
+ }
2134
+ }
2135
+ const check = await execa3(
2136
+ "git",
2137
+ ["rev-parse", "--verify", branch],
2138
+ { cwd: projectPath, reject: false }
2139
+ );
2140
+ if (check.exitCode === 0) {
2141
+ await execa3("git", ["worktree", "add", wt, branch], {
2142
+ cwd: projectPath
2143
+ });
2144
+ } else {
2145
+ await execa3("git", ["worktree", "add", "-b", branch, wt], {
2146
+ cwd: projectPath
2147
+ });
2148
+ }
2149
+ return { worktreePath: wt, branchName: branch };
2150
+ }
2151
+ async function removeWorktree(projectPath, wtPath) {
2152
+ await execa3("git", ["worktree", "remove", "--force", wtPath], {
2153
+ cwd: projectPath,
2154
+ reject: false
2155
+ });
2156
+ await execa3("git", ["worktree", "prune"], {
2157
+ cwd: projectPath,
2158
+ reject: false
2159
+ });
2160
+ }
2161
+ async function detectWorktreeBranch(projectPath, wtPath) {
2162
+ const { stdout } = await execa3(
2163
+ "git",
2164
+ ["worktree", "list", "--porcelain"],
2165
+ { cwd: projectPath }
2166
+ );
2167
+ const wtNorm = wtPath.replace(/\\/g, "/");
2168
+ const lines = stdout.split("\n");
2169
+ for (let i = 0; i < lines.length; i++) {
2170
+ if (lines[i].replace(/\\/g, "/").includes(wtNorm)) {
2171
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
2172
+ if (lines[j].startsWith("branch ")) {
2173
+ return lines[j].replace("branch refs/heads/", "");
2174
+ }
2175
+ }
2176
+ break;
2177
+ }
2178
+ }
2179
+ return null;
2180
+ }
2181
+ async function listAgentBranches(projectPath) {
2182
+ const prefixes = [...new Set(Object.values(TYPE_BRANCH_PREFIX))];
2183
+ const all = [];
2184
+ for (const prefix of prefixes) {
2185
+ const { stdout: stdout2 } = await execa3(
2186
+ "git",
2187
+ ["branch", "--list", `${prefix}/*`],
2188
+ { cwd: projectPath, reject: false }
2189
+ );
2190
+ all.push(
2191
+ ...stdout2.split("\n").map((b) => b.trim().replace(/^\*\s*/, "")).filter(Boolean)
2192
+ );
2193
+ }
2194
+ const { stdout } = await execa3(
2195
+ "git",
2196
+ ["branch", "--list", "bb/item-*"],
2197
+ { cwd: projectPath, reject: false }
2198
+ );
2199
+ all.push(
2200
+ ...stdout.split("\n").map((b) => b.trim().replace(/^\*\s*/, "")).filter(Boolean)
2201
+ );
2202
+ return [...new Set(all)].sort();
2203
+ }
2204
+ function extractItemNumberFromBranch(branch) {
2205
+ if (branch.startsWith("bb/item-")) {
2206
+ const n = parseInt(branch.replace("bb/item-", ""), 10);
2207
+ return isNaN(n) ? null : n;
2208
+ }
2209
+ const m = branch.match(/\/(?:[a-z]+-)?(\d+)/);
2210
+ return m ? parseInt(m[1], 10) : null;
2211
+ }
2212
+
2213
+ // src/commands/agent/execute.ts
2214
+ function buildMcpConfig2() {
2215
+ const apiUrl = getApiUrl();
2216
+ const token = getToken();
2217
+ return JSON.stringify({
2218
+ mcpServers: {
2219
+ bumblebee: {
2220
+ url: `${apiUrl}/mcp`,
2221
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
2222
+ }
2223
+ }
2224
+ });
2225
+ }
2226
+ async function executeOne(slug, projectPath, idOrNumber, tracker) {
2227
+ let item;
2228
+ try {
2229
+ item = await resolveItemId(slug, idOrNumber);
2230
+ } catch (e) {
2231
+ return { key: idOrNumber, status: "failed", error: `Resolve failed: ${e.message}` };
2232
+ }
2233
+ const itemId = item.id;
2234
+ const key = item.key ?? `#${item.number}`;
2235
+ const knowledge = readKnowledge(projectPath);
2236
+ const comments = await getItemComments(itemId);
2237
+ const commentsCtx = formatCommentsContext(comments);
2238
+ const prompt = buildExecutePrompt(item, knowledge, commentsCtx);
2239
+ let workDir;
2240
+ let branchName;
2241
+ try {
2242
+ const wt = await createWorktree(projectPath, slug, item);
2243
+ workDir = wt.worktreePath;
2244
+ branchName = wt.branchName;
2245
+ } catch (e) {
2246
+ return { key, status: "failed", error: `Worktree failed: ${e.message}` };
2247
+ }
2248
+ let sessionId;
2249
+ try {
2250
+ const session = await apiPost("/api/agent-sessions/start", {
2251
+ work_item_id: itemId,
2252
+ origin: "cli",
2253
+ phase: "execute"
2254
+ }, { project_slug: slug });
2255
+ sessionId = session.id;
2256
+ } catch (e) {
2257
+ return { key, status: "failed", error: `Session start failed: ${e.message}`, branch: branchName };
2258
+ }
2259
+ await updatePhase(sessionId, "execute", { branch_name: branchName, worktree_path: workDir });
2260
+ if (["open", "confirmed", "approved"].includes(item.status)) {
2261
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_progress" });
2262
+ }
2263
+ if (tracker) tracker.update(key, "execute", "running", "Starting implementation...");
2264
+ const streamer = new AgentStreamer(sessionId);
2265
+ if (tracker) {
2266
+ streamer.setCallback((_p, t) => {
2267
+ if (t) tracker.update(key, "execute", "running", t.slice(0, 60));
2268
+ });
2269
+ }
2270
+ streamer.start();
2271
+ const result = await runClaude({
2272
+ cmd: [
2273
+ "claude",
2274
+ "--output-format",
2275
+ "stream-json",
2276
+ "--verbose",
2277
+ "--permission-mode",
2278
+ "bypassPermissions",
2279
+ "--mcp-config",
2280
+ "-",
2281
+ "-p",
2282
+ prompt
2283
+ ],
2284
+ cwd: workDir,
2285
+ onLine: (line) => streamer.feed(line),
2286
+ stdinData: buildMcpConfig2()
2287
+ });
2288
+ const textBlocks = streamer.stop();
2289
+ const tail = textBlocks.slice(-3).join("\n\n") || "No text output captured.";
2290
+ let extra = "";
2291
+ if (result.timedOut) extra = "\n**Reason**: Process timed out\n";
2292
+ else if (result.stalled) extra = "\n**Reason**: Process stalled (no output)\n";
2293
+ else if (result.stderr) extra = `
2294
+ **Stderr**: ${result.stderr.slice(0, 300)}
2295
+ `;
2296
+ const body = [
2297
+ "## Agent Execution Report\n",
2298
+ `**Branch**: \`${branchName}\`
2299
+ `,
2300
+ `**Exit code**: \`${result.returncode}\`
2301
+ `,
2302
+ extra,
2303
+ `
2304
+ ### Output (last messages)
2305
+
2306
+ ${tail}`
2307
+ ].join("\n");
2308
+ await apiPost(`/api/work-items/${itemId}/comments`, {
2309
+ body,
2310
+ author: "bb-agent",
2311
+ type: "agent_output"
2312
+ });
2313
+ if (result.returncode === 0) {
2314
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_review" });
2315
+ await completeSession(sessionId, "completed");
2316
+ return { key, status: "ok", branch: branchName, worktree: workDir };
2317
+ } else {
2318
+ await completeSession(sessionId, "failed", { error: result.stderr.slice(0, 500) });
2319
+ return { key, status: "failed", error: result.stderr.slice(0, 500) || `Exit code ${result.returncode}`, branch: branchName };
2320
+ }
2321
+ }
2322
+ var executeCommand = new Command11("execute").description("Phase 2: Create a worktree and implement with Claude Code").argument("<id>", "Work item ID, number, or KEY-number").option("--no-worktree", "Work in main directory").option("--cleanup", "Remove worktree after completion").action(async (id, opts) => {
2323
+ await withErrorHandling(async () => {
2324
+ const slug = requireProject();
2325
+ const projectPath = requireProjectPath(slug);
2326
+ const item = await resolveItemId(slug, id);
2327
+ const itemId = item.id;
2328
+ const key = item.key ?? `#${item.number}`;
2329
+ const knowledge = readKnowledge(projectPath);
2330
+ const comments = await getItemComments(itemId);
2331
+ const commentsCtx = formatCommentsContext(comments);
2332
+ const prompt = buildExecutePrompt(item, knowledge, commentsCtx);
2333
+ let workDir = projectPath;
2334
+ let branchName = null;
2335
+ if (opts.worktree !== false) {
2336
+ console.log(chalk14.cyan(`Creating worktree for ${key}...`));
2337
+ const wt = await createWorktree(projectPath, slug, item);
2338
+ workDir = wt.worktreePath;
2339
+ branchName = wt.branchName;
2340
+ console.log(chalk14.green(`Worktree: ${workDir}`));
2341
+ console.log(chalk14.green(`Branch: ${branchName}`));
2342
+ }
2343
+ const session = await apiPost("/api/agent-sessions/start", {
2344
+ work_item_id: itemId,
2345
+ origin: "cli"
2346
+ }, { project_slug: slug });
2347
+ const sessionId = session.id;
2348
+ console.log(chalk14.green(`Session: ${sessionId}`));
2349
+ if (["open", "confirmed", "approved"].includes(item.status)) {
2350
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_progress" });
2351
+ console.log(chalk14.dim("Status -> in_progress"));
2352
+ }
2353
+ const streamer = new AgentStreamer(sessionId);
2354
+ streamer.setCallback((_p, text3) => {
2355
+ if (text3) process.stdout.write(text3);
2356
+ });
2357
+ streamer.start();
2358
+ console.log(chalk14.cyan(`
2359
+ Spawning Claude Code agent in ${workDir}...
2360
+ `));
2361
+ const result = await runClaude({
2362
+ cmd: [
2363
+ "claude",
2364
+ "--output-format",
2365
+ "stream-json",
2366
+ "--verbose",
2367
+ "--permission-mode",
2368
+ "bypassPermissions",
2369
+ "--mcp-config",
2370
+ "-",
2371
+ "-p",
2372
+ prompt
2373
+ ],
2374
+ cwd: workDir,
2375
+ onLine: (line) => streamer.feed(line),
2376
+ stdinData: buildMcpConfig2()
2377
+ });
2378
+ const textBlocks = streamer.stop();
2379
+ const tail = textBlocks.slice(-3).join("\n\n") || "No text output captured.";
2380
+ const bodyLines = ["## Agent Execution Report\n"];
2381
+ if (branchName) bodyLines.push(`**Branch**: \`${branchName}\`
2382
+ `);
2383
+ bodyLines.push(`**Exit code**: \`${result.returncode}\`
2384
+ `);
2385
+ if (result.timedOut) bodyLines.push("**Reason**: Process timed out\n");
2386
+ else if (result.stalled) bodyLines.push("**Reason**: Process stalled (no output)\n");
2387
+ else if (result.stderr) bodyLines.push(`**Stderr**: ${result.stderr.slice(0, 300)}
2388
+ `);
2389
+ bodyLines.push(`
2390
+ ### Output (last messages)
2391
+
2392
+ ${tail}`);
2393
+ await apiPost(`/api/work-items/${itemId}/comments`, {
2394
+ body: bodyLines.join("\n"),
2395
+ author: "bb-agent",
2396
+ type: "agent_output"
2397
+ });
2398
+ if (result.returncode === 0) {
2399
+ console.log(chalk14.green("\nAgent completed successfully."));
2400
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_review" });
2401
+ console.log(chalk14.dim("Status -> in_review"));
2402
+ } else {
2403
+ if (result.timedOut) console.log(chalk14.yellow(`
2404
+ Agent timed out.`));
2405
+ else if (result.stalled) console.log(chalk14.yellow(`
2406
+ Agent stalled.`));
2407
+ else console.log(chalk14.yellow(`
2408
+ Agent exited with code ${result.returncode}.`));
2409
+ }
2410
+ if (branchName && workDir !== projectPath) {
2411
+ if (opts.cleanup) {
2412
+ await removeWorktree(projectPath, workDir);
2413
+ console.log(chalk14.dim("Worktree removed."));
2414
+ } else {
2415
+ console.log(chalk14.dim(`
2416
+ Worktree: ${workDir}`));
2417
+ console.log(chalk14.dim(`Cleanup: bb agent cleanup ${item.number}`));
2418
+ }
2419
+ }
2420
+ });
2421
+ });
2422
+ var batchExecuteCommand = new Command11("batch-execute").description("Implement multiple work items in parallel").argument("<items...>", "Work item IDs/numbers").option("-P, --parallel <n>", "Max parallel agents", "2").action(async (items, opts) => {
2423
+ await withErrorHandling(async () => {
2424
+ const slug = requireProject();
2425
+ const projectPath = requireProjectPath(slug);
2426
+ const maxParallel = parseInt(opts.parallel, 10);
2427
+ console.log(chalk14.cyan(`Executing ${items.length} items (max ${maxParallel} parallel)...
2428
+ `));
2429
+ const tracker = new AgentProgressTracker();
2430
+ for (const ref of items) tracker.register(ref);
2431
+ tracker.start();
2432
+ const limit = pLimit2(maxParallel);
2433
+ const results = await Promise.all(
2434
+ items.map(
2435
+ (ref) => limit(async () => {
2436
+ const r = await executeOne(slug, projectPath, ref, tracker);
2437
+ tracker.complete(ref, r.status === "ok", (r.branch ?? r.error ?? "Done").slice(0, 60));
2438
+ return r;
2439
+ })
2440
+ )
2441
+ );
2442
+ tracker.stop();
2443
+ const ok = results.filter((r) => r.status === "ok").length;
2444
+ console.log(chalk14.bold(`
2445
+ Done: ${ok}/${items.length} succeeded.`));
2446
+ const branches = results.filter((r) => r.branch);
2447
+ if (branches.length) {
2448
+ console.log(chalk14.dim(`
2449
+ Merge all with: bb agent merge --target release/dev`));
2450
+ }
2451
+ });
2452
+ });
2453
+
2454
+ // src/commands/agent/test.ts
2455
+ import { Command as Command12 } from "commander";
2456
+ import chalk15 from "chalk";
2457
+ import fs9 from "fs";
2458
+ async function testOne(slug, projectPath, idOrNumber, tracker) {
2459
+ let item;
2460
+ try {
2461
+ item = await resolveItemId(slug, idOrNumber);
2462
+ } catch (e) {
2463
+ return { key: idOrNumber, status: "failed", error: `Resolve failed: ${e.message}` };
2464
+ }
2465
+ const itemId = item.id;
2466
+ const itemNumber = item.number;
2467
+ const key = item.key ?? `#${itemNumber}`;
2468
+ const wt = findWorktree(slug, itemNumber);
2469
+ if (!fs9.existsSync(wt)) {
2470
+ return { key, status: "failed", error: "No worktree found (execute first)" };
2471
+ }
2472
+ const workDir = wt;
2473
+ const knowledge = readKnowledge(projectPath);
2474
+ const comments = await getItemComments(itemId);
2475
+ const commentsCtx = formatCommentsContext(comments);
2476
+ const prompt = buildTestPrompt(item, knowledge, commentsCtx);
2477
+ if (tracker) {
2478
+ tracker.update(key, "test", "running", "Starting tests...");
2479
+ }
2480
+ let sessionId = null;
2481
+ try {
2482
+ const session = await apiPost("/api/agent-sessions/start", {
2483
+ work_item_id: itemId,
2484
+ origin: "cli",
2485
+ phase: "test"
2486
+ }, { project_slug: slug });
2487
+ sessionId = session.id;
2488
+ } catch {
2489
+ }
2490
+ let streamer = null;
2491
+ if (sessionId) {
2492
+ streamer = new AgentStreamer(sessionId);
2493
+ if (tracker) {
2494
+ streamer.setCallback((_p, t) => {
2495
+ if (t) tracker.update(key, "test", "running", t.slice(0, 60));
2496
+ });
2497
+ }
2498
+ streamer.start();
2499
+ await updatePhase(sessionId, "test");
2500
+ }
2501
+ const result = await runClaude({
2502
+ cmd: ["claude", "-p", prompt, "--output-format", "stream-json"],
2503
+ cwd: workDir,
2504
+ onLine: (line) => streamer?.feed(line)
2505
+ });
2506
+ const textBlocks = streamer?.stop() ?? [];
2507
+ if (result.returncode !== 0) {
2508
+ if (sessionId) await completeSession(sessionId, "failed", { error: result.stderr.slice(0, 500) });
2509
+ return { key, status: "failed", error: result.stderr.slice(0, 500) || `Exit code ${result.returncode}` };
2510
+ }
2511
+ const report = textBlocks.join("\n\n");
2512
+ if (!report) {
2513
+ if (sessionId) await completeSession(sessionId, "failed", { error: "Empty test report" });
2514
+ return { key, status: "failed", error: "Empty test report" };
2515
+ }
2516
+ const reportLower = report.toLowerCase();
2517
+ const testsPassed = reportLower.includes("**status**: pass") || reportLower.includes("status: pass") || reportLower.includes("all tests pass") && !reportLower.split("all tests pass")[0].slice(-50).includes("fail");
2518
+ await apiPost(`/api/work-items/${itemId}/comments`, {
2519
+ body: report,
2520
+ author: "bb-agent",
2521
+ type: "test_report"
2522
+ });
2523
+ if (testsPassed) {
2524
+ if (sessionId) await completeSession(sessionId, "completed");
2525
+ return { key, status: "ok", report: report.slice(0, 200) };
2526
+ } else {
2527
+ await apiPut(`/api/work-items/${itemId}`, { status: "failed" });
2528
+ if (sessionId) await completeSession(sessionId, "failed", { error: "Tests failed" });
2529
+ return { key, status: "failed", error: "Tests failed", report: report.slice(0, 200) };
2530
+ }
2531
+ }
2532
+ var testCommand = new Command12("test").description("Phase 3: Run tests in the worktree and report results").argument("<id>", "Work item ID, number, or KEY-number to test").action(async (id) => {
2533
+ await withErrorHandling(async () => {
2534
+ const slug = requireProject();
2535
+ const projectPath = requireProjectPath(slug);
2536
+ console.log(chalk15.cyan(`Fetching work item ${id}...`));
2537
+ const item = await resolveItemId(slug, id);
2538
+ const itemId = item.id;
2539
+ const itemNumber = item.number;
2540
+ const key = item.key ?? `#${itemNumber}`;
2541
+ const wt = findWorktree(slug, itemNumber);
2542
+ if (!fs9.existsSync(wt)) {
2543
+ console.error(chalk15.red(`No worktree found for ${key}. Run 'bb agent execute ${id}' first.`));
2544
+ process.exit(1);
2545
+ }
2546
+ const workDir = wt;
2547
+ const knowledge = readKnowledge(projectPath);
2548
+ const comments = await getItemComments(itemId);
2549
+ const commentsCtx = formatCommentsContext(comments);
2550
+ const prompt = buildTestPrompt(item, knowledge, commentsCtx);
2551
+ console.log(chalk15.cyan(`Running tests in ${workDir}...`));
2552
+ const result = await runClaude({
2553
+ cmd: ["claude", "-p", prompt, "--output-format", "stream-json"],
2554
+ cwd: workDir,
2555
+ onLine: (line) => {
2556
+ try {
2557
+ const payload = JSON.parse(line);
2558
+ if (payload.type === "assistant") {
2559
+ for (const block of payload.content ?? []) {
2560
+ if (block.type === "text") process.stdout.write(block.text);
2561
+ }
2562
+ }
2563
+ } catch {
2564
+ }
2565
+ }
2566
+ });
2567
+ if (result.returncode !== 0) {
2568
+ console.error(chalk15.red(`Claude test runner failed:
2569
+ ${result.stderr}`));
2570
+ process.exit(1);
2571
+ }
2572
+ const report = result.textBlocks.join("\n\n");
2573
+ if (!report) {
2574
+ console.error(chalk15.red("Empty test report."));
2575
+ process.exit(1);
2576
+ }
2577
+ await apiPost(`/api/work-items/${itemId}/comments`, {
2578
+ body: report,
2579
+ author: "bb-agent",
2580
+ type: "test_report"
2581
+ });
2582
+ console.log(chalk15.green("Test report posted as comment."));
2583
+ const reportLower = report.toLowerCase();
2584
+ const testsPassed = reportLower.includes("**status**: pass") || reportLower.includes("status: pass") || reportLower.includes("all tests pass") && !reportLower.split("all tests pass")[0].slice(-50).includes("fail");
2585
+ if (testsPassed) {
2586
+ console.log(chalk15.green("All tests passed!"));
2587
+ } else {
2588
+ await apiPut(`/api/work-items/${itemId}`, { status: "failed" });
2589
+ console.log(chalk15.red("Tests failed. Status -> failed"));
2590
+ console.log(chalk15.dim(`Fix with: bb agent reimplement ${id}`));
2591
+ process.exit(1);
2592
+ }
2593
+ });
2594
+ });
2595
+
2596
+ // src/commands/agent/verify.ts
2597
+ import { Command as Command13 } from "commander";
2598
+ import chalk16 from "chalk";
2599
+ var verifyCommand = new Command13("verify").description("Phase 0: Verify requirements -- analyse feasibility and return READY or NEEDS_INFO verdict").argument("<id>", "Work item ID, number, or KEY-number to verify").action(async (id) => {
2600
+ await withErrorHandling(async () => {
2601
+ const slug = requireProject();
2602
+ const projectPath = requireProjectPath(slug);
2603
+ console.log(chalk16.cyan(`Fetching work item ${id}...`));
2604
+ const item = await resolveItemId(slug, id);
2605
+ const itemId = item.id;
2606
+ const key = item.key ?? `#${item.number}`;
2607
+ const knowledge = readKnowledge(projectPath);
2608
+ const comments = await getItemComments(itemId);
2609
+ const commentsCtx = formatCommentsContext(comments);
2610
+ const prompt = buildVerifyPrompt(item, knowledge, commentsCtx);
2611
+ console.log(chalk16.cyan(`Running requirement analysis for ${key}...`));
2612
+ let sessionId = null;
2613
+ try {
2614
+ const session = await apiPost("/api/agent-sessions/start", {
2615
+ work_item_id: itemId,
2616
+ origin: "cli",
2617
+ phase: "verify"
2618
+ }, { project_slug: slug });
2619
+ sessionId = session.id;
2620
+ } catch {
2621
+ }
2622
+ let streamer = null;
2623
+ if (sessionId) {
2624
+ streamer = new AgentStreamer(sessionId);
2625
+ streamer.start();
2626
+ await updatePhase(sessionId, "verify");
2627
+ }
2628
+ const result = await runClaude({
2629
+ cmd: ["claude", "-p", prompt, "--output-format", "stream-json"],
2630
+ cwd: projectPath,
2631
+ onLine: (line) => {
2632
+ streamer?.feed(line);
2633
+ try {
2634
+ const payload = JSON.parse(line);
2635
+ if (payload.type === "assistant") {
2636
+ for (const block of payload.content ?? []) {
2637
+ if (block.type === "text") process.stdout.write(block.text);
2638
+ }
2639
+ }
2640
+ } catch {
2641
+ }
2642
+ }
2643
+ });
2644
+ const textBlocks = streamer?.stop() ?? result.textBlocks;
2645
+ if (result.returncode !== 0) {
2646
+ console.error(chalk16.red(`Claude analysis failed:
2647
+ ${result.stderr}`));
2648
+ if (sessionId) await completeSession(sessionId, "failed", { error: result.stderr.slice(0, 500) });
2649
+ process.exit(1);
2650
+ }
2651
+ const analysis = textBlocks.join("\n\n");
2652
+ if (!analysis) {
2653
+ console.error(chalk16.red("Claude returned an empty response."));
2654
+ if (sessionId) await completeSession(sessionId, "failed", { error: "Empty response" });
2655
+ process.exit(1);
2656
+ }
2657
+ const analysisUpper = analysis.toUpperCase();
2658
+ let verdict;
2659
+ if (analysisUpper.includes("VERDICT: READY")) {
2660
+ verdict = "ready";
2661
+ } else if (analysisUpper.includes("VERDICT: NEEDS_INFO")) {
2662
+ verdict = "needs_info";
2663
+ } else {
2664
+ console.log(chalk16.yellow("No explicit verdict found in analysis. Defaulting to READY."));
2665
+ verdict = "ready";
2666
+ }
2667
+ if (verdict === "ready") {
2668
+ await apiPost(`/api/work-items/${itemId}/comments`, {
2669
+ body: analysis,
2670
+ author: "bb-agent",
2671
+ type: "proposal"
2672
+ });
2673
+ console.log(chalk16.green("Analysis posted as proposal comment."));
2674
+ await apiPut(`/api/work-items/${itemId}`, { plan: analysis });
2675
+ console.log(chalk16.green("Plan saved to work item."));
2676
+ if (item.status === "open") {
2677
+ await apiPut(`/api/work-items/${itemId}`, { status: "confirmed" });
2678
+ console.log(chalk16.dim("Status -> confirmed"));
2679
+ }
2680
+ if (sessionId) await completeSession(sessionId, "completed");
2681
+ console.log(chalk16.green(`
2682
+ VERDICT: READY -- ${key} is ready for implementation.`));
2683
+ } else {
2684
+ await apiPost(`/api/work-items/${itemId}/comments`, {
2685
+ body: analysis,
2686
+ author: "bb-agent",
2687
+ type: "analysis"
2688
+ });
2689
+ console.log(chalk16.yellow("Analysis posted as comment."));
2690
+ await apiPut(`/api/work-items/${itemId}`, { status: "needs_info" });
2691
+ console.log(chalk16.dim("Status -> needs_info"));
2692
+ if (sessionId) await completeSession(sessionId, "failed", { error: "NEEDS_INFO" });
2693
+ console.log(chalk16.yellow(`
2694
+ VERDICT: NEEDS_INFO -- ${key} requires clarification before implementation.`));
2695
+ process.exit(1);
2696
+ }
2697
+ });
2698
+ });
2699
+
2700
+ // src/commands/agent/reimplement.ts
2701
+ import { Command as Command14 } from "commander";
2702
+ import chalk17 from "chalk";
2703
+ import fs11 from "fs";
2704
+
2705
+ // src/lib/docker-runner.ts
2706
+ import fs10 from "fs";
2707
+ import path8 from "path";
2708
+ import { execa as execa4 } from "execa";
2709
+ var COMPOSE_FILES = [
2710
+ "docker-compose.test.yml",
2711
+ "Dockerfile.api-test",
2712
+ "Dockerfile.web-test"
2713
+ ];
2714
+ function ensureComposeFiles(workDir, projectPath) {
2715
+ for (const name of COMPOSE_FILES) {
2716
+ const src = path8.join(projectPath, name);
2717
+ const dst = path8.join(workDir, name);
2718
+ if (fs10.existsSync(src) && !fs10.existsSync(dst)) {
2719
+ fs10.copyFileSync(src, dst);
2720
+ }
2721
+ }
2722
+ }
2723
+ function truncateOutput2(output, maxChars = 4e3) {
2724
+ if (output.length <= maxChars) return output;
2725
+ return `... (truncated, showing last ${maxChars} chars)
2726
+ ` + output.slice(-maxChars);
2727
+ }
2728
+ async function runDockerTests(workDir, timeout = 6e5) {
2729
+ const composeCmd = ["docker", "compose", "-f", "docker-compose.test.yml"];
2730
+ try {
2731
+ const result = await execa4(
2732
+ composeCmd[0],
2733
+ [...composeCmd.slice(1), "up", "--build", "--abort-on-container-exit"],
2734
+ {
2735
+ cwd: workDir,
2736
+ timeout,
2737
+ reject: false
2738
+ }
2739
+ );
2740
+ const raw = (result.stdout ?? "") + "\n" + (result.stderr ?? "");
2741
+ const exitCode = result.exitCode ?? 1;
2742
+ const ps = await execa4(
2743
+ composeCmd[0],
2744
+ [
2745
+ ...composeCmd.slice(1),
2746
+ "ps",
2747
+ "-a",
2748
+ "--format",
2749
+ "{{.Service}} {{.ExitCode}}"
2750
+ ],
2751
+ {
2752
+ cwd: workDir,
2753
+ reject: false
2754
+ }
2755
+ );
2756
+ const serviceResults = {};
2757
+ for (const line of (ps.stdout ?? "").split("\n")) {
2758
+ const parts = line.split(" ");
2759
+ if (parts.length === 2) {
2760
+ const [svc, code] = parts;
2761
+ serviceResults[svc] = parseInt(code, 10);
2762
+ if (isNaN(serviceResults[svc])) {
2763
+ serviceResults[svc] = -1;
2764
+ }
2765
+ }
2766
+ }
2767
+ const lines = [];
2768
+ let allPass = true;
2769
+ for (const svc of ["api-test", "web-build"]) {
2770
+ const code = serviceResults[svc];
2771
+ if (code === void 0) {
2772
+ lines.push(`- **${svc}**: unknown (container not found)`);
2773
+ allPass = false;
2774
+ } else if (code === 0) {
2775
+ lines.push(`- **${svc}**: passed`);
2776
+ } else {
2777
+ lines.push(`- **${svc}**: FAILED (exit code ${code})`);
2778
+ allPass = false;
2779
+ }
2780
+ }
2781
+ return {
2782
+ success: exitCode === 0 && allPass,
2783
+ rawOutput: raw,
2784
+ details: lines.join("\n")
2785
+ };
2786
+ } catch (err) {
2787
+ if (err.timedOut) {
2788
+ return {
2789
+ success: false,
2790
+ rawOutput: "",
2791
+ details: `- Docker tests timed out after ${timeout / 1e3}s`
2792
+ };
2793
+ }
2794
+ return {
2795
+ success: false,
2796
+ rawOutput: "",
2797
+ details: "- `docker` command not found. Is Docker installed?"
2798
+ };
2799
+ } finally {
2800
+ await execa4(
2801
+ composeCmd[0],
2802
+ [...composeCmd.slice(1), "down", "-v", "--remove-orphans"],
2803
+ {
2804
+ cwd: workDir,
2805
+ reject: false,
2806
+ timeout: 6e4
2807
+ }
2808
+ ).catch(() => {
2809
+ });
2810
+ }
2811
+ }
2812
+ async function doSingleMerge(projectPath, branch, target) {
2813
+ const { stdout: current } = await execa4(
2814
+ "git",
2815
+ ["rev-parse", "--abbrev-ref", "HEAD"],
2816
+ { cwd: projectPath }
2817
+ );
2818
+ const check = await execa4(
2819
+ "git",
2820
+ ["rev-parse", "--verify", target],
2821
+ { cwd: projectPath, reject: false }
2822
+ );
2823
+ if (check.exitCode !== 0) {
2824
+ await execa4("git", ["branch", target], {
2825
+ cwd: projectPath,
2826
+ reject: false
2827
+ });
2828
+ }
2829
+ await execa4("git", ["worktree", "prune"], {
2830
+ cwd: projectPath,
2831
+ reject: false
2832
+ });
2833
+ const co = await execa4("git", ["checkout", target], {
2834
+ cwd: projectPath,
2835
+ reject: false
2836
+ });
2837
+ if (co.exitCode !== 0) return false;
2838
+ const merge = await execa4(
2839
+ "git",
2840
+ [
2841
+ "merge",
2842
+ branch,
2843
+ "--no-ff",
2844
+ "-m",
2845
+ `Merge ${branch} into ${target}`
2846
+ ],
2847
+ {
2848
+ cwd: projectPath,
2849
+ reject: false
2850
+ }
2851
+ );
2852
+ const ok = merge.exitCode === 0;
2853
+ if (!ok) {
2854
+ await execa4("git", ["merge", "--abort"], {
2855
+ cwd: projectPath,
2856
+ reject: false
2857
+ });
2858
+ }
2859
+ await execa4("git", ["checkout", current.trim()], {
2860
+ cwd: projectPath,
2861
+ reject: false
2862
+ });
2863
+ return ok;
2864
+ }
2865
+
2866
+ // src/commands/agent/reimplement.ts
2867
+ function buildMcpConfig3() {
2868
+ const apiUrl = getApiUrl();
2869
+ const token = getToken();
2870
+ return JSON.stringify({
2871
+ mcpServers: {
2872
+ bumblebee: {
2873
+ url: `${apiUrl}/mcp`,
2874
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
2875
+ }
2876
+ }
2877
+ });
2878
+ }
2879
+ async function reimplementOne(slug, projectPath, idOrNumber, dockerOutput = "", tracker) {
2880
+ let item;
2881
+ try {
2882
+ item = await resolveItemId(slug, idOrNumber);
2883
+ } catch (e) {
2884
+ return { key: idOrNumber, status: "failed", error: `Resolve failed: ${e.message}` };
2885
+ }
2886
+ const itemId = item.id;
2887
+ const itemNumber = item.number;
2888
+ const key = item.key ?? `#${itemNumber}`;
2889
+ const wt = findWorktree(slug, itemNumber);
2890
+ if (!fs11.existsSync(wt)) {
2891
+ return { key, status: "failed", error: "No worktree found (execute first)" };
2892
+ }
2893
+ const workDir = wt;
2894
+ const branchName = await detectWorktreeBranch(projectPath, wt);
2895
+ const knowledge = readKnowledge(projectPath);
2896
+ const comments = await getItemComments(itemId);
2897
+ const commentsCtx = formatCommentsContext(comments);
2898
+ const prompt = buildReimplementPrompt(item, knowledge, commentsCtx, dockerOutput);
2899
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_progress" });
2900
+ if (tracker) {
2901
+ tracker.update(key, "reimplement", "running", "Re-implementing...");
2902
+ }
2903
+ let sessionId = null;
2904
+ try {
2905
+ const session = await apiPost("/api/agent-sessions/start", {
2906
+ work_item_id: itemId,
2907
+ origin: "cli",
2908
+ phase: "reimplement"
2909
+ }, { project_slug: slug });
2910
+ sessionId = session.id;
2911
+ } catch {
2912
+ }
2913
+ let streamer = null;
2914
+ if (sessionId) {
2915
+ streamer = new AgentStreamer(sessionId);
2916
+ if (tracker) {
2917
+ streamer.setCallback((_p, t) => {
2918
+ if (t) tracker.update(key, "reimplement", "running", t.slice(0, 60));
2919
+ });
2920
+ }
2921
+ streamer.start();
2922
+ await updatePhase(sessionId, "reimplement", {
2923
+ branch_name: branchName ?? "",
2924
+ worktree_path: workDir
2925
+ });
2926
+ }
2927
+ const result = await runClaude({
2928
+ cmd: [
2929
+ "claude",
2930
+ "--output-format",
2931
+ "stream-json",
2932
+ "--verbose",
2933
+ "--permission-mode",
2934
+ "bypassPermissions",
2935
+ "--mcp-config",
2936
+ "-",
2937
+ "-p",
2938
+ prompt
2939
+ ],
2940
+ cwd: workDir,
2941
+ onLine: (line) => streamer?.feed(line),
2942
+ stdinData: buildMcpConfig3()
2943
+ });
2944
+ const textBlocks = streamer?.stop() ?? [];
2945
+ const tail = textBlocks.slice(-3).join("\n\n") || "No text output captured.";
2946
+ let extra = "";
2947
+ if (result.timedOut) extra = "\n**Reason**: Process timed out\n";
2948
+ else if (result.stalled) extra = "\n**Reason**: Process stalled (no output)\n";
2949
+ else if (result.stderr) extra = `
2950
+ **Stderr**: ${result.stderr.slice(0, 300)}
2951
+ `;
2952
+ const body = [
2953
+ "## Re-implementation Report\n",
2954
+ `**Branch**: \`${branchName ?? "unknown"}\`
2955
+ `,
2956
+ `**Exit code**: \`${result.returncode}\`
2957
+ `,
2958
+ extra,
2959
+ `
2960
+ ### Output (last messages)
2961
+
2962
+ ${tail}`
2963
+ ].join("\n");
2964
+ await apiPost(`/api/work-items/${itemId}/comments`, {
2965
+ body,
2966
+ author: "bb-agent",
2967
+ type: "agent_output"
2968
+ });
2969
+ if (result.returncode === 0) {
2970
+ if (sessionId) await completeSession(sessionId, "completed");
2971
+ return { key, status: "ok", branch: branchName, worktree: workDir };
2972
+ } else {
2973
+ if (sessionId) await completeSession(sessionId, "failed", { error: result.stderr.slice(0, 500) });
2974
+ return { key, status: "failed", error: result.stderr.slice(0, 500) || `Exit code ${result.returncode}`, branch: branchName };
2975
+ }
2976
+ }
2977
+ var reimplementCommand = new Command14("reimplement").description("Re-implement a failed work item using previous feedback").argument("<id>", "Work item ID, number, or KEY-number to re-implement").option("--test", "Run tests after re-implementation (default)", true).option("--no-test", "Skip tests after re-implementation").option("--auto-merge", "Auto-merge to target on test pass").option("-t, --target <branch>", "Target branch for auto-merge", "release/dev").action(async (id, opts) => {
2978
+ await withErrorHandling(async () => {
2979
+ const slug = requireProject();
2980
+ const projectPath = requireProjectPath(slug);
2981
+ console.log(chalk17.cyan(`Fetching work item ${id}...`));
2982
+ const item = await resolveItemId(slug, id);
2983
+ const itemId = item.id;
2984
+ const itemNumber = item.number;
2985
+ const key = item.key ?? `#${itemNumber}`;
2986
+ const wt = findWorktree(slug, itemNumber);
2987
+ if (!fs11.existsSync(wt)) {
2988
+ console.error(chalk17.red(`No worktree found for ${key}. Run 'bb agent execute ${id}' first.`));
2989
+ process.exit(1);
2990
+ }
2991
+ const workDir = wt;
2992
+ const branchName = await detectWorktreeBranch(projectPath, wt);
2993
+ const knowledge = readKnowledge(projectPath);
2994
+ const comments = await getItemComments(itemId);
2995
+ const commentsCtx = formatCommentsContext(comments);
2996
+ const prompt = buildReimplementPrompt(item, knowledge, commentsCtx);
2997
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_progress" });
2998
+ console.log(chalk17.dim("Status -> in_progress"));
2999
+ const session = await apiPost("/api/agent-sessions/start", {
3000
+ work_item_id: itemId,
3001
+ origin: "cli"
3002
+ }, { project_slug: slug });
3003
+ const sessionId = session.id;
3004
+ console.log(chalk17.green(`Session: ${sessionId}`));
3005
+ console.log(chalk17.cyan(`
3006
+ Re-implementing in ${workDir}...
3007
+ `));
3008
+ const streamer = new AgentStreamer(sessionId);
3009
+ streamer.setCallback((_p, text3) => {
3010
+ if (text3) process.stdout.write(text3);
3011
+ });
3012
+ streamer.start();
3013
+ const result = await runClaude({
3014
+ cmd: [
3015
+ "claude",
3016
+ "--output-format",
3017
+ "stream-json",
3018
+ "--verbose",
3019
+ "--permission-mode",
3020
+ "bypassPermissions",
3021
+ "--mcp-config",
3022
+ "-",
3023
+ "-p",
3024
+ prompt
3025
+ ],
3026
+ cwd: workDir,
3027
+ onLine: (line) => streamer.feed(line),
3028
+ stdinData: buildMcpConfig3()
3029
+ });
3030
+ const textBlocks = streamer.stop();
3031
+ const tail = textBlocks.slice(-3).join("\n\n") || "No text output captured.";
3032
+ const bodyLines = ["## Re-implementation Report\n"];
3033
+ if (branchName) bodyLines.push(`**Branch**: \`${branchName}\`
3034
+ `);
3035
+ bodyLines.push(`**Exit code**: \`${result.returncode}\`
3036
+ `);
3037
+ if (result.timedOut) bodyLines.push("**Reason**: Process timed out\n");
3038
+ else if (result.stalled) bodyLines.push("**Reason**: Process stalled (no output)\n");
3039
+ else if (result.stderr) bodyLines.push(`**Stderr**: ${result.stderr.slice(0, 300)}
3040
+ `);
3041
+ bodyLines.push(`
3042
+ ### Output (last messages)
3043
+
3044
+ ${tail}`);
3045
+ await apiPost(`/api/work-items/${itemId}/comments`, {
3046
+ body: bodyLines.join("\n"),
3047
+ author: "bb-agent",
3048
+ type: "agent_output"
3049
+ });
3050
+ if (result.returncode !== 0) {
3051
+ if (result.timedOut) console.log(chalk17.yellow(`
3052
+ Agent timed out.`));
3053
+ else if (result.stalled) console.log(chalk17.yellow(`
3054
+ Agent stalled.`));
3055
+ else console.log(chalk17.yellow(`
3056
+ Agent exited with code ${result.returncode}.`));
3057
+ if (result.stderr) console.log(chalk17.dim(result.stderr.slice(0, 300)));
3058
+ return;
3059
+ }
3060
+ console.log(chalk17.green("\nRe-implementation completed."));
3061
+ if (opts.test !== false) {
3062
+ console.log(chalk17.cyan("\nRunning tests..."));
3063
+ const testResult = await testOne(slug, projectPath, id);
3064
+ if (testResult.status === "ok") {
3065
+ console.log(chalk17.green("All tests passed!"));
3066
+ if (opts.autoMerge && branchName) {
3067
+ console.log(chalk17.cyan(`
3068
+ Merging ${branchName} into ${opts.target}...`));
3069
+ const mergeOk = await doSingleMerge(projectPath, branchName, opts.target);
3070
+ if (mergeOk) {
3071
+ console.log(chalk17.green(`Merged into ${opts.target}.`));
3072
+ await apiPut(`/api/work-items/${itemId}`, { status: "resolved" });
3073
+ console.log(chalk17.dim("Status -> resolved"));
3074
+ } else {
3075
+ console.log(chalk17.red("Merge failed. Resolve conflicts manually."));
3076
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_review" });
3077
+ }
3078
+ } else {
3079
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_review" });
3080
+ console.log(chalk17.dim("Status -> in_review"));
3081
+ }
3082
+ } else {
3083
+ console.log(chalk17.red("Tests still failing after re-implementation."));
3084
+ console.log(chalk17.dim(`Try again: bb agent reimplement ${id}`));
3085
+ }
3086
+ } else {
3087
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_review" });
3088
+ console.log(chalk17.dim("Status -> in_review"));
3089
+ }
3090
+ });
3091
+ });
3092
+
3093
+ // src/commands/agent/run.ts
3094
+ import { Command as Command15 } from "commander";
3095
+ import chalk18 from "chalk";
3096
+ import fs12 from "fs";
3097
+ import readline from "readline";
3098
+ import pLimit3 from "p-limit";
3099
+ async function confirm(question) {
3100
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3101
+ return new Promise((resolve) => {
3102
+ rl.question(question + " (y/N) ", (answer) => {
3103
+ rl.close();
3104
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
3105
+ });
3106
+ });
3107
+ }
3108
+ async function dockerTestOne(slug, projectPath, idOrNumber, timeout = 600) {
3109
+ let item;
3110
+ try {
3111
+ item = await resolveItemId(slug, idOrNumber);
3112
+ } catch (e) {
3113
+ return { key: idOrNumber, status: "failed", output: "", details: `Resolve failed: ${e.message}` };
3114
+ }
3115
+ const itemId = item.id;
3116
+ const itemNumber = item.number;
3117
+ const key = item.key ?? `#${itemNumber}`;
3118
+ const wt = findWorktree(slug, itemNumber);
3119
+ if (!fs12.existsSync(wt)) {
3120
+ return { key, status: "failed", output: "", details: "No worktree found (execute first)" };
3121
+ }
3122
+ const workDir = wt;
3123
+ ensureComposeFiles(workDir, projectPath);
3124
+ const result = await runDockerTests(workDir, timeout * 1e3);
3125
+ let body;
3126
+ let commentType;
3127
+ if (result.success) {
3128
+ body = `## Docker Test Report
3129
+
3130
+ **Status**: PASS
3131
+
3132
+ ${result.details}`;
3133
+ commentType = "test_report";
3134
+ } else {
3135
+ const truncated = truncateOutput2(result.rawOutput, 4e3);
3136
+ body = `## Docker Test Report
3137
+
3138
+ **Status**: FAIL
3139
+
3140
+ ${result.details}
3141
+
3142
+ ### Docker Output
3143
+
3144
+ \`\`\`
3145
+ ${truncated}
3146
+ \`\`\``;
3147
+ commentType = "test_failure";
3148
+ }
3149
+ await apiPost(`/api/work-items/${itemId}/comments`, {
3150
+ body,
3151
+ author: "bb-agent",
3152
+ type: commentType
3153
+ });
3154
+ const status = result.success ? "ok" : "failed";
3155
+ return { key, status, output: result.rawOutput, details: result.details };
3156
+ }
3157
+ var runCommand = new Command15("run").description("Full autonomous loop: verify -> execute -> Docker test -> reimplement retry -> merge").argument("<id>", "Work item ID, number, or KEY-number").option("--skip-verify", "Skip the requirement verification phase").option("-y, --yes", "Auto-confirm after verification").option("--auto-merge", "Auto-merge to target branch on test pass (default)", true).option("--no-auto-merge", "Disable auto-merge").option("-t, --target <branch>", "Target branch for auto-merge", "release/dev").option("--max-retries <n>", "Max re-implementation attempts on test failure", "3").option("--timeout <seconds>", "Docker test timeout in seconds", "600").option("--auto-split", "Auto-split multi-package items (default)", true).option("--no-auto-split", "Disable auto-split").action(async (id, opts) => {
3158
+ await withErrorHandling(async () => {
3159
+ const slug = requireProject();
3160
+ const projectPath = requireProjectPath(slug);
3161
+ const maxRetries = parseInt(opts.maxRetries, 10);
3162
+ const timeout = parseInt(opts.timeout, 10);
3163
+ if (!opts.skipVerify) {
3164
+ console.log(chalk18.bold.cyan("Phase 1: Verifying requirements...\n"));
3165
+ const item = await resolveItemId(slug, id);
3166
+ const itemId = item.id;
3167
+ const key = item.key ?? `#${item.number}`;
3168
+ const knowledge = readKnowledge(projectPath);
3169
+ const comments = await getItemComments(itemId);
3170
+ const commentsCtx = formatCommentsContext(comments);
3171
+ const prompt = buildVerifyPrompt(item, knowledge, commentsCtx);
3172
+ let sessionId = null;
3173
+ try {
3174
+ const session = await apiPost("/api/agent-sessions/start", {
3175
+ work_item_id: itemId,
3176
+ origin: "cli",
3177
+ phase: "verify"
3178
+ }, { project_slug: slug });
3179
+ sessionId = session.id;
3180
+ } catch {
3181
+ }
3182
+ let streamer = null;
3183
+ if (sessionId) {
3184
+ streamer = new AgentStreamer(sessionId);
3185
+ streamer.start();
3186
+ await updatePhase(sessionId, "verify");
3187
+ }
3188
+ const verifyResult = await runClaude({
3189
+ cmd: ["claude", "-p", prompt, "--output-format", "stream-json"],
3190
+ cwd: projectPath,
3191
+ onLine: (line) => streamer?.feed(line)
3192
+ });
3193
+ const textBlocks = streamer?.stop() ?? verifyResult.textBlocks;
3194
+ if (verifyResult.returncode !== 0) {
3195
+ console.error(chalk18.red(`Claude analysis failed:
3196
+ ${verifyResult.stderr}`));
3197
+ if (sessionId) await completeSession(sessionId, "failed", { error: verifyResult.stderr.slice(0, 500) });
3198
+ console.log(chalk18.red("\nVerification failed -- aborting run."));
3199
+ process.exit(1);
3200
+ }
3201
+ const analysis = textBlocks.join("\n\n");
3202
+ if (!analysis) {
3203
+ console.error(chalk18.red("Claude returned an empty response."));
3204
+ if (sessionId) await completeSession(sessionId, "failed", { error: "Empty response" });
3205
+ process.exit(1);
3206
+ }
3207
+ const analysisUpper = analysis.toUpperCase();
3208
+ let verdict;
3209
+ if (analysisUpper.includes("VERDICT: READY")) {
3210
+ verdict = "ready";
3211
+ } else if (analysisUpper.includes("VERDICT: NEEDS_INFO")) {
3212
+ verdict = "needs_info";
3213
+ } else {
3214
+ verdict = "ready";
3215
+ }
3216
+ if (verdict === "ready") {
3217
+ await apiPost(`/api/work-items/${itemId}/comments`, {
3218
+ body: analysis,
3219
+ author: "bb-agent",
3220
+ type: "proposal"
3221
+ });
3222
+ await apiPut(`/api/work-items/${itemId}`, { plan: analysis });
3223
+ if (item.status === "open") {
3224
+ await apiPut(`/api/work-items/${itemId}`, { status: "confirmed" });
3225
+ }
3226
+ if (sessionId) await completeSession(sessionId, "completed");
3227
+ console.log(chalk18.green(`VERDICT: READY -- ${key} is ready for implementation.`));
3228
+ } else {
3229
+ await apiPost(`/api/work-items/${itemId}/comments`, {
3230
+ body: analysis,
3231
+ author: "bb-agent",
3232
+ type: "analysis"
3233
+ });
3234
+ await apiPut(`/api/work-items/${itemId}`, { status: "needs_info" });
3235
+ if (sessionId) await completeSession(sessionId, "failed", { error: "NEEDS_INFO" });
3236
+ console.log(chalk18.yellow(`VERDICT: NEEDS_INFO -- ${key} requires clarification.`));
3237
+ console.log(chalk18.red("\nVerification failed -- aborting run."));
3238
+ process.exit(1);
3239
+ }
3240
+ if (!opts.yes) {
3241
+ console.log();
3242
+ const ok = await confirm("Proceed with implementation?");
3243
+ if (!ok) {
3244
+ console.log(chalk18.yellow(`Aborted. Run bb agent execute ${id} when ready.`));
3245
+ return;
3246
+ }
3247
+ }
3248
+ } else {
3249
+ console.log(chalk18.dim("Skipping verification phase."));
3250
+ }
3251
+ if (opts.autoSplit !== false) {
3252
+ console.log(chalk18.bold.cyan("\nPhase 1.5: Checking for split...\n"));
3253
+ const itemForSplit = await resolveItemId(slug, id);
3254
+ const commentsForSplit = await getItemComments(itemForSplit.id);
3255
+ let proposal = null;
3256
+ for (let i = commentsForSplit.length - 1; i >= 0; i--) {
3257
+ if (commentsForSplit[i].type === "proposal") {
3258
+ proposal = commentsForSplit[i].body;
3259
+ break;
3260
+ }
3261
+ }
3262
+ if (proposal && /NEEDS_SPLIT:\s*true/i.test(proposal)) {
3263
+ console.log(chalk18.cyan("Item needs splitting into sub-items."));
3264
+ console.log(chalk18.dim("Use 'bb agent split' to split, then 'bb agent batch-run' for children."));
3265
+ }
3266
+ }
3267
+ console.log(chalk18.bold.cyan("\nPhase 2: Implementing...\n"));
3268
+ const execResult = await executeOne(slug, projectPath, id);
3269
+ if (execResult.status !== "ok") {
3270
+ console.error(chalk18.red(`Execution failed: ${execResult.error ?? "unknown error"}`));
3271
+ process.exit(1);
3272
+ }
3273
+ console.log(chalk18.green("Execution completed."));
3274
+ console.log(chalk18.bold.cyan("\nPhase 3: Docker testing..."));
3275
+ let dockerResult = await dockerTestOne(slug, projectPath, id, timeout);
3276
+ if (dockerResult.status === "ok") {
3277
+ console.log(chalk18.green("Docker tests passed!"));
3278
+ } else {
3279
+ console.log(chalk18.red("Docker tests failed."));
3280
+ let dockerOutput = dockerResult.output ?? "";
3281
+ let retriesDone = 0;
3282
+ while (retriesDone < maxRetries) {
3283
+ retriesDone++;
3284
+ console.log(chalk18.bold.cyan(`
3285
+ Retry ${retriesDone}/${maxRetries}: Re-implementing...`));
3286
+ const reimplResult = await reimplementOne(slug, projectPath, id, dockerOutput);
3287
+ if (reimplResult.status !== "ok") {
3288
+ console.log(chalk18.red(`Re-implementation failed: ${reimplResult.error ?? "?"}`));
3289
+ continue;
3290
+ }
3291
+ console.log(chalk18.cyan("Re-testing with Docker..."));
3292
+ dockerResult = await dockerTestOne(slug, projectPath, id, timeout);
3293
+ if (dockerResult.status === "ok") {
3294
+ console.log(chalk18.green("Docker tests passed after re-implementation!"));
3295
+ break;
3296
+ }
3297
+ dockerOutput = dockerResult.output ?? "";
3298
+ }
3299
+ if (dockerResult.status !== "ok") {
3300
+ const itemFailed = await resolveItemId(slug, id);
3301
+ await apiPut(`/api/work-items/${itemFailed.id}`, { status: "failed" });
3302
+ console.log(chalk18.dim("Status -> failed"));
3303
+ await apiPost(`/api/work-items/${itemFailed.id}/comments`, {
3304
+ body: `## Agent Run Failed
3305
+
3306
+ All ${maxRetries} re-implementation retries exhausted.
3307
+
3308
+ ### Last Docker Output
3309
+
3310
+ ${dockerResult.details ?? "No details"}
3311
+
3312
+ Manual intervention required.`,
3313
+ author: "bb-agent",
3314
+ type: "test_failure"
3315
+ });
3316
+ console.log(chalk18.red(`
3317
+ All ${maxRetries} retries exhausted.`));
3318
+ console.log(chalk18.dim(`Status -> failed. Manual fix: bb agent continue ${id}`));
3319
+ return;
3320
+ }
3321
+ }
3322
+ if (opts.autoMerge !== false) {
3323
+ const itemForMerge = await resolveItemId(slug, id);
3324
+ const itemNumber = itemForMerge.number;
3325
+ const wt = findWorktree(slug, itemNumber);
3326
+ const branchName = await detectWorktreeBranch(projectPath, wt);
3327
+ if (branchName) {
3328
+ console.log(chalk18.bold.cyan(`
3329
+ Phase 4: Merging ${branchName} into ${opts.target}...`));
3330
+ const mergeOk = await doSingleMerge(projectPath, branchName, opts.target);
3331
+ if (mergeOk) {
3332
+ console.log(chalk18.green(`Merged into ${opts.target}.`));
3333
+ await apiPut(`/api/work-items/${itemForMerge.id}`, { status: "resolved" });
3334
+ console.log(chalk18.dim("Status -> resolved"));
3335
+ await removeWorktree(projectPath, wt);
3336
+ console.log(chalk18.dim("Worktree cleaned up."));
3337
+ } else {
3338
+ console.log(chalk18.red("Merge failed -- conflicts detected."));
3339
+ await apiPut(`/api/work-items/${itemForMerge.id}`, { status: "in_review" });
3340
+ console.log(chalk18.dim("Status -> in_review (manual merge needed)"));
3341
+ }
3342
+ } else {
3343
+ console.log(chalk18.yellow("Could not detect branch -- skipping merge."));
3344
+ }
3345
+ } else {
3346
+ console.log(chalk18.dim("Auto-merge disabled. Merge manually when ready."));
3347
+ }
3348
+ });
3349
+ });
3350
+ var batchRunCommand = new Command15("batch-run").description("Execute -> test -> merge for multiple items").argument("<items...>", "Work item IDs/numbers for full loop").option("-P, --parallel <n>", "Max parallel agents", "2").option("--auto-merge", "Auto-merge passing items to target").option("-t, --target <branch>", "Target branch for auto-merge", "release/dev").action(async (items, opts) => {
3351
+ await withErrorHandling(async () => {
3352
+ const slug = requireProject();
3353
+ const projectPath = requireProjectPath(slug);
3354
+ const maxParallel = parseInt(opts.parallel, 10);
3355
+ const target = opts.target;
3356
+ console.log(chalk18.bold.cyan(`Phase 1: Implementing ${items.length} items (max ${maxParallel} parallel)...
3357
+ `));
3358
+ const execTracker = new AgentProgressTracker();
3359
+ for (const ref of items) execTracker.register(ref);
3360
+ execTracker.start();
3361
+ const limit = pLimit3(maxParallel);
3362
+ const execResults = await Promise.all(
3363
+ items.map(
3364
+ (ref) => limit(async () => {
3365
+ const r = await executeOne(slug, projectPath, ref, execTracker);
3366
+ execTracker.complete(ref, r.status === "ok", (r.branch ?? r.error ?? "").slice(0, 60));
3367
+ return r;
3368
+ })
3369
+ )
3370
+ );
3371
+ execTracker.stop();
3372
+ const executed = execResults.filter((r) => r.status === "ok").map((r) => r.key);
3373
+ console.log(chalk18.bold(`
3374
+ Execute done: ${executed.length}/${items.length} succeeded.`));
3375
+ if (!executed.length) return;
3376
+ console.log(chalk18.bold.cyan("\nPhase 2: Docker testing...\n"));
3377
+ const testTracker = new AgentProgressTracker();
3378
+ for (const ref of executed) testTracker.register(ref);
3379
+ testTracker.start();
3380
+ const testResults = await Promise.all(
3381
+ executed.map(
3382
+ (ref) => limit(async () => {
3383
+ const r = await dockerTestOne(slug, projectPath, ref);
3384
+ testTracker.complete(ref, r.status === "ok", (r.details ?? "").slice(0, 60));
3385
+ return r;
3386
+ })
3387
+ )
3388
+ );
3389
+ testTracker.stop();
3390
+ const passed = testResults.filter((r) => r.status === "ok").map((r) => r.key);
3391
+ const failedItems = testResults.filter((r) => r.status !== "ok").map((r) => r.key);
3392
+ console.log(chalk18.bold(`
3393
+ Test done: ${passed.length}/${executed.length} passed.`));
3394
+ if (opts.autoMerge && passed.length) {
3395
+ console.log(chalk18.bold.cyan(`
3396
+ Phase 3: Merging to ${target}...
3397
+ `));
3398
+ for (const itemKey of passed) {
3399
+ try {
3400
+ const mergeItem = await resolveItemId(slug, itemKey);
3401
+ const wt = findWorktree(slug, mergeItem.number);
3402
+ const branchName = await detectWorktreeBranch(projectPath, wt);
3403
+ if (branchName) {
3404
+ const mergeOk = await doSingleMerge(projectPath, branchName, target);
3405
+ if (mergeOk) {
3406
+ console.log(chalk18.green(` ${itemKey.padStart(8)} -- merged`));
3407
+ await apiPut(`/api/work-items/${mergeItem.id}`, { status: "resolved" });
3408
+ await removeWorktree(projectPath, wt);
3409
+ } else {
3410
+ console.log(chalk18.red(` ${itemKey.padStart(8)} -- merge conflict`));
3411
+ }
3412
+ }
3413
+ } catch (e) {
3414
+ console.log(chalk18.red(` ${itemKey.padStart(8)} -- merge error: ${e.message}`));
3415
+ }
3416
+ }
3417
+ } else if (!opts.autoMerge && passed.length) {
3418
+ console.log(chalk18.dim("Merge with: bb agent merge --target release/dev"));
3419
+ }
3420
+ if (failedItems.length) {
3421
+ console.log(chalk18.yellow(`
3422
+ Failed items: ${failedItems.join(" ")}`));
3423
+ console.log(chalk18.dim("Re-implement: bb agent reimplement <item>"));
3424
+ }
3425
+ });
3426
+ });
3427
+
3428
+ // src/commands/agent/continue.ts
3429
+ import { Command as Command16 } from "commander";
3430
+ import chalk19 from "chalk";
3431
+ function buildMcpConfig4() {
3432
+ const apiUrl = getApiUrl();
3433
+ const token = getToken();
3434
+ return JSON.stringify({
3435
+ mcpServers: {
3436
+ bumblebee: {
3437
+ url: `${apiUrl}/mcp`,
3438
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
3439
+ }
3440
+ }
3441
+ });
3442
+ }
3443
+ var continueCommand = new Command16("continue").description("Continue a previous agent run (reads prior comments for context)").argument("<id>", "Work item ID, number, or KEY-number to continue").action(async (id) => {
3444
+ await withErrorHandling(async () => {
3445
+ const slug = requireProject();
3446
+ const projectPath = requireProjectPath(slug);
3447
+ const item = await resolveItemId(slug, id);
3448
+ const itemId = item.id;
3449
+ const key = item.key ?? `#${item.number}`;
3450
+ const knowledge = readKnowledge(projectPath);
3451
+ const comments = await getItemComments(itemId);
3452
+ const commentsCtx = formatCommentsContext(comments);
3453
+ const prompt = buildExecutePrompt(item, knowledge, commentsCtx);
3454
+ console.log(chalk19.cyan(`Creating worktree for ${key}...`));
3455
+ const wt = await createWorktree(projectPath, slug, item);
3456
+ const workDir = wt.worktreePath;
3457
+ const branchName = wt.branchName;
3458
+ console.log(chalk19.green(`Worktree: ${workDir}`));
3459
+ console.log(chalk19.green(`Branch: ${branchName}`));
3460
+ const session = await apiPost("/api/agent-sessions/start", {
3461
+ work_item_id: itemId,
3462
+ origin: "cli"
3463
+ }, { project_slug: slug });
3464
+ const sessionId = session.id;
3465
+ console.log(chalk19.green(`Session: ${sessionId}`));
3466
+ if (["open", "confirmed", "approved"].includes(item.status)) {
3467
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_progress" });
3468
+ console.log(chalk19.dim("Status -> in_progress"));
3469
+ }
3470
+ const streamer = new AgentStreamer(sessionId);
3471
+ streamer.setCallback((_p, text3) => {
3472
+ if (text3) process.stdout.write(text3);
3473
+ });
3474
+ streamer.start();
3475
+ console.log(chalk19.cyan(`
3476
+ Spawning Claude Code agent in ${workDir}...
3477
+ `));
3478
+ const result = await runClaude({
3479
+ cmd: [
3480
+ "claude",
3481
+ "--output-format",
3482
+ "stream-json",
3483
+ "--verbose",
3484
+ "--permission-mode",
3485
+ "bypassPermissions",
3486
+ "--mcp-config",
3487
+ "-",
3488
+ "-p",
3489
+ prompt
3490
+ ],
3491
+ cwd: workDir,
3492
+ onLine: (line) => streamer.feed(line),
3493
+ stdinData: buildMcpConfig4()
3494
+ });
3495
+ const textBlocks = streamer.stop();
3496
+ const tail = textBlocks.slice(-3).join("\n\n") || "No text output captured.";
3497
+ const bodyLines = ["## Agent Execution Report\n"];
3498
+ bodyLines.push(`**Branch**: \`${branchName}\`
3499
+ `);
3500
+ bodyLines.push(`**Exit code**: \`${result.returncode}\`
3501
+ `);
3502
+ if (result.timedOut) bodyLines.push("**Reason**: Process timed out\n");
3503
+ else if (result.stalled) bodyLines.push("**Reason**: Process stalled (no output)\n");
3504
+ else if (result.stderr) bodyLines.push(`**Stderr**: ${result.stderr.slice(0, 300)}
3505
+ `);
3506
+ bodyLines.push(`
3507
+ ### Output (last messages)
3508
+
3509
+ ${tail}`);
3510
+ await apiPost(`/api/work-items/${itemId}/comments`, {
3511
+ body: bodyLines.join("\n"),
3512
+ author: "bb-agent",
3513
+ type: "agent_output"
3514
+ });
3515
+ if (result.returncode === 0) {
3516
+ console.log(chalk19.green("\nAgent completed successfully."));
3517
+ await apiPut(`/api/work-items/${itemId}`, { status: "in_review" });
3518
+ console.log(chalk19.dim("Status -> in_review"));
3519
+ } else {
3520
+ if (result.timedOut) console.log(chalk19.yellow(`
3521
+ Agent timed out.`));
3522
+ else if (result.stalled) console.log(chalk19.yellow(`
3523
+ Agent stalled.`));
3524
+ else console.log(chalk19.yellow(`
3525
+ Agent exited with code ${result.returncode}.`));
3526
+ }
3527
+ console.log(chalk19.dim(`
3528
+ Worktree: ${workDir}`));
3529
+ console.log(chalk19.dim(`Cleanup: bb agent cleanup ${item.number}`));
3530
+ });
3531
+ });
3532
+
3533
+ // src/commands/agent/split.ts
3534
+ import { Command as Command17 } from "commander";
3535
+ import chalk20 from "chalk";
3536
+ import readline2 from "readline";
3537
+ async function confirm2(question) {
3538
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3539
+ return new Promise((resolve) => {
3540
+ rl.question(question + " (y/N) ", (answer) => {
3541
+ rl.close();
3542
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
3543
+ });
3544
+ });
3545
+ }
3546
+ var splitCommand = new Command17("split").description("Split a work item into sub-items based on the latest proposal's SPLIT_RESULT").argument("<id>", "Parent work item ID or number").option("-y, --yes", "Auto-confirm split").action(async (id, opts) => {
3547
+ await withErrorHandling(async () => {
3548
+ const slug = requireProject();
3549
+ const item = await resolveItemId(slug, id);
3550
+ const itemId = item.id;
3551
+ const key = item.key ?? `#${item.number}`;
3552
+ const comments = await getItemComments(itemId);
3553
+ let proposal = null;
3554
+ for (let i = comments.length - 1; i >= 0; i--) {
3555
+ if (comments[i].type === "proposal") {
3556
+ proposal = comments[i].body;
3557
+ break;
3558
+ }
3559
+ }
3560
+ if (!proposal) {
3561
+ console.error(chalk20.red(`No proposal comment found for ${key}. Run 'bb agent suggest ${id}' first.`));
3562
+ process.exit(1);
3563
+ }
3564
+ const splitMatch = proposal.match(/### SPLIT_RESULT\s*\n([\s\S]*?)(?:\n###|\Z|$)/);
3565
+ if (!splitMatch) {
3566
+ console.log(chalk20.yellow(`No SPLIT_RESULT block found in proposal for ${key}.`));
3567
+ console.log(chalk20.dim("The suggest prompt may not have included split analysis."));
3568
+ process.exit(1);
3569
+ }
3570
+ const block = splitMatch[1].trim();
3571
+ const needsSplitMatch = block.match(/NEEDS_SPLIT:\s*(true|false)/i);
3572
+ if (!needsSplitMatch || needsSplitMatch[1].toLowerCase() === "false") {
3573
+ console.log(chalk20.green(`${key} does not need splitting (single-package scope).`));
3574
+ return;
3575
+ }
3576
+ const subItems = [];
3577
+ let currentItem = {};
3578
+ for (const line of block.split("\n")) {
3579
+ const trimmed = line.trim();
3580
+ if (trimmed.startsWith("- SCOPE:")) {
3581
+ if (currentItem.title) {
3582
+ subItems.push({ ...currentItem });
3583
+ }
3584
+ currentItem = { scope: trimmed.split(":").slice(1).join(":").trim() };
3585
+ } else if (trimmed.startsWith("TITLE:")) {
3586
+ currentItem.title = trimmed.split(":").slice(1).join(":").trim();
3587
+ } else if (trimmed.startsWith("DESCRIPTION:")) {
3588
+ currentItem.description = trimmed.split(":").slice(1).join(":").trim();
3589
+ } else if (trimmed.startsWith("ACCEPTANCE_CRITERIA:")) {
3590
+ currentItem.acceptance_criteria = trimmed.split(":").slice(1).join(":").trim();
3591
+ }
3592
+ }
3593
+ if (currentItem.title) {
3594
+ subItems.push({ ...currentItem });
3595
+ }
3596
+ if (!subItems.length) {
3597
+ console.log(chalk20.yellow("SPLIT_RESULT block found but no items could be parsed."));
3598
+ process.exit(1);
3599
+ }
3600
+ console.log(chalk20.bold(`
3601
+ Proposed sub-items for ${key}:
3602
+ `));
3603
+ console.log(" # Scope Title Description");
3604
+ console.log(" " + "-".repeat(70));
3605
+ for (let i = 0; i < subItems.length; i++) {
3606
+ const si = subItems[i];
3607
+ const desc = (si.description ?? "").length > 60 ? (si.description ?? "").slice(0, 60) + "..." : si.description ?? "";
3608
+ console.log(
3609
+ ` ${String(i + 1).padEnd(2)} ${(si.scope ?? "?").padEnd(15)} ${(si.title ?? "?").padEnd(30)} ${desc}`
3610
+ );
3611
+ }
3612
+ if (!opts.yes) {
3613
+ const ok = await confirm2(`
3614
+ Create ${subItems.length} sub-items under ${key}?`);
3615
+ if (!ok) {
3616
+ console.log(chalk20.yellow("Aborted."));
3617
+ return;
3618
+ }
3619
+ }
3620
+ const createdKeys = [];
3621
+ for (const si of subItems) {
3622
+ const payload = {
3623
+ title: si.title ?? "Untitled",
3624
+ type: "task",
3625
+ description: si.description ?? "",
3626
+ acceptance_criteria: si.acceptance_criteria ?? "",
3627
+ parent_id: itemId,
3628
+ priority: item.priority ?? "medium",
3629
+ status: "open"
3630
+ };
3631
+ const child = await apiPost(`/api/projects/${slug}/work-items`, payload);
3632
+ const childKey = child.key ?? `#${child.number}`;
3633
+ createdKeys.push(childKey);
3634
+ console.log(chalk20.green(` Created ${childKey}: ${si.title ?? ""}`));
3635
+ }
3636
+ const summaryLines = [`## Split into ${createdKeys.length} sub-items
3637
+ `];
3638
+ for (let i = 0; i < createdKeys.length; i++) {
3639
+ const si = subItems[i];
3640
+ summaryLines.push(`- **${createdKeys[i]}** [${si.scope ?? "?"}]: ${si.title ?? "?"}`);
3641
+ }
3642
+ await apiPost(`/api/work-items/${itemId}/comments`, {
3643
+ body: summaryLines.join("\n"),
3644
+ author: "bb-agent",
3645
+ type: "agent_output"
3646
+ });
3647
+ console.log(chalk20.green(`
3648
+ Split complete! Created ${createdKeys.length} sub-items.`));
3649
+ console.log(chalk20.dim(`
3650
+ Next step:`));
3651
+ console.log(chalk20.dim(` bb agent batch-run ${createdKeys.join(" ")}`));
3652
+ });
3653
+ });
3654
+
3655
+ // src/commands/agent/integrate.ts
3656
+ import { Command as Command18 } from "commander";
3657
+ import chalk21 from "chalk";
3658
+ import fs13 from "fs";
3659
+ import readline3 from "readline";
3660
+ import { execa as execa5 } from "execa";
3661
+ async function confirm3(question) {
3662
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
3663
+ return new Promise((resolve) => {
3664
+ rl.question(question + " (y/N) ", (answer) => {
3665
+ rl.close();
3666
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
3667
+ });
3668
+ });
3669
+ }
3670
+ var integrateCommand = new Command18("integrate").description("Integrate all child branches: merge into integration branch, Docker test, merge to target").argument("<id>", "Parent work item ID or number").option("-t, --target <branch>", "Target branch for integration", "release/dev").option("--cleanup", "Remove worktrees after successful integration").option("--timeout <seconds>", "Docker test timeout in seconds", "600").option("-y, --yes", "Auto-confirm integration").action(async (id, opts) => {
3671
+ await withErrorHandling(async () => {
3672
+ const slug = requireProject();
3673
+ const projectPath = requireProjectPath(slug);
3674
+ const timeout = parseInt(opts.timeout, 10) * 1e3;
3675
+ const target = opts.target;
3676
+ const item = await resolveItemId(slug, id);
3677
+ const itemId = item.id;
3678
+ const key = item.key ?? `#${item.number}`;
3679
+ const children = await apiGet(`/api/work-items/${itemId}/children`);
3680
+ if (!children || children.length === 0) {
3681
+ console.error(chalk21.red(`No child items found for ${key}.`));
3682
+ process.exit(1);
3683
+ }
3684
+ const notReady = children.filter((c) => !["resolved", "in_review"].includes(c.status));
3685
+ if (notReady.length) {
3686
+ console.log(chalk21.yellow("Some children are not ready for integration:"));
3687
+ for (const c of notReady) {
3688
+ const ck = c.key ?? `#${c.number}`;
3689
+ console.log(chalk21.yellow(` ${ck}: ${c.status}`));
3690
+ }
3691
+ if (!opts.yes) {
3692
+ const ok = await confirm3("Proceed anyway?");
3693
+ if (!ok) return;
3694
+ }
3695
+ }
3696
+ const childBranches = [];
3697
+ for (const c of children) {
3698
+ if (["resolved", "in_review"].includes(c.status)) {
3699
+ const wt = findWorktree(slug, c.number);
3700
+ if (fs13.existsSync(wt)) {
3701
+ const branch = await detectWorktreeBranch(projectPath, wt);
3702
+ if (branch) {
3703
+ childBranches.push({
3704
+ key: c.key ?? `#${c.number}`,
3705
+ branch,
3706
+ worktree: wt,
3707
+ id: c.id
3708
+ });
3709
+ } else {
3710
+ console.log(chalk21.yellow(` Could not detect branch for #${c.number}`));
3711
+ }
3712
+ }
3713
+ }
3714
+ }
3715
+ if (!childBranches.length) {
3716
+ console.error(chalk21.red("No child branches found. Children may not have worktrees."));
3717
+ process.exit(1);
3718
+ }
3719
+ console.log(chalk21.bold(`
3720
+ Integrating ${childBranches.length} branches for ${key}:`));
3721
+ for (const cb of childBranches) {
3722
+ console.log(` ${cb.key}: ${cb.branch}`);
3723
+ }
3724
+ const integrationBranch = `integrate/${key.toLowerCase()}`;
3725
+ console.log(chalk21.cyan(`
3726
+ Creating integration branch: ${integrationBranch}`));
3727
+ const { stdout: currentRaw } = await execa5("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
3728
+ cwd: projectPath
3729
+ });
3730
+ const current = currentRaw.trim();
3731
+ await execa5("git", ["worktree", "prune"], { cwd: projectPath, reject: false });
3732
+ await execa5("git", ["branch", "-D", integrationBranch], { cwd: projectPath, reject: false });
3733
+ await execa5("git", ["branch", integrationBranch, target], { cwd: projectPath, reject: false });
3734
+ const co = await execa5("git", ["checkout", integrationBranch], { cwd: projectPath, reject: false });
3735
+ if (co.exitCode !== 0) {
3736
+ console.error(chalk21.red(`Failed to checkout integration branch: ${co.stderr}`));
3737
+ process.exit(1);
3738
+ }
3739
+ const mergeFailures = [];
3740
+ for (const cb of childBranches) {
3741
+ process.stdout.write(` Merging ${cb.branch}... `);
3742
+ const mergeResult = await execa5(
3743
+ "git",
3744
+ ["merge", cb.branch, "--no-ff", "-m", `Merge ${cb.branch} into ${integrationBranch}`],
3745
+ { cwd: projectPath, reject: false }
3746
+ );
3747
+ if (mergeResult.exitCode === 0) {
3748
+ console.log(chalk21.green("ok"));
3749
+ } else {
3750
+ console.log(chalk21.red("CONFLICT"));
3751
+ await execa5("git", ["merge", "--abort"], { cwd: projectPath, reject: false });
3752
+ mergeFailures.push(cb);
3753
+ }
3754
+ }
3755
+ if (mergeFailures.length) {
3756
+ console.log(chalk21.red(`
3757
+ Merge conflicts in ${mergeFailures.length} branch(es):`));
3758
+ for (const mf of mergeFailures) {
3759
+ console.log(chalk21.red(` ${mf.key}: ${mf.branch}`));
3760
+ }
3761
+ await execa5("git", ["checkout", current], { cwd: projectPath, reject: false });
3762
+ await execa5("git", ["branch", "-D", integrationBranch], { cwd: projectPath, reject: false });
3763
+ await apiPost(`/api/work-items/${itemId}/comments`, {
3764
+ body: `## Integration Failed
3765
+
3766
+ Merge conflicts in: ${mergeFailures.map((mf) => mf.key).join(", ")}`,
3767
+ author: "bb-agent",
3768
+ type: "test_failure"
3769
+ });
3770
+ process.exit(1);
3771
+ }
3772
+ console.log(chalk21.green(`
3773
+ All branches merged into ${integrationBranch}.`));
3774
+ console.log(chalk21.cyan("\nRunning Docker tests on integration branch..."));
3775
+ ensureComposeFiles(projectPath, projectPath);
3776
+ const dockerResult = await runDockerTests(projectPath, timeout);
3777
+ if (dockerResult.success) {
3778
+ console.log(chalk21.green(`Docker tests passed!
3779
+ ${dockerResult.details}`));
3780
+ console.log(chalk21.cyan(`
3781
+ Merging ${integrationBranch} into ${target}...`));
3782
+ await execa5("git", ["checkout", target], { cwd: projectPath, reject: false });
3783
+ const ffResult = await execa5(
3784
+ "git",
3785
+ ["merge", integrationBranch, "--ff-only"],
3786
+ { cwd: projectPath, reject: false }
3787
+ );
3788
+ if (ffResult.exitCode !== 0) {
3789
+ await execa5(
3790
+ "git",
3791
+ ["merge", integrationBranch, "--no-ff", "-m", `Merge ${integrationBranch} into ${target}`],
3792
+ { cwd: projectPath, reject: false }
3793
+ );
3794
+ }
3795
+ await apiPut(`/api/work-items/${itemId}`, { status: "resolved" });
3796
+ console.log(chalk21.dim("Parent status -> resolved"));
3797
+ for (const cb of childBranches) {
3798
+ await apiPut(`/api/work-items/${cb.id}`, { status: "resolved" });
3799
+ }
3800
+ if (opts.cleanup) {
3801
+ for (const cb of childBranches) {
3802
+ await removeWorktree(projectPath, cb.worktree);
3803
+ }
3804
+ console.log(chalk21.dim("Worktrees cleaned up."));
3805
+ }
3806
+ await execa5("git", ["branch", "-D", integrationBranch], { cwd: projectPath, reject: false });
3807
+ await apiPost(`/api/work-items/${itemId}/comments`, {
3808
+ body: `## Integration Successful
3809
+
3810
+ All ${childBranches.length} branches merged and Docker tests passed.
3811
+ Merged to \`${target}\`.`,
3812
+ author: "bb-agent",
3813
+ type: "agent_output"
3814
+ });
3815
+ console.log(chalk21.green(`
3816
+ Integration complete! All branches merged to ${target}.`));
3817
+ } else {
3818
+ console.log(chalk21.red(`Docker tests failed!
3819
+ ${dockerResult.details}`));
3820
+ await execa5("git", ["checkout", current], { cwd: projectPath, reject: false });
3821
+ await execa5("git", ["branch", "-D", integrationBranch], { cwd: projectPath, reject: false });
3822
+ const truncated = truncateOutput2(dockerResult.rawOutput, 4e3);
3823
+ await apiPost(`/api/work-items/${itemId}/comments`, {
3824
+ body: `## Integration Failed
3825
+
3826
+ **Docker tests failed.**
3827
+
3828
+ ${dockerResult.details}
3829
+
3830
+ ### Docker Output
3831
+
3832
+ \`\`\`
3833
+ ${truncated}
3834
+ \`\`\``,
3835
+ author: "bb-agent",
3836
+ type: "test_failure"
3837
+ });
3838
+ console.log(chalk21.red("\nIntegration failed. See Docker test output above."));
3839
+ process.exit(1);
3840
+ }
3841
+ await execa5("git", ["checkout", current], { cwd: projectPath, reject: false });
3842
+ });
3843
+ });
3844
+
3845
+ // src/commands/agent/merge.ts
3846
+ import { Command as Command19 } from "commander";
3847
+ import chalk22 from "chalk";
3848
+ import fs14 from "fs";
3849
+ import { execa as execa6 } from "execa";
3850
+ var mergeCommand = new Command19("merge").description("Merge agent branches into a target branch (e.g. release/dev)").argument("[items...]", "Specific item numbers (default: all agent branches)").option("-t, --target <branch>", "Target branch to merge into", "release/dev").option("--cleanup", "Remove worktrees + branches after successful merge").option("--agent", "Use Claude to resolve merge conflicts").action(async (items, opts) => {
3851
+ await withErrorHandling(async () => {
3852
+ const slug = requireProject();
3853
+ const projectPath = requireProjectPath(slug);
3854
+ const { stdout: currentRaw } = await execa6("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
3855
+ cwd: projectPath
3856
+ });
3857
+ const current = currentRaw.trim();
3858
+ const allBranches = await listAgentBranches(projectPath);
3859
+ let branches;
3860
+ if (items && items.length > 0) {
3861
+ const wantedNums = new Set(items.map((n) => parseInt(n, 10)));
3862
+ branches = allBranches.filter((b) => {
3863
+ const num = extractItemNumberFromBranch(b);
3864
+ return num !== null && wantedNums.has(num);
3865
+ });
3866
+ } else {
3867
+ branches = allBranches;
3868
+ }
3869
+ if (!branches.length) {
3870
+ console.log(chalk22.yellow("No agent branches found to merge."));
3871
+ return;
3872
+ }
3873
+ const target = opts.target;
3874
+ console.log(chalk22.cyan(`Merging ${branches.length} branches into ${target}...
3875
+ `));
3876
+ for (const b of branches) {
3877
+ console.log(` ${b}`);
3878
+ }
3879
+ console.log();
3880
+ const check = await execa6("git", ["rev-parse", "--verify", target], {
3881
+ cwd: projectPath,
3882
+ reject: false
3883
+ });
3884
+ if (check.exitCode !== 0) {
3885
+ console.log(chalk22.yellow(`Branch '${target}' does not exist. Creating from HEAD...`));
3886
+ await execa6("git", ["branch", target], { cwd: projectPath });
3887
+ }
3888
+ await execa6("git", ["worktree", "prune"], { cwd: projectPath, reject: false });
3889
+ const co = await execa6("git", ["checkout", target], { cwd: projectPath, reject: false });
3890
+ if (co.exitCode !== 0) {
3891
+ console.error(chalk22.red(`Failed to checkout ${target}: ${co.stderr}`));
3892
+ return;
3893
+ }
3894
+ const merged = [];
3895
+ const failed = [];
3896
+ for (const branch of branches) {
3897
+ process.stdout.write(` Merging ${branch}... `);
3898
+ const mergeResult = await execa6(
3899
+ "git",
3900
+ ["merge", branch, "--no-ff", "-m", `Merge ${branch} into ${target}`],
3901
+ { cwd: projectPath, reject: false }
3902
+ );
3903
+ if (mergeResult.exitCode === 0) {
3904
+ console.log(chalk22.green("ok"));
3905
+ merged.push(branch);
3906
+ } else {
3907
+ if (opts.agent) {
3908
+ console.log(chalk22.yellow("conflict -> resolving with Claude..."));
3909
+ await runClaude({
3910
+ cmd: [
3911
+ "claude",
3912
+ "-p",
3913
+ `Resolve all merge conflicts in this git repo. The merge of '${branch}' into '${target}' has conflicts. Use 'git diff' to find conflicts, resolve them keeping both sets of changes where possible, then stage and commit. Do NOT abort the merge.`,
3914
+ "--output-format",
3915
+ "text",
3916
+ "--permission-mode",
3917
+ "bypassPermissions"
3918
+ ],
3919
+ cwd: projectPath,
3920
+ timeout: 3e5
3921
+ });
3922
+ const statusCheck = await execa6(
3923
+ "git",
3924
+ ["diff", "--name-only", "--diff-filter=U"],
3925
+ { cwd: projectPath, reject: false }
3926
+ );
3927
+ if (statusCheck.stdout.trim() === "") {
3928
+ console.log(chalk22.green(" Conflict resolved by agent"));
3929
+ merged.push(branch);
3930
+ } else {
3931
+ console.log(chalk22.red(" Agent could not resolve all conflicts"));
3932
+ await execa6("git", ["merge", "--abort"], { cwd: projectPath, reject: false });
3933
+ failed.push([branch, "conflict (agent failed)"]);
3934
+ }
3935
+ } else {
3936
+ console.log(chalk22.red("CONFLICT"));
3937
+ await execa6("git", ["merge", "--abort"], { cwd: projectPath, reject: false });
3938
+ failed.push([branch, "conflict"]);
3939
+ }
3940
+ }
3941
+ }
3942
+ console.log();
3943
+ console.log(chalk22.bold(`Merge Results -> ${target}`));
3944
+ console.log("-".repeat(50));
3945
+ for (const b of merged) {
3946
+ console.log(` ${b.padEnd(40)} ${chalk22.green("merged")}`);
3947
+ }
3948
+ for (const [b, reason] of failed) {
3949
+ console.log(` ${b.padEnd(40)} ${chalk22.red(reason)}`);
3950
+ }
3951
+ if (opts.cleanup && merged.length) {
3952
+ for (const branch of merged) {
3953
+ const num = extractItemNumberFromBranch(branch);
3954
+ if (num !== null) {
3955
+ try {
3956
+ const wt = findWorktree(slug, num);
3957
+ if (fs14.existsSync(wt)) {
3958
+ await removeWorktree(projectPath, wt);
3959
+ }
3960
+ } catch {
3961
+ }
3962
+ }
3963
+ await execa6("git", ["branch", "-D", branch], {
3964
+ cwd: projectPath,
3965
+ reject: false
3966
+ });
3967
+ }
3968
+ console.log(chalk22.dim("Cleaned up merged worktrees and branches."));
3969
+ }
3970
+ if (failed.length) {
3971
+ console.log(chalk22.yellow("\nFailed branches can be retried:"));
3972
+ console.log(chalk22.dim(" bb agent merge --agent (use Claude to resolve conflicts)"));
3973
+ console.log(chalk22.dim(` or resolve manually: git checkout ${target} && git merge <branch>`));
3974
+ }
3975
+ await execa6("git", ["checkout", current], { cwd: projectPath, reject: false });
3976
+ });
3977
+ });
3978
+
3979
+ // src/commands/agent/status.ts
3980
+ import { Command as Command20 } from "commander";
3981
+ import chalk23 from "chalk";
3982
+ var statusCommand = new Command20("status").description("Show agent sessions for the current project").action(async () => {
3983
+ await withErrorHandling(async () => {
3984
+ const slug = requireProject();
3985
+ const sessions = await apiGet("/api/agent-sessions", { project_slug: slug });
3986
+ if (!sessions || sessions.length === 0) {
3987
+ console.log(chalk23.dim("No agent sessions."));
3988
+ return;
3989
+ }
3990
+ for (const s of sessions) {
3991
+ const colorFn = s.status === "running" ? chalk23.yellow : s.status === "completed" ? chalk23.green : s.status === "failed" ? chalk23.red : chalk23.white;
3992
+ const sid = String(s.id).slice(0, 8);
3993
+ const itemId = s.work_item_id ?? "--";
3994
+ console.log(` ${colorFn(s.status.padStart(10))} ${sid} item: ${itemId}`);
3995
+ }
3996
+ });
3997
+ });
3998
+ var abortCommand = new Command20("abort").description("Abort a running agent session").argument("<session-id>", "Session ID to abort").action(async (sessionId) => {
3999
+ await withErrorHandling(async () => {
4000
+ await apiPost(`/api/agent-sessions/${sessionId}/abort`);
4001
+ console.log(chalk23.yellow(`Session ${sessionId.slice(0, 8)} aborted.`));
4002
+ });
4003
+ });
4004
+
4005
+ // src/commands/agent/worktree.ts
4006
+ import { Command as Command21 } from "commander";
4007
+ import chalk24 from "chalk";
4008
+ import fs15 from "fs";
4009
+ import { execa as execa7 } from "execa";
4010
+ var worktreesCommand = new Command21("worktrees").description("List active agent worktrees for the current project").action(async () => {
4011
+ await withErrorHandling(async () => {
4012
+ const slug = requireProject();
4013
+ const projectPath = requireProjectPath(slug);
4014
+ const result = await execa7("git", ["worktree", "list"], {
4015
+ cwd: projectPath,
4016
+ reject: false
4017
+ });
4018
+ if (result.exitCode !== 0) {
4019
+ console.error(chalk24.red("Failed to list worktrees."));
4020
+ process.exit(1);
4021
+ }
4022
+ const wtDir = WORKTREES_DIR.replace(/\\/g, "/") + "/" + slug;
4023
+ const lines = result.stdout.trim().split("\n");
4024
+ const bbLines = lines.filter((ln) => ln.replace(/\\/g, "/").includes(wtDir));
4025
+ if (!bbLines.length) {
4026
+ console.log(chalk24.dim("No agent worktrees."));
4027
+ return;
4028
+ }
4029
+ console.log(chalk24.bold("Agent worktrees:"));
4030
+ for (const ln of bbLines) {
4031
+ console.log(` ${ln}`);
4032
+ }
4033
+ });
4034
+ });
4035
+ var cleanupCommand = new Command21("cleanup").description("Remove the worktree created for a work item").argument("<item-number>", "Work item number whose worktree to remove").option("-D, --delete-branch", "Also delete the git branch").action(async (itemNumberStr, opts) => {
4036
+ await withErrorHandling(async () => {
4037
+ const slug = requireProject();
4038
+ const projectPath = requireProjectPath(slug);
4039
+ const itemNumber = parseInt(itemNumberStr, 10);
4040
+ if (isNaN(itemNumber)) {
4041
+ console.error(chalk24.red("Invalid item number."));
4042
+ process.exit(1);
4043
+ }
4044
+ const wt = findWorktree(slug, itemNumber);
4045
+ if (!fs15.existsSync(wt)) {
4046
+ console.log(chalk24.yellow(`No worktree found for item #${itemNumber}.`));
4047
+ return;
4048
+ }
4049
+ const branch = await detectWorktreeBranch(projectPath, wt);
4050
+ await removeWorktree(projectPath, wt);
4051
+ console.log(chalk24.green(`Worktree removed: ${wt}`));
4052
+ if (opts.deleteBranch && branch) {
4053
+ await execa7("git", ["branch", "-D", branch], {
4054
+ cwd: projectPath,
4055
+ reject: false
4056
+ });
4057
+ console.log(chalk24.green(`Branch deleted: ${branch}`));
4058
+ } else if (branch) {
4059
+ console.log(chalk24.dim(`Branch '${branch}' kept. Delete with: git branch -D ${branch}`));
4060
+ } else {
4061
+ console.log(chalk24.dim("Could not detect branch name (worktree may have been stale)."));
4062
+ }
4063
+ });
4064
+ });
4065
+
4066
+ // src/commands/agent/daemon.ts
4067
+ import { Command as Command22 } from "commander";
4068
+ import chalk25 from "chalk";
4069
+ import os4 from "os";
4070
+ function daemonId() {
4071
+ return `${os4.hostname()}-${process.pid}`;
4072
+ }
4073
+ async function pollPendingSessions(slug) {
4074
+ try {
4075
+ const sessions = await apiGet("/api/agent-sessions", { project_slug: slug });
4076
+ return sessions.filter((s) => s.status === "pending" && !s.claimed_by);
4077
+ } catch {
4078
+ return [];
4079
+ }
4080
+ }
4081
+ async function claimSession(sessionId, dId) {
4082
+ try {
4083
+ return await apiPost(`/api/agent-sessions/${sessionId}/claim`, void 0, { daemon_id: dId });
4084
+ } catch {
4085
+ return null;
4086
+ }
4087
+ }
4088
+ async function runSession(slug, projectPath, session) {
4089
+ const sessionId = session.id;
4090
+ const workItemId = session.work_item_id;
4091
+ const phase = session.phase ?? "execute";
4092
+ if (!workItemId) {
4093
+ console.log(chalk25.yellow(` Session ${sessionId}: no work_item_id, skipping.`));
4094
+ await apiPost(`/api/agent-sessions/${sessionId}/complete`, {
4095
+ status: "failed",
4096
+ error: "No work item specified"
4097
+ });
4098
+ return;
4099
+ }
4100
+ let idOrNumber;
4101
+ try {
4102
+ const item = await apiGet(`/api/work-items/${workItemId}`);
4103
+ idOrNumber = item.key ?? String(item.number);
4104
+ } catch (e) {
4105
+ console.log(chalk25.red(` Session ${sessionId}: failed to resolve item ${workItemId}: ${e.message}`));
4106
+ await apiPost(`/api/agent-sessions/${sessionId}/complete`, {
4107
+ status: "failed",
4108
+ error: String(e.message)
4109
+ });
4110
+ return;
4111
+ }
4112
+ console.log(chalk25.cyan(` Running ${phase} for ${idOrNumber}...`));
4113
+ let result;
4114
+ if (phase === "suggest" || phase === "verify") {
4115
+ result = await suggestOne(slug, projectPath, idOrNumber);
4116
+ } else {
4117
+ result = await executeOne(slug, projectPath, idOrNumber);
4118
+ }
4119
+ const status = result.status === "ok" ? "ok" : "failed";
4120
+ const color = status === "ok" ? chalk25.green : chalk25.red;
4121
+ const suffix = status !== "ok" ? ` -- ${result.error ?? ""}` : "";
4122
+ console.log(color(` ${idOrNumber}: ${status}${suffix}`));
4123
+ }
4124
+ function sleep(ms) {
4125
+ return new Promise((resolve) => setTimeout(resolve, ms));
4126
+ }
4127
+ var daemonCommand = new Command22("daemon").description("Start the agent daemon -- polls for web-initiated sessions and executes them").option("-p, --poll <seconds>", "Polling interval in seconds", "5").action(async (opts) => {
4128
+ await withErrorHandling(async () => {
4129
+ const slug = getCurrentProject();
4130
+ if (!slug) {
4131
+ console.error(chalk25.red("No project configured. Run 'bb init' or 'bb project use <slug>' first."));
4132
+ process.exit(1);
4133
+ }
4134
+ const projectPath = getProjectPath(slug);
4135
+ if (!projectPath) {
4136
+ console.error(chalk25.red("No project path configured. Run 'bb init' first."));
4137
+ process.exit(1);
4138
+ }
4139
+ const token = getToken();
4140
+ if (!token) {
4141
+ console.error(chalk25.red("Not authenticated. Run 'bb login' first."));
4142
+ process.exit(1);
4143
+ }
4144
+ const dId = daemonId();
4145
+ const apiUrl = getApiUrl();
4146
+ const pollInterval = parseInt(opts.poll, 10) * 1e3;
4147
+ console.log(chalk25.bold.green("Bumblebee Agent Daemon"));
4148
+ console.log(` Project: ${chalk25.cyan(slug)}`);
4149
+ console.log(` Path: ${chalk25.cyan(projectPath)}`);
4150
+ console.log(` API: ${chalk25.cyan(apiUrl)}`);
4151
+ console.log(` Daemon: ${chalk25.cyan(dId)}`);
4152
+ console.log(` Poll: every ${opts.poll}s`);
4153
+ console.log(chalk25.dim("\nListening for web-initiated agent requests... (Ctrl+C to stop)\n"));
4154
+ let running = true;
4155
+ const shutdown = () => {
4156
+ console.log(chalk25.yellow("\nShutting down daemon..."));
4157
+ running = false;
4158
+ };
4159
+ process.on("SIGINT", shutdown);
4160
+ process.on("SIGTERM", shutdown);
4161
+ while (running) {
4162
+ const pending = await pollPendingSessions(slug);
4163
+ for (const session of pending) {
4164
+ const claimed = await claimSession(session.id, dId);
4165
+ if (claimed) {
4166
+ console.log(chalk25.cyan(`Claimed session ${session.id}`));
4167
+ try {
4168
+ await runSession(slug, projectPath, claimed);
4169
+ } catch (e) {
4170
+ console.log(chalk25.red(`Session ${session.id} failed: ${e.message}`));
4171
+ try {
4172
+ await apiPost(`/api/agent-sessions/${session.id}/complete`, {
4173
+ status: "failed",
4174
+ error: String(e.message)
4175
+ });
4176
+ } catch {
4177
+ }
4178
+ }
4179
+ }
4180
+ }
4181
+ await sleep(pollInterval);
4182
+ }
4183
+ console.log(chalk25.dim("Daemon stopped."));
4184
+ });
4185
+ });
4186
+
4187
+ // src/commands/agent/index.ts
4188
+ var agentCommand = new Command23("agent").description("Agent session management").addCommand(suggestCommand).addCommand(executeCommand).addCommand(testCommand).addCommand(verifyCommand).addCommand(reimplementCommand).addCommand(runCommand).addCommand(continueCommand).addCommand(splitCommand).addCommand(integrateCommand).addCommand(mergeCommand).addCommand(statusCommand).addCommand(abortCommand).addCommand(worktreesCommand).addCommand(cleanupCommand).addCommand(batchSuggestCommand).addCommand(batchExecuteCommand).addCommand(batchRunCommand).addCommand(daemonCommand);
4189
+
4190
+ // src/index.ts
4191
+ var program = new Command24();
4192
+ program.name("bb").description("Bumblebee \u2014 Dev Task Management + Claude Code Automation CLI").version("0.3.0");
4193
+ program.addCommand(authCommand);
4194
+ program.addCommand(projectCommand);
4195
+ program.addCommand(itemCommand);
4196
+ program.addCommand(commentCommand);
4197
+ program.addCommand(sprintCommand);
4198
+ program.addCommand(labelCommand);
4199
+ program.addCommand(boardCommand);
4200
+ program.addCommand(doctorCommand);
4201
+ program.addCommand(initCommand);
4202
+ program.addCommand(agentCommand);
4203
+ program.command("login").description("Log in (alias for auth login)").option("-e, --email <email>", "Email address").option("-p, --password <password>", "Password").action(async (opts) => {
4204
+ await program.parseAsync(["auth", "login", ...opts.email ? ["-e", opts.email] : [], ...opts.password ? ["-p", opts.password] : []], { from: "user" });
4205
+ });
4206
+ program.command("logout").description("Log out (alias for auth logout)").action(async () => {
4207
+ await program.parseAsync(["auth", "logout"], { from: "user" });
4208
+ });
4209
+ process.on("unhandledRejection", (err) => {
4210
+ console.error(`Error: ${err?.message ?? err}`);
4211
+ process.exit(1);
4212
+ });
4213
+ program.parse();
4214
+ //# sourceMappingURL=index.js.map