bosun 0.29.8 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.mjs CHANGED
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * Usage:
7
7
  * bosun # start with default config
8
- * bosun --setup # run setup wizard
8
+ * bosun --setup # launch web setup wizard
9
+ * bosun --setup-terminal # terminal setup wizard
9
10
  * bosun --args "-MaxParallel 6" # pass orchestrator args
10
11
  * bosun --help # show help
11
12
  *
@@ -65,7 +66,8 @@ function showHelp() {
65
66
  bosun [options]
66
67
 
67
68
  COMMANDS
68
- --setup Run the interactive setup wizard
69
+ --setup Launch the web-based setup wizard (default)
70
+ --setup-terminal Run the legacy terminal setup wizard
69
71
  --where Show the resolved bosun config directory
70
72
  --doctor Validate bosun .env/config setup
71
73
  --help Show this help
@@ -194,7 +196,8 @@ function showHelp() {
194
196
 
195
197
  EXAMPLES
196
198
  bosun # start with defaults
197
- bosun --setup # interactive setup
199
+ bosun --setup # web setup wizard
200
+ bosun --setup-terminal # terminal setup wizard
198
201
  bosun --script ./my-orchestrator.sh # custom script
199
202
  bosun --args "-MaxParallel 4" --no-telegram-bot # custom args
200
203
  bosun --no-codex --no-autofix # minimal mode
@@ -497,9 +500,10 @@ function startDaemon() {
497
500
  env: {
498
501
  ...process.env,
499
502
  BOSUN_DAEMON: "1",
500
- // Propagate the bosun package directory so repo-root detection works
503
+ // Propagate the bosun config directory so repo-root detection works
501
504
  // even when the daemon child's cwd is not inside a git repo.
502
- BOSUN_DIR: process.env.BOSUN_DIR || fileURLToPath(new URL(".", import.meta.url)),
505
+ // Use the proper config dir (APPDATA/bosun or ~/bosun), NOT __dirname.
506
+ BOSUN_DIR: process.env.BOSUN_DIR || resolveConfigDirForCli(),
503
507
  // Propagate REPO_ROOT if available; otherwise resolve from cwd before detaching
504
508
  ...(process.env.REPO_ROOT
505
509
  ? {}
@@ -983,17 +987,24 @@ async function main() {
983
987
  process.exit(result.ok ? 0 : 1);
984
988
  }
985
989
 
986
- // Handle --setup
987
- if (args.includes("--setup") || args.includes("setup")) {
990
+ // Handle --setup-terminal (legacy terminal wizard)
991
+ if (args.includes("--setup-terminal")) {
988
992
  const configDirArg = getArgValue("--config-dir");
989
- if (configDirArg) {
990
- process.env.BOSUN_DIR = configDirArg;
991
- }
993
+ if (configDirArg) process.env.BOSUN_DIR = configDirArg;
992
994
  const { runSetup } = await import("./setup.mjs");
993
995
  await runSetup();
994
996
  process.exit(0);
995
997
  }
996
998
 
999
+ // Handle --setup (web wizard — default)
1000
+ if (args.includes("--setup") || args.includes("setup")) {
1001
+ const configDirArg = getArgValue("--config-dir");
1002
+ if (configDirArg) process.env.BOSUN_DIR = configDirArg;
1003
+ const { startSetupServer } = await import("./setup-web-server.mjs");
1004
+ await startSetupServer();
1005
+ // Server keeps running until setup completes
1006
+ }
1007
+
997
1008
  // Handle --whatsapp-auth
998
1009
  if (args.includes("--whatsapp-auth") || args.includes("whatsapp-auth")) {
999
1010
  const mode = args.includes("--pairing-code") ? "pairing-code" : "qr";
@@ -1007,13 +1018,13 @@ async function main() {
1007
1018
  if (!IS_DAEMON_CHILD) {
1008
1019
  const { shouldRunSetup } = await import("./setup.mjs");
1009
1020
  if (shouldRunSetup()) {
1010
- console.log("\n 🚀 First run detected — launching setup wizard...\n");
1011
1021
  const configDirArg = getArgValue("--config-dir");
1012
1022
  if (configDirArg) {
1013
1023
  process.env.BOSUN_DIR = configDirArg;
1014
1024
  }
1015
- const { runSetup } = await import("./setup.mjs");
1016
- await runSetup();
1025
+ console.log("\n 🚀 First run detected launching setup wizard...\n");
1026
+ const { startSetupServer } = await import("./setup-web-server.mjs");
1027
+ await startSetupServer();
1017
1028
  console.log("\n Setup complete! Starting bosun...\n");
1018
1029
  }
1019
1030
  }
package/config.mjs CHANGED
@@ -17,6 +17,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { resolve, dirname, basename, relative, isAbsolute } from "node:path";
18
18
  import { execSync } from "node:child_process";
19
19
  import { fileURLToPath } from "node:url";
20
+ import os from "node:os";
20
21
  import { resolveAgentSdkConfig } from "./agent-sdk.mjs";
21
22
  import {
22
23
  ensureAgentPromptWorkspace,
@@ -60,6 +61,10 @@ function isWslInteropRuntime() {
60
61
  }
61
62
 
62
63
  function resolveConfigDir(repoRoot) {
64
+ // 1. Explicit env override
65
+ if (process.env.BOSUN_DIR) return resolve(process.env.BOSUN_DIR);
66
+
67
+ // 2. Platform-aware user home
63
68
  const preferWindowsDirs =
64
69
  process.platform === "win32" && !isWslInteropRuntime();
65
70
  const baseDir = preferWindowsDirs
@@ -67,13 +72,13 @@ function resolveConfigDir(repoRoot) {
67
72
  process.env.LOCALAPPDATA ||
68
73
  process.env.USERPROFILE ||
69
74
  process.env.HOME ||
70
- process.cwd()
75
+ os.homedir()
71
76
  : process.env.HOME ||
72
77
  process.env.XDG_CONFIG_HOME ||
73
78
  process.env.USERPROFILE ||
74
79
  process.env.APPDATA ||
75
80
  process.env.LOCALAPPDATA ||
76
- process.cwd();
81
+ os.homedir();
77
82
  return resolve(baseDir, "bosun");
78
83
  }
79
84
 
@@ -353,7 +358,11 @@ function detectRepoRoot() {
353
358
  }
354
359
  }
355
360
 
356
- // 5. Final fallback to cwd
361
+ // 5. Final fallback — warn and return cwd. This is unlikely to be a valid
362
+ // git repo (e.g. when the daemon spawns with cwd=homedir), but returning
363
+ // null would crash downstream callers like resolve(repoRoot). The warning
364
+ // helps diagnose "not a git repository" errors from child processes.
365
+ console.warn("[config] detectRepoRoot: no git repository found — falling back to cwd:", process.cwd());
357
366
  return process.cwd();
358
367
  }
359
368
 
@@ -42,6 +42,7 @@ function ensureElectronInstalled() {
42
42
  const result = spawnSync("npm", ["install"], {
43
43
  cwd: desktopDir,
44
44
  stdio: "inherit",
45
+ shell: process.platform === "win32",
45
46
  env: process.env,
46
47
  });
47
48
  return result.status === 0 && existsSync(electronBin);
@@ -60,6 +61,7 @@ function launch() {
60
61
 
61
62
  const child = spawn(electronBin, args, {
62
63
  stdio: "inherit",
64
+ shell: process.platform === "win32",
63
65
  env: {
64
66
  ...process.env,
65
67
  BOSUN_DESKTOP: "1",
package/monitor.mjs CHANGED
@@ -12703,12 +12703,14 @@ if (isContainerEnabled()) {
12703
12703
  // ── Start PR Cleanup Daemon ──────────────────────────────────────────────────
12704
12704
  // Automatically resolves PR conflicts and CI failures every 30 minutes
12705
12705
  if (config.prCleanupEnabled !== false) {
12706
- console.log("[monitor] Starting PR cleanup daemon...");
12706
+ const prRepoRoot = effectiveRepoRoot || repoRoot || process.cwd();
12707
+ console.log(`[monitor] Starting PR cleanup daemon (repoRoot: ${prRepoRoot})...`);
12707
12708
  prCleanupDaemon = new PRCleanupDaemon({
12708
12709
  intervalMs: 30 * 60 * 1000, // 30 minutes
12709
12710
  maxConcurrentCleanups: 3,
12710
12711
  dryRun: false,
12711
12712
  autoMerge: true,
12713
+ repoRoot: prRepoRoot,
12712
12714
  });
12713
12715
  prCleanupDaemon.start();
12714
12716
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.29.8",
3
+ "version": "0.31.0",
4
4
  "description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache 2.0",
@@ -158,6 +158,7 @@
158
158
  "sdk-conflict-resolver.mjs",
159
159
  "session-tracker.mjs",
160
160
  "setup.mjs",
161
+ "setup-web-server.mjs",
161
162
  "pwsh-runtime.mjs",
162
163
  "shared-knowledge.mjs",
163
164
  "shared-state-manager.mjs",
@@ -23,9 +23,9 @@ const exec = promisify(execCallback);
23
23
  * Check if a branch is already checked out in an existing git worktree.
24
24
  * Returns the worktree path if claimed, or null if free.
25
25
  */
26
- async function getWorktreeForBranch(branch) {
26
+ async function getWorktreeForBranch(branch, repoRoot) {
27
27
  try {
28
- const { stdout } = await exec(`git worktree list --porcelain`);
28
+ const { stdout } = await exec(`git worktree list --porcelain`, { cwd: repoRoot });
29
29
  // Each worktree block is separated by a blank line.
30
30
  // Look for a line like: branch refs/heads/<branch>
31
31
  const blocks = stdout.split(/\n\n/);
@@ -64,6 +64,7 @@ class PRCleanupDaemon {
64
64
  ...CONFIG,
65
65
  ...(config && typeof config === "object" ? config : {}),
66
66
  };
67
+ this.repoRoot = this.config.repoRoot || process.cwd();
67
68
  this.cleanupQueue = [];
68
69
  this.activeCleanups = new Map(); // pr# → cleanup state
69
70
  this.lastRunStartedAt = 0;
@@ -157,6 +158,7 @@ class PRCleanupDaemon {
157
158
  try {
158
159
  const { stdout } = await exec(
159
160
  `gh pr list --json number,title,mergeable,labels,statusCheckRollup,headRefName --limit 50`,
161
+ { cwd: this.repoRoot },
160
162
  );
161
163
  const allPRs = JSON.parse(stdout);
162
164
 
@@ -377,10 +379,10 @@ class PRCleanupDaemon {
377
379
  tmpDir = await mkdtemp(join(tmpdir(), "pr-merge-"));
378
380
 
379
381
  // Fetch all relevant refs
380
- await exec(`git fetch origin ${pr.headRefName} main`);
382
+ await exec(`git fetch origin ${pr.headRefName} main`, { cwd: this.repoRoot });
381
383
 
382
384
  // Guard: skip if the branch is already claimed by another worktree
383
- const existingWt = await getWorktreeForBranch(pr.headRefName);
385
+ const existingWt = await getWorktreeForBranch(pr.headRefName, this.repoRoot);
384
386
  if (existingWt) {
385
387
  console.warn(
386
388
  `[pr-cleanup-daemon] WARN: Branch "${pr.headRefName}" is in an active worktree at ${existingWt} — skipping conflict resolution`,
@@ -391,6 +393,7 @@ class PRCleanupDaemon {
391
393
  // Create worktree on the PR branch
392
394
  await exec(
393
395
  `git worktree add "${tmpDir}" "origin/${pr.headRefName}" --detach`,
396
+ { cwd: this.repoRoot },
394
397
  );
395
398
  await exec(
396
399
  `git checkout -B "${pr.headRefName}" "origin/${pr.headRefName}"`,
@@ -455,13 +458,13 @@ class PRCleanupDaemon {
455
458
  } finally {
456
459
  if (tmpDir) {
457
460
  try {
458
- await exec(`git worktree remove "${tmpDir}" --force`);
461
+ await exec(`git worktree remove "${tmpDir}" --force`, { cwd: this.repoRoot });
459
462
  } catch {
460
463
  try {
461
464
  await rm(tmpDir, { recursive: true, force: true });
462
465
  } catch {}
463
466
  try {
464
- await exec(`git worktree prune`);
467
+ await exec(`git worktree prune`, { cwd: this.repoRoot });
465
468
  } catch {}
466
469
  }
467
470
  }
@@ -499,10 +502,10 @@ class PRCleanupDaemon {
499
502
  tmpDir = await mkdtemp(join(tmpdir(), "pr-cleanup-"));
500
503
 
501
504
  // Fetch latest refs first
502
- await exec(`git fetch origin ${pr.headRefName}`);
505
+ await exec(`git fetch origin ${pr.headRefName}`, { cwd: this.repoRoot });
503
506
 
504
507
  // Guard: skip if the branch is already claimed by another worktree
505
- const existingWt = await getWorktreeForBranch(pr.headRefName);
508
+ const existingWt = await getWorktreeForBranch(pr.headRefName, this.repoRoot);
506
509
  if (existingWt) {
507
510
  console.warn(
508
511
  `[pr-cleanup-daemon] WARN: Branch "${pr.headRefName}" is in an active worktree at ${existingWt} — skipping CI re-trigger`,
@@ -513,6 +516,7 @@ class PRCleanupDaemon {
513
516
  // Create a temporary worktree for the PR branch
514
517
  await exec(
515
518
  `git worktree add "${tmpDir}" "origin/${pr.headRefName}" --detach`,
519
+ { cwd: this.repoRoot },
516
520
  );
517
521
 
518
522
  // Checkout the branch properly inside the worktree
@@ -538,14 +542,14 @@ class PRCleanupDaemon {
538
542
  // Clean up the temporary worktree
539
543
  if (tmpDir) {
540
544
  try {
541
- await exec(`git worktree remove "${tmpDir}" --force`);
545
+ await exec(`git worktree remove "${tmpDir}" --force`, { cwd: this.repoRoot });
542
546
  } catch {
543
547
  // If worktree remove fails, try manual cleanup
544
548
  try {
545
549
  await rm(tmpDir, { recursive: true, force: true });
546
550
  } catch {}
547
551
  try {
548
- await exec(`git worktree prune`);
552
+ await exec(`git worktree prune`, { cwd: this.repoRoot });
549
553
  } catch {}
550
554
  }
551
555
  }
@@ -605,7 +609,7 @@ class PRCleanupDaemon {
605
609
  }
606
610
 
607
611
  try {
608
- await exec(`gh pr merge ${pr.number} --auto --squash --delete-branch`);
612
+ await exec(`gh pr merge ${pr.number} --auto --squash --delete-branch`, { cwd: this.repoRoot });
609
613
  this.stats.autoMerges++;
610
614
  console.log(`[pr-cleanup-daemon] ✓ Auto-merged PR #${pr.number}`);
611
615
  } catch (err) {
@@ -626,7 +630,7 @@ class PRCleanupDaemon {
626
630
  try {
627
631
  // Use GitHub API to get the list of changed files and estimate conflict scope
628
632
  // This avoids the need for local checkout entirely
629
- const { stdout } = await exec(`gh pr diff ${pr.number} --name-only`);
633
+ const { stdout } = await exec(`gh pr diff ${pr.number} --name-only`, { cwd: this.repoRoot });
630
634
  const changedFiles = stdout.trim().split("\n").filter(Boolean);
631
635
 
632
636
  // Estimate: each changed file could have ~10 lines of conflicts on average
@@ -641,8 +645,8 @@ class PRCleanupDaemon {
641
645
  let tmpDir;
642
646
  try {
643
647
  tmpDir = await mkdtemp(join(tmpdir(), "pr-conflict-"));
644
- await exec(`git fetch origin ${pr.headRefName} main`);
645
- await exec(`git worktree add "${tmpDir}" "origin/main" --detach`);
648
+ await exec(`git fetch origin ${pr.headRefName} main`, { cwd: this.repoRoot });
649
+ await exec(`git worktree add "${tmpDir}" "origin/main" --detach`, { cwd: this.repoRoot });
646
650
 
647
651
  // Attempt merge to count conflicts
648
652
  try {
@@ -675,13 +679,13 @@ class PRCleanupDaemon {
675
679
  } finally {
676
680
  if (tmpDir) {
677
681
  try {
678
- await exec(`git worktree remove "${tmpDir}" --force`);
682
+ await exec(`git worktree remove "${tmpDir}" --force`, { cwd: this.repoRoot });
679
683
  } catch {
680
684
  try {
681
685
  await rm(tmpDir, { recursive: true, force: true });
682
686
  } catch {}
683
687
  try {
684
- await exec(`git worktree prune`);
688
+ await exec(`git worktree prune`, { cwd: this.repoRoot });
685
689
  } catch {}
686
690
  }
687
691
  }
@@ -704,6 +708,7 @@ class PRCleanupDaemon {
704
708
  const child = spawn("node", args, {
705
709
  stdio: "inherit",
706
710
  env: process.env,
711
+ cwd: this.repoRoot,
707
712
  });
708
713
 
709
714
  child.on("exit", (code) => {
@@ -726,6 +731,7 @@ class PRCleanupDaemon {
726
731
  try {
727
732
  const { stdout } = await exec(
728
733
  `gh pr list --json number,title,mergeable,statusCheckRollup,headRefName,autoMergeRequest --limit 30`,
734
+ { cwd: this.repoRoot },
729
735
  );
730
736
  const allPRs = JSON.parse(stdout);
731
737
 
@@ -767,6 +773,7 @@ class PRCleanupDaemon {
767
773
  try {
768
774
  await exec(
769
775
  `gh pr merge ${pr.number} --auto --squash --delete-branch`,
776
+ { cwd: this.repoRoot },
770
777
  );
771
778
  console.log(
772
779
  `[pr-cleanup-daemon] ⏳ Auto-merge queued for PR #${pr.number} (CI pending)`,
@@ -787,7 +794,7 @@ class PRCleanupDaemon {
787
794
 
788
795
  // All green + mergeable → merge now
789
796
  try {
790
- await exec(`gh pr merge ${pr.number} --squash --delete-branch`);
797
+ await exec(`gh pr merge ${pr.number} --squash --delete-branch`, { cwd: this.repoRoot });
791
798
  this.stats.autoMerges++;
792
799
  console.log(
793
800
  `[pr-cleanup-daemon] ✅ Auto-merged green PR #${pr.number}: ${pr.title}`,
@@ -797,6 +804,7 @@ class PRCleanupDaemon {
797
804
  try {
798
805
  await exec(
799
806
  `gh pr merge ${pr.number} --auto --squash --delete-branch`,
807
+ { cwd: this.repoRoot },
800
808
  );
801
809
  console.log(
802
810
  `[pr-cleanup-daemon] ⏳ Auto-merge enabled for PR #${pr.number}`,
@@ -854,6 +862,7 @@ class PRCleanupDaemon {
854
862
  try {
855
863
  const { stdout } = await exec(
856
864
  `gh pr view ${prNumber} --json mergeable,statusCheckRollup`,
865
+ { cwd: this.repoRoot },
857
866
  );
858
867
  return JSON.parse(stdout);
859
868
  } catch (err) {