bosun 0.26.6 → 0.27.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/README.md CHANGED
@@ -1,19 +1,19 @@
1
- # @virtengine/bosun
1
+ # bosun
2
2
 
3
- **Bosun** is a production-grade supervisor for AI coding agents. It routes tasks across executors, manages retries and failover, automates PR lifecycle, and keeps you in control through Telegram (with optional WhatsApp and container isolation).
3
+ Bosun is a production-grade supervisor for AI coding agents. It routes tasks across executors, automates PR lifecycles, and keeps operators in control through Telegram, the Mini App dashboard, and optional WhatsApp notifications.
4
4
 
5
- [Website](https://bosun.virtengine.com) · [Docs](https://bosun.virtengine.com/docs/) · [GitHub](https://github.com/virtengine/virtengine/tree/main/scripts/bosun) · [npm](https://www.npmjs.com/package/@virtengine/bosun) · [Issues](https://github.com/virtengine/virtengine/issues)
5
+ [Website](https://bosun.virtengine.com) · [Docs](https://bosun.virtengine.com/docs/) · [GitHub](https://github.com/virtengine/bosun?tab=readme-ov-file#bosun) · [npm](https://www.npmjs.com/package/bosun) · [Issues](https://github.com/virtengine/bosun/issues)
6
6
 
7
- ![CI](https://github.com/virtengine/virtengine/actions/workflows/ci.yaml/badge.svg)
8
- ![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)
9
- ![npm](https://img.shields.io/npm/v/@virtengine/bosun.svg)
7
+ [![CI](https://github.com/virtengine/bosun/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/virtengine/bosun/actions/workflows/ci.yaml)
8
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
9
+ [![npm](https://img.shields.io/npm/v/bosun.svg)](https://www.npmjs.com/package/bosun)
10
10
 
11
11
  ---
12
12
 
13
13
  ## Quick start
14
14
 
15
15
  ```bash
16
- npm install -g @virtengine/bosun
16
+ npm install -g bosun
17
17
  cd your-repo
18
18
  bosun
19
19
  ```
@@ -46,7 +46,7 @@ Requires:
46
46
 
47
47
  **Published docs (website):** https://bosun.virtengine.com/docs/
48
48
 
49
- **Source docs (markdown):** `_docs/` is the source of truth for long-form documentation. Keep `site/docs` in sync with these markdown files so the website mirrors the same content.
49
+ **Source docs (markdown):** `_docs/` is the source of truth for long-form documentation. The website should be generated from the same markdown content so docs stay in sync.
50
50
 
51
51
  Key references:
52
52
  - [GitHub adapter enhancements](_docs/KANBAN_GITHUB_ENHANCEMENT.md)
@@ -78,6 +78,9 @@ npm -C scripts/bosun test
78
78
 
79
79
  # Prepublish safety checks
80
80
  npm -C scripts/bosun run prepublishOnly
81
+
82
+ # Install local git hooks (pre-commit + pre-push)
83
+ npm -C scripts/bosun run hooks:install
81
84
  ```
82
85
 
83
86
  ---
@@ -187,14 +187,17 @@ async function run() {
187
187
  for (const item of mapped) {
188
188
  const context = {
189
189
  sdk: agent,
190
- taskId: process.env.VE_TASK_ID || "",
191
- taskTitle: process.env.VE_TASK_TITLE || "",
190
+ taskId: process.env.VE_TASK_ID || process.env.BOSUN_TASK_ID || "",
191
+ taskTitle:
192
+ process.env.VE_TASK_TITLE || process.env.BOSUN_TASK_TITLE || "",
192
193
  taskDescription:
193
194
  process.env.VE_TASK_DESCRIPTION ||
195
+ process.env.BOSUN_TASK_DESCRIPTION ||
194
196
  process.env.VE_DESCRIPTION ||
195
197
  process.env.VK_DESCRIPTION ||
196
198
  "",
197
- branch: process.env.VE_BRANCH_NAME || "",
199
+ branch:
200
+ process.env.VE_BRANCH_NAME || process.env.BOSUN_BRANCH_NAME || "",
198
201
  worktreePath: process.cwd(),
199
202
  extra: {
200
203
  source_event: sourceEvent,
package/agent-hooks.mjs CHANGED
@@ -817,6 +817,15 @@ function _buildEnv(ctx) {
817
817
  VE_SDK: ctx.sdk ?? "",
818
818
  VE_REPO_ROOT: ctx.repoRoot ?? REPO_ROOT,
819
819
  VE_HOOK_BLOCKING: "false", // Overridden per-hook in execution
820
+ // ── Bosun canonical aliases (always set so agents see a consistent set) ──
821
+ BOSUN_TASK_ID: ctx.taskId ?? "",
822
+ BOSUN_TASK_TITLE: ctx.taskTitle ?? "",
823
+ BOSUN_TASK_DESCRIPTION: ctx.taskDescription ?? "",
824
+ BOSUN_BRANCH_NAME: ctx.branch ?? "",
825
+ BOSUN_WORKTREE_PATH: ctx.worktreePath ?? "",
826
+ BOSUN_SDK: ctx.sdk ?? "",
827
+ BOSUN_MANAGED: "1",
828
+ VE_MANAGED: "1",
820
829
  };
821
830
 
822
831
  // Merge any extra context values as env vars
package/agent-prompts.mjs CHANGED
@@ -227,6 +227,32 @@ You are the always-on reliability guardian for bosun in devmode.
227
227
  - No placeholders/stubs/TODO-only output.
228
228
  - Keep behavior stable and production-safe.
229
229
 
230
+ ## Bosun Task Agent — Git & PR Workflow
231
+
232
+ You are running as a **Bosun-managed task agent**. Environment variables
233
+ \`BOSUN_TASK_TITLE\`, \`BOSUN_BRANCH_NAME\`, \`BOSUN_TASK_ID\`, and their
234
+ \`VE_*\` / \`VK_*\` aliases are available in your environment.
235
+
236
+ **Before committing:**
237
+ - Run auto-formatting tools (gofmt, prettier, etc.) relevant to changed files.
238
+ - Fix any lint or vet warnings introduced by your changes.
239
+
240
+ **After committing:**
241
+ - If a precommit hook auto-applies additional formatting changes, add those
242
+ to a follow-up commit before pushing.
243
+ - Merge any upstream changes from the base branch before pushing:
244
+ \`git fetch origin && git merge origin/<base-branch> --no-edit\`
245
+ Resolve any conflicts that arise.
246
+ - Push: \`git push --set-upstream origin {{BRANCH}}\`
247
+ - After a successful push, open a Pull Request:
248
+ \`gh pr create --title "{{TASK_TITLE}}" --body "Closes task {{TASK_ID}}"\`
249
+ - **Do NOT** run \`gh pr merge\` — the orchestrator handles merges after CI.
250
+
251
+ **Do NOT:**
252
+ - Bypass pre-push hooks (\`git push --no-verify\` is forbidden).
253
+ - Use \`git add .\` — stage files individually.
254
+ - Wait for user confirmation before pushing or opening the PR.
255
+
230
256
  ## Agent Status Endpoint
231
257
  - URL: http://127.0.0.1:{{ENDPOINT_PORT}}/api/tasks/{{TASK_ID}}
232
258
  - POST /status {"status":"inreview"} after PR-ready push
package/autofix.mjs CHANGED
@@ -348,12 +348,36 @@ export function isDevMode() {
348
348
  return false;
349
349
  }
350
350
 
351
- // Check for monorepo markers (source repo)
352
- const repoRoot = resolve(__dirname, "..", "..");
351
+ // Check for bosun repo markers (standalone repo)
352
+ let repoCursor = resolve(__dirname);
353
+ for (let i = 0; i < 5; i += 1) {
354
+ const pkgPath = resolve(repoCursor, "package.json");
355
+ if (existsSync(pkgPath)) {
356
+ try {
357
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
358
+ if (pkg?.name === "bosun" && existsSync(resolve(repoCursor, "monitor.mjs"))) {
359
+ _devModeCache = true;
360
+ return true;
361
+ }
362
+ } catch {
363
+ /* ignore */
364
+ }
365
+ }
366
+ repoCursor = resolve(repoCursor, "..");
367
+ }
368
+
369
+ // Check for monorepo markers (source repo). Walk up a few levels to
370
+ // handle scripts/bosun -> repo root layout.
353
371
  const monoRepoMarkers = ["go.mod", "Makefile", "AGENTS.md", "x"];
354
- const isMonoRepo = monoRepoMarkers.some((m) =>
355
- existsSync(resolve(repoRoot, m)),
356
- );
372
+ let cursor = resolve(__dirname);
373
+ let isMonoRepo = false;
374
+ for (let i = 0; i < 5; i += 1) {
375
+ if (monoRepoMarkers.some((m) => existsSync(resolve(cursor, m)))) {
376
+ isMonoRepo = true;
377
+ break;
378
+ }
379
+ cursor = resolve(cursor, "..");
380
+ }
357
381
 
358
382
  _devModeCache = isMonoRepo;
359
383
  return isMonoRepo;
package/cli.mjs CHANGED
@@ -178,7 +178,7 @@ function showHelp() {
178
178
  bosun --no-codex --no-autofix # minimal mode
179
179
 
180
180
  DOCS
181
- https://www.npmjs.com/package/@virtengine/bosun
181
+ https://www.npmjs.com/package/bosun
182
182
  `);
183
183
  }
184
184
 
package/monitor.mjs CHANGED
@@ -6315,7 +6315,7 @@ function resolveUpstreamFromTask(task) {
6315
6315
  if (
6316
6316
  text.includes("bosun") ||
6317
6317
  text.includes("codex monitor") ||
6318
- text.includes("@virtengine/bosun") ||
6318
+ text.includes("bosun") ||
6319
6319
  text.includes("scripts/bosun")
6320
6320
  ) {
6321
6321
  return DEFAULT_BOSUN_UPSTREAM;
package/package.json CHANGED
@@ -1,26 +1,25 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.26.6",
3
+ "version": "0.27.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",
7
- "author": "VirtEngine <dev@virtengine.com>",
8
- "homepage": "https://github.com/virtengine/bosun",
7
+ "author": "VirtEngine Maintainers <maintainers@virtengine.com>",
8
+ "homepage": "https://bosun.virtengine.com",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/virtengine/virtengine.git",
12
- "directory": "scripts/bosun"
11
+ "url": "git+https://github.com/virtengine/bosun.git"
13
12
  },
14
13
  "bugs": {
15
- "url": "https://github.com/virtengine/virtengine/issues"
14
+ "url": "https://github.com/virtengine/bosun/issues"
16
15
  },
17
16
  "keywords": [
17
+ "bosun",
18
18
  "codex",
19
19
  "orchestrator",
20
20
  "monitor",
21
21
  "ai",
22
22
  "automation",
23
- "vibe-kanban",
24
23
  "telegram",
25
24
  "devops",
26
25
  "executor",
package/postinstall.mjs CHANGED
@@ -12,6 +12,8 @@
12
12
  */
13
13
 
14
14
  import { execSync } from "node:child_process";
15
+ import { existsSync } from "node:fs";
16
+ import { resolve } from "node:path";
15
17
 
16
18
  const isWin = process.platform === "win32";
17
19
 
@@ -182,6 +184,18 @@ function main() {
182
184
  console.log(" bosun Start with existing config");
183
185
  console.log(" bosun --help See all options");
184
186
  console.log("");
187
+
188
+ // Auto-install git hooks when inside the repo and hooks are present.
189
+ try {
190
+ if (process.env.BOSUN_SKIP_GIT_HOOKS) return;
191
+ const cwd = process.cwd();
192
+ const hooksDir = resolve(cwd, ".githooks");
193
+ if (existsSync(resolve(cwd, ".git")) && existsSync(hooksDir)) {
194
+ execSync("git config core.hooksPath .githooks", { stdio: "ignore" });
195
+ }
196
+ } catch {
197
+ // Non-blocking; hooks can be installed via `npm run hooks:install`
198
+ }
185
199
  }
186
200
 
187
201
  main();
package/publish.mjs CHANGED
@@ -225,9 +225,9 @@ function main() {
225
225
  if (status === 0 && !dryRun) {
226
226
  console.log(
227
227
  "\n[publish] REMINDER: deprecate the legacy npm package to redirect users:\n" +
228
- " npm deprecate bosun@'*' \"Renamed to @virtengine/bosun. Install: npm install -g @virtengine/bosun\"\n" +
228
+ " npm deprecate bosun@'*' \"Renamed to bosun. Install: npm install -g bosun\"\n" +
229
229
  " # If a scoped legacy package exists:\n" +
230
- " npm deprecate @virtengine/bosun@'*' \"Renamed to @virtengine/bosun. Install: npm install -g @virtengine/bosun\"\n",
230
+ " npm deprecate bosun@'*' \"Renamed to bosun. Install: npm install -g bosun\"\n",
231
231
  );
232
232
  }
233
233
  process.exit(status);
@@ -506,6 +506,19 @@ export class SessionTracker {
506
506
  this.#markDirty(sessionId);
507
507
  }
508
508
 
509
+ /**
510
+ * Rename a session (update its title).
511
+ * @param {string} sessionId
512
+ * @param {string} newTitle
513
+ */
514
+ renameSession(sessionId, newTitle) {
515
+ const session = this.#sessions.get(sessionId);
516
+ if (!session) return;
517
+ session.taskTitle = newTitle;
518
+ session.title = newTitle;
519
+ this.#markDirty(sessionId);
520
+ }
521
+
509
522
  /**
510
523
  * Flush all dirty sessions to disk immediately.
511
524
  */
package/setup.mjs CHANGED
@@ -16,7 +16,7 @@
16
16
  * Usage:
17
17
  * bosun --setup # interactive wizard
18
18
  * bosun-setup # same (bin alias)
19
- * npx @virtengine/bosun setup
19
+ * npx bosun setup
20
20
  * node setup.mjs --non-interactive # use env vars, skip prompts
21
21
  */
22
22
 
@@ -1724,7 +1724,7 @@ async function main() {
1724
1724
  const hasVk = check(
1725
1725
  "Vibe-Kanban CLI",
1726
1726
  commandExists("vibe-kanban") || bundledBinExists("vibe-kanban"),
1727
- "Bundled with @virtengine/bosun as a dependency",
1727
+ "Bundled with bosun as a dependency",
1728
1728
  );
1729
1729
 
1730
1730
  if (!hasVk) {
@@ -1732,8 +1732,8 @@ async function main() {
1732
1732
  "vibe-kanban not found. This is bundled with bosun, so this is unexpected.",
1733
1733
  );
1734
1734
  info("Try reinstalling:");
1735
- console.log(" npm uninstall -g @virtengine/bosun");
1736
- console.log(" npm install -g @virtengine/bosun\n");
1735
+ console.log(" npm uninstall -g bosun");
1736
+ console.log(" npm install -g bosun\n");
1737
1737
  }
1738
1738
 
1739
1739
  if (!hasNode) {
@@ -699,7 +699,7 @@ function generateSystemdUnit({ daemon = false } = {}) {
699
699
 
700
700
  return `[Unit]
701
701
  Description=bosun — AI Orchestrator Supervisor
702
- Documentation=https://www.npmjs.com/package/@virtengine/bosun
702
+ Documentation=https://www.npmjs.com/package/bosun
703
703
  After=network-online.target
704
704
  Wants=network-online.target
705
705
 
package/task-executor.mjs CHANGED
@@ -2710,11 +2710,16 @@ class TaskExecutor {
2710
2710
  async executeTask(task, options = {}) {
2711
2711
  const taskId = task.id || task.task_id;
2712
2712
  const taskTitle = task.title || "(untitled)";
2713
- if (this._paused) {
2713
+ if (this._paused && !options?.force) {
2714
2714
  console.log(
2715
2715
  `${TAG} executor paused — skipping task "${taskTitle}" (${taskId})`,
2716
2716
  );
2717
- return;
2717
+ return { skipped: true, reason: "paused" };
2718
+ }
2719
+ if (this._paused && options?.force) {
2720
+ console.log(
2721
+ `${TAG} executor paused but force=true — executing task "${taskTitle}" (${taskId})`,
2722
+ );
2718
2723
  }
2719
2724
  if (this._isBaseBranchLimitReached(task)) {
2720
2725
  const baseBranch = this._resolveTaskBaseBranch(task);
@@ -3073,7 +3078,39 @@ class TaskExecutor {
3073
3078
  `${TAG} executing task "${taskTitle}" in ${wt.path} on branch ${branch} (sdk=${resolvedSdk})`,
3074
3079
  );
3075
3080
 
3076
- // 6a. Start session tracking for review handoff
3081
+ // 6a. Inject task context env vars so spawned agents (Codex/Copilot/Claude)
3082
+ // inherit the full Bosun task context regardless of which env-var naming
3083
+ // convention they read (VE_*, VK_*, or BOSUN_*). We save and restore to
3084
+ // avoid polluting the parent process when running multiple parallel tasks.
3085
+ const _savedEnvKeys = [
3086
+ "VE_TASK_ID", "VE_TASK_TITLE", "VE_TASK_DESCRIPTION",
3087
+ "VE_BRANCH_NAME", "VE_WORKTREE_PATH", "VE_SDK", "VE_MANAGED",
3088
+ "VK_TITLE", "VK_DESCRIPTION",
3089
+ "BOSUN_TASK_ID", "BOSUN_TASK_TITLE", "BOSUN_TASK_DESCRIPTION",
3090
+ "BOSUN_BRANCH_NAME", "BOSUN_WORKTREE_PATH", "BOSUN_SDK", "BOSUN_MANAGED",
3091
+ ];
3092
+ const _savedEnv = {};
3093
+ for (const k of _savedEnvKeys) _savedEnv[k] = process.env[k];
3094
+
3095
+ // Set both naming conventions so any agent instruction set detects them.
3096
+ process.env.VE_TASK_ID = taskId;
3097
+ process.env.VE_TASK_TITLE = taskTitle;
3098
+ process.env.VE_TASK_DESCRIPTION = String(task.description || task.body || "");
3099
+ process.env.VE_BRANCH_NAME = branch;
3100
+ process.env.VE_WORKTREE_PATH = wt.path;
3101
+ process.env.VE_SDK = resolvedSdk;
3102
+ process.env.VE_MANAGED = "1";
3103
+ process.env.VK_TITLE = taskTitle;
3104
+ process.env.VK_DESCRIPTION = String(task.description || task.body || "");
3105
+ process.env.BOSUN_TASK_ID = taskId;
3106
+ process.env.BOSUN_TASK_TITLE = taskTitle;
3107
+ process.env.BOSUN_TASK_DESCRIPTION = String(task.description || task.body || "");
3108
+ process.env.BOSUN_BRANCH_NAME = branch;
3109
+ process.env.BOSUN_WORKTREE_PATH = wt.path;
3110
+ process.env.BOSUN_SDK = resolvedSdk;
3111
+ process.env.BOSUN_MANAGED = "1";
3112
+
3113
+ // 6b. Start session tracking for review handoff
3077
3114
  const sessionTracker = getSessionTracker();
3078
3115
  sessionTracker.startSession(taskId, taskTitle);
3079
3116
 
@@ -3097,6 +3134,16 @@ class TaskExecutor {
3097
3134
  },
3098
3135
  });
3099
3136
 
3137
+ // Restore env vars that were injected for this task slot so parallel
3138
+ // tasks running in the same process don't see stale values.
3139
+ for (const k of _savedEnvKeys) {
3140
+ if (_savedEnv[k] === undefined) {
3141
+ delete process.env[k];
3142
+ } else {
3143
+ process.env[k] = _savedEnv[k];
3144
+ }
3145
+ }
3146
+
3100
3147
  // Track attempts on task for PR body
3101
3148
  task._executionResult = result;
3102
3149
 
package/telegram-bot.mjs CHANGED
@@ -780,7 +780,7 @@ let agentChatId = null; // chat where agent is running
780
780
  // ── Sticky UI menu state (keep /menu accessible at bottom) ─────────────────
781
781
  const stickyMenuState = new Map();
782
782
  const stickyMenuTimers = new Map();
783
- const STICKY_MENU_BUMP_MS = 1200;
783
+ const STICKY_MENU_BUMP_MS = 200;
784
784
 
785
785
  // ── Queues ──────────────────────────────────────────────────────────────────
786
786
 
@@ -1876,6 +1876,48 @@ async function handleCallbackQuery(query) {
1876
1876
  enqueueCommand(() => cmdResumeTasks(chatId));
1877
1877
  return;
1878
1878
  }
1879
+ if (data === "cb:close_menu") {
1880
+ // Close and disable sticky menu
1881
+ const state = stickyMenuState.get(chatId);
1882
+ if (state?.enabled) {
1883
+ // Cancel any pending bump timer
1884
+ const timer = stickyMenuTimers.get(chatId);
1885
+ if (timer) {
1886
+ clearTimeout(timer);
1887
+ stickyMenuTimers.delete(chatId);
1888
+ }
1889
+ // Delete the sticky menu message
1890
+ if (state.messageId) {
1891
+ await deleteDirect(chatId, state.messageId).catch(() => {});
1892
+ }
1893
+ // Disable sticky state
1894
+ stickyMenuState.delete(chatId);
1895
+ } else if (query.message?.message_id) {
1896
+ // Not sticky — just delete the message
1897
+ await deleteDirect(chatId, query.message.message_id).catch(() => {});
1898
+ }
1899
+ return;
1900
+ }
1901
+ if (data === "cb:toggle_menu") {
1902
+ // Toggle sticky menu on/off
1903
+ const state = stickyMenuState.get(chatId);
1904
+ if (state?.enabled) {
1905
+ // Menu is open → close it
1906
+ const timer = stickyMenuTimers.get(chatId);
1907
+ if (timer) {
1908
+ clearTimeout(timer);
1909
+ stickyMenuTimers.delete(chatId);
1910
+ }
1911
+ if (state.messageId) {
1912
+ await deleteDirect(chatId, state.messageId).catch(() => {});
1913
+ }
1914
+ stickyMenuState.delete(chatId);
1915
+ } else {
1916
+ // Menu is closed → open it
1917
+ enqueueCommand(() => cmdMenu(chatId));
1918
+ }
1919
+ return;
1920
+ }
1879
1921
  if (data === "cb:confirm_restart") {
1880
1922
  await sendReply(
1881
1923
  chatId,
@@ -3385,11 +3427,15 @@ async function showStartTaskModelPicker(chatId, taskId, sdk, executor) {
3385
3427
 
3386
3428
  function uiNavRow(parent) {
3387
3429
  if (!parent) {
3388
- return [uiButton("🏠 Home", uiGoAction("home"))];
3430
+ return [
3431
+ uiButton("🏠 Home", uiGoAction("home")),
3432
+ uiButton("✖ Close", "cb:close_menu"),
3433
+ ];
3389
3434
  }
3390
3435
  return [
3391
3436
  uiButton("⬅️ Back", uiGoAction(parent)),
3392
3437
  uiButton("🏠 Home", uiGoAction("home")),
3438
+ uiButton("✖ Close", "cb:close_menu"),
3393
3439
  ];
3394
3440
  }
3395
3441
 
@@ -3623,6 +3669,7 @@ Object.assign(UI_SCREENS, {
3623
3669
  } else if (telegramUiUrl) {
3624
3670
  rows.unshift([{ text: "🌐 Open Control Center", url: getBrowserUiUrl() || telegramUiUrl }]);
3625
3671
  }
3672
+ rows.push([uiButton("✖ Close Menu", "cb:close_menu")]);
3626
3673
  return buildKeyboard(rows);
3627
3674
  },
3628
3675
  },