bosun 0.29.6 → 0.29.8

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
@@ -66,6 +66,7 @@ function showHelp() {
66
66
 
67
67
  COMMANDS
68
68
  --setup Run the interactive setup wizard
69
+ --where Show the resolved bosun config directory
69
70
  --doctor Validate bosun .env/config setup
70
71
  --help Show this help
71
72
  --version Show version
@@ -203,6 +204,48 @@ function showHelp() {
203
204
  `);
204
205
  }
205
206
 
207
+ function isWslInteropRuntime() {
208
+ return Boolean(
209
+ process.env.WSL_DISTRO_NAME ||
210
+ process.env.WSL_INTEROP ||
211
+ (process.platform === "win32" &&
212
+ String(process.env.HOME || "")
213
+ .trim()
214
+ .startsWith("/home/")),
215
+ );
216
+ }
217
+
218
+ function resolveConfigDirForCli() {
219
+ if (process.env.BOSUN_DIR) return resolve(process.env.BOSUN_DIR);
220
+ const preferWindowsDirs =
221
+ process.platform === "win32" && !isWslInteropRuntime();
222
+ const baseDir = preferWindowsDirs
223
+ ? process.env.APPDATA ||
224
+ process.env.LOCALAPPDATA ||
225
+ process.env.USERPROFILE ||
226
+ process.env.HOME ||
227
+ process.cwd()
228
+ : process.env.HOME ||
229
+ process.env.XDG_CONFIG_HOME ||
230
+ process.env.USERPROFILE ||
231
+ process.env.APPDATA ||
232
+ process.env.LOCALAPPDATA ||
233
+ process.cwd();
234
+ return resolve(baseDir, "bosun");
235
+ }
236
+
237
+ function printConfigLocations() {
238
+ const configDir = resolveConfigDirForCli();
239
+ const envPath = resolve(configDir, ".env");
240
+ const configPath = resolve(configDir, "bosun.config.json");
241
+ const workspacesPath = resolve(configDir, "workspaces");
242
+ console.log("\n Bosun config directory");
243
+ console.log(` ${configDir}`);
244
+ console.log(` .env: ${envPath}`);
245
+ console.log(` bosun.config.json: ${configPath}`);
246
+ console.log(` workspaces: ${workspacesPath}\n`);
247
+ }
248
+
206
249
  // ── Main ─────────────────────────────────────────────────────────────────────
207
250
 
208
251
  // ── Daemon Mode ──────────────────────────────────────────────────────────────
@@ -572,6 +615,12 @@ async function main() {
572
615
  process.exit(0);
573
616
  }
574
617
 
618
+ // Handle --where
619
+ if (args.includes("--where") || args.includes("where")) {
620
+ printConfigLocations();
621
+ process.exit(0);
622
+ }
623
+
575
624
  // Handle desktop shortcut controls
576
625
  if (args.includes("--desktop-shortcut")) {
577
626
  const { installDesktopShortcut, getDesktopShortcutMethodName } =
package/codex-config.mjs CHANGED
@@ -312,6 +312,51 @@ export function hasSandboxPermissions(toml) {
312
312
  return /^sandbox_permissions\s*=/m.test(toml);
313
313
  }
314
314
 
315
+ function stripSandboxPermissions(toml) {
316
+ let next = toml.replace(
317
+ /^\s*#\s*Sandbox permissions.*(?:\r?\n)?/gim,
318
+ "",
319
+ );
320
+ next = next.replace(/^\s*sandbox_permissions\s*=.*(?:\r?\n)?/gim, "");
321
+ return next;
322
+ }
323
+
324
+ function extractSandboxPermissionsValue(toml) {
325
+ const match = toml.match(/^\s*sandbox_permissions\s*=\s*(.+)$/m);
326
+ if (!match) return "";
327
+ const raw = String(match[1] || "").split("#")[0].trim();
328
+ if (!raw) return "";
329
+ if (raw.startsWith("[")) {
330
+ const values = parseTomlArrayLiteral(raw);
331
+ return values.join(",");
332
+ }
333
+ const quoted = raw.match(/^"(.*)"$/) || raw.match(/^'(.*)'$/);
334
+ if (quoted) return quoted[1];
335
+ return raw;
336
+ }
337
+
338
+ function insertTopLevelSandboxPermissions(toml, permValue) {
339
+ const block = buildSandboxPermissions(permValue).trim();
340
+ const tableIdx = toml.search(/^\s*\[/m);
341
+ if (tableIdx === -1) {
342
+ return `${toml.trimEnd()}\n\n${block}\n`;
343
+ }
344
+ const head = toml.slice(0, tableIdx).trimEnd();
345
+ const tail = toml.slice(tableIdx).trimStart();
346
+ return `${head}\n\n${block}\n\n${tail}`;
347
+ }
348
+
349
+ function ensureTopLevelSandboxPermissions(toml, envValue) {
350
+ const existingValue = extractSandboxPermissionsValue(toml);
351
+ const permValue = envValue || existingValue || "disk-full-write-access";
352
+ const stripped = stripSandboxPermissions(toml);
353
+ const updated = insertTopLevelSandboxPermissions(stripped, permValue);
354
+ return {
355
+ toml: updated,
356
+ changed: updated !== toml,
357
+ };
358
+ }
359
+
315
360
  /**
316
361
  * Build a [features] block with the recommended flags.
317
362
  * Reads environment overrides: set CODEX_FEATURES_<NAME>=false to disable.
@@ -407,16 +452,18 @@ export function ensureFeatureFlags(toml, envOverrides = process.env) {
407
452
 
408
453
  /**
409
454
  * Build the sandbox_permissions top-level key.
410
- * Default: ["disk-full-write-access"] for agentic workloads.
455
+ * Default: "disk-full-write-access" for agentic workloads.
456
+ *
457
+ * Codex CLI expects sandbox_permissions as a plain string, NOT an array.
411
458
  *
412
459
  * @param {string} [envValue] CODEX_SANDBOX_PERMISSIONS env var value
413
460
  * @returns {string} TOML line(s)
414
461
  */
415
462
  export function buildSandboxPermissions(envValue) {
416
- const perms = envValue
417
- ? envValue.split(",").map((s) => `"${s.trim()}"`)
418
- : ['"disk-full-write-access"'];
419
- return `\n# Sandbox permissions (added by bosun)\nsandbox_permissions = [${perms.join(", ")}]\n`;
463
+ const perm = envValue
464
+ ? envValue.split(",").map((s) => s.trim()).filter(Boolean).join(",")
465
+ : "disk-full-write-access";
466
+ return `\n# Sandbox permissions (added by bosun)\nsandbox_permissions = "${perm}"\n`;
420
467
  }
421
468
 
422
469
  function parseTomlArrayLiteral(raw) {
@@ -1289,10 +1336,13 @@ export function ensureCodexConfig({
1289
1336
 
1290
1337
  // ── 1e. Ensure sandbox permissions ────────────────────────
1291
1338
 
1292
- if (!hasSandboxPermissions(toml)) {
1339
+ {
1293
1340
  const envPerms = env.CODEX_SANDBOX_PERMISSIONS || "";
1294
- toml = toml.trimEnd() + "\n" + buildSandboxPermissions(envPerms || undefined);
1295
- result.sandboxAdded = true;
1341
+ const ensured = ensureTopLevelSandboxPermissions(toml, envPerms);
1342
+ if (ensured.changed) {
1343
+ toml = ensured.toml;
1344
+ result.sandboxAdded = true;
1345
+ }
1296
1346
  }
1297
1347
 
1298
1348
  // ── 1f. Ensure sandbox workspace-write defaults ───────────
package/hook-profiles.mjs CHANGED
@@ -552,6 +552,10 @@ function buildDisableEnv(hookConfig) {
552
552
  };
553
553
  }
554
554
 
555
+ function normalizePathForOutput(value) {
556
+ return String(value || "").replace(/\\/g, "/");
557
+ }
558
+
555
559
  export function scaffoldAgentHookFiles(repoRoot, options = {}) {
556
560
  const root = resolve(repoRoot || process.cwd());
557
561
  const targets = normalizeHookTargets(options.targets);
@@ -585,13 +589,13 @@ export function scaffoldAgentHookFiles(repoRoot, options = {}) {
585
589
  const codexPath = resolve(root, ".codex", "hooks.json");
586
590
  const existedBefore = existsSync(codexPath);
587
591
  if (existedBefore && !overwriteExisting) {
588
- result.skipped.push(relative(root, codexPath));
592
+ result.skipped.push(normalizePathForOutput(relative(root, codexPath)));
589
593
  } else {
590
594
  writeJson(codexPath, codexHookConfig);
591
595
  if (existedBefore) {
592
- result.updated.push(relative(root, codexPath));
596
+ result.updated.push(normalizePathForOutput(relative(root, codexPath)));
593
597
  } else {
594
- result.written.push(relative(root, codexPath));
598
+ result.written.push(normalizePathForOutput(relative(root, codexPath)));
595
599
  }
596
600
  }
597
601
  }
@@ -610,18 +614,18 @@ export function scaffoldAgentHookFiles(repoRoot, options = {}) {
610
614
  hasLegacyBridgeInCopilotConfig(existingCopilot);
611
615
 
612
616
  if (existedBefore && !overwriteExisting && !forceLegacyMigration) {
613
- result.skipped.push(relative(root, copilotPath));
617
+ result.skipped.push(normalizePathForOutput(relative(root, copilotPath)));
614
618
  } else {
615
619
  writeJson(copilotPath, config);
616
620
  if (existedBefore) {
617
- result.updated.push(relative(root, copilotPath));
621
+ result.updated.push(normalizePathForOutput(relative(root, copilotPath)));
618
622
  if (forceLegacyMigration) {
619
623
  result.warnings.push(
620
- `${relative(root, copilotPath)} contained legacy bridge path and was auto-updated`,
624
+ `${normalizePathForOutput(relative(root, copilotPath))} contained legacy bridge path and was auto-updated`,
621
625
  );
622
626
  }
623
627
  } else {
624
- result.written.push(relative(root, copilotPath));
628
+ result.written.push(normalizePathForOutput(relative(root, copilotPath)));
625
629
  }
626
630
  }
627
631
  }
@@ -634,15 +638,15 @@ export function scaffoldAgentHookFiles(repoRoot, options = {}) {
634
638
 
635
639
  if (existing === null && existsSync(claudePath)) {
636
640
  result.warnings.push(
637
- `${relative(root, claudePath)} exists but is not valid JSON; skipped`,
641
+ `${normalizePathForOutput(relative(root, claudePath))} exists but is not valid JSON; skipped`,
638
642
  );
639
643
  } else {
640
644
  const merged = mergeClaudeSettings(existing, generated);
641
645
  writeJson(claudePath, merged);
642
646
  if (existedBefore) {
643
- result.updated.push(relative(root, claudePath));
647
+ result.updated.push(normalizePathForOutput(relative(root, claudePath)));
644
648
  } else {
645
- result.written.push(relative(root, claudePath));
649
+ result.written.push(normalizePathForOutput(relative(root, claudePath)));
646
650
  }
647
651
  }
648
652
  }
package/monitor.mjs CHANGED
@@ -216,6 +216,7 @@ import {
216
216
  setKanbanBackend,
217
217
  listTasks as listKanbanTasks,
218
218
  updateTaskStatus as updateKanbanTaskStatus,
219
+ updateTask as updateKanbanTask,
219
220
  listProjects as listKanbanProjects,
220
221
  createTask as createKanbanTask,
221
222
  } from "./kanban-adapter.mjs";
@@ -9026,8 +9027,20 @@ async function triggerTaskPlannerViaKanban(
9026
9027
  `[monitor] task planner task already exists in backlog — skipping: "${existingPlanner.title}" (${existingPlanner.id})`,
9027
9028
  );
9028
9029
  // Best-effort: keep backlog task aligned with current requirements
9029
- // Note: For now, we skip updating existing tasks as not all backends support partial updates
9030
- // TODO: Implement backend-specific update logic if needed
9030
+ // Update description if the backend supports it, so the agent gets fresh context
9031
+ try {
9032
+ await updateKanbanTask(existingPlanner.id, {
9033
+ description: desiredDescription,
9034
+ });
9035
+ console.log(
9036
+ `[monitor] updated description of existing planner task: "${existingPlanner.title}" (${existingPlanner.id})`,
9037
+ );
9038
+ } catch (updateErr) {
9039
+ // Not all backends support partial description updates — log and continue
9040
+ console.log(
9041
+ `[monitor] could not update existing planner task description (${updateErr.message || updateErr}) — skipping`,
9042
+ );
9043
+ }
9031
9044
 
9032
9045
  const taskUrl = buildTaskUrl(existingPlanner, projectId);
9033
9046
  if (notify) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.29.6",
3
+ "version": "0.29.8",
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",
@@ -151,6 +151,7 @@
151
151
  "prepublish-check.mjs",
152
152
  "presence.mjs",
153
153
  "primary-agent.mjs",
154
+ "repo-config.mjs",
154
155
  "repo-root.mjs",
155
156
  "restart-controller.mjs",
156
157
  "review-agent.mjs",