bosun 0.34.2 → 0.34.4

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/monitor.mjs CHANGED
@@ -98,6 +98,7 @@ import {
98
98
  resetMergeStrategyDedup,
99
99
  } from "./merge-strategy.mjs";
100
100
  import { assessTask, quickAssess } from "./task-assessment.mjs";
101
+ import { getBosunCoAuthorTrailer } from "./git-commit-helpers.mjs";
101
102
  import {
102
103
  normalizeDedupKey,
103
104
  stripAnsi,
@@ -253,6 +254,60 @@ function getAgentAlertsPath() {
253
254
  return resolve(repoRoot, ".cache", "agent-work-logs", "agent-alerts.jsonl");
254
255
  }
255
256
 
257
+ function getAgentAlertsStatePath() {
258
+ return resolve(
259
+ repoRoot,
260
+ ".cache",
261
+ "agent-work-logs",
262
+ "agent-alert-tail-state.json",
263
+ );
264
+ }
265
+
266
+ function loadAgentAlertsState() {
267
+ const statePath = getAgentAlertsStatePath();
268
+ try {
269
+ if (!existsSync(statePath)) return;
270
+ const raw = readFileSync(statePath, "utf8");
271
+ const parsed = JSON.parse(raw);
272
+ const offset = Number(parsed?.offset || 0);
273
+ if (Number.isFinite(offset) && offset >= 0) {
274
+ agentAlertsOffset = offset;
275
+ }
276
+ const dedupEntries = Array.isArray(parsed?.dedupEntries)
277
+ ? parsed.dedupEntries
278
+ : [];
279
+ agentAlertsDedup.clear();
280
+ for (const entry of dedupEntries) {
281
+ if (!entry || typeof entry !== "object") continue;
282
+ const key = String(entry.key || "").trim();
283
+ const ts = Number(entry.ts || 0);
284
+ if (!key || !Number.isFinite(ts) || ts <= 0) continue;
285
+ agentAlertsDedup.set(key, ts);
286
+ }
287
+ } catch (err) {
288
+ console.warn(`[monitor] failed loading alert tail state: ${err.message}`);
289
+ }
290
+ }
291
+
292
+ function saveAgentAlertsState() {
293
+ const statePath = getAgentAlertsStatePath();
294
+ try {
295
+ mkdirSync(dirname(statePath), { recursive: true });
296
+ const dedupEntries = [...agentAlertsDedup.entries()]
297
+ .sort((a, b) => b[1] - a[1])
298
+ .slice(0, 200)
299
+ .map(([key, ts]) => ({ key, ts }));
300
+ const payload = {
301
+ offset: agentAlertsOffset,
302
+ dedupEntries,
303
+ updatedAt: new Date().toISOString(),
304
+ };
305
+ writeFileSync(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
306
+ } catch (err) {
307
+ console.warn(`[monitor] failed saving alert tail state: ${err.message}`);
308
+ }
309
+ }
310
+
256
311
  function rememberAlert(key) {
257
312
  agentAlertsDedup.set(key, Date.now());
258
313
  if (agentAlertsDedup.size > 200) {
@@ -590,6 +645,9 @@ async function pollAgentAlerts() {
590
645
  } catch {
591
646
  return;
592
647
  }
648
+ if (data.length < agentAlertsOffset) {
649
+ agentAlertsOffset = 0;
650
+ }
593
651
  if (!data || data.length <= agentAlertsOffset) return;
594
652
  const chunk = data.slice(agentAlertsOffset);
595
653
  agentAlertsOffset = data.length;
@@ -614,11 +672,14 @@ async function pollAgentAlerts() {
614
672
  telegramChatId &&
615
673
  process.env.AGENT_ALERTS_NOTIFY === "true"
616
674
  ) {
617
- void sendTelegramMessage(formatAgentAlert(alert), {
618
- dedupKey: `agent-alert:${alert.type || "alert"}:${alert.attempt_id || "unknown"}`,
619
- }).catch(() => {});
675
+ runDetached("agent-alerts:telegram", () =>
676
+ sendTelegramMessage(formatAgentAlert(alert), {
677
+ dedupKey: `agent-alert:${alert.type || "alert"}:${alert.attempt_id || "unknown"}`,
678
+ }),
679
+ );
620
680
  }
621
681
  }
682
+ saveAgentAlertsState();
622
683
  }
623
684
 
624
685
  function startAgentWorkAnalyzer() {
@@ -648,11 +709,12 @@ function stopAgentWorkAnalyzer() {
648
709
 
649
710
  function startAgentAlertTailer() {
650
711
  if (agentAlertsTimer) return;
712
+ loadAgentAlertsState();
651
713
  agentAlertsTimer = setInterval(() => {
652
- void pollAgentAlerts();
714
+ runDetached("agent-alerts:poll", pollAgentAlerts);
653
715
  }, AGENT_ALERT_POLL_MS);
654
716
  agentAlertsTimer.unref?.();
655
- void pollAgentAlerts();
717
+ runDetached("agent-alerts:poll", pollAgentAlerts);
656
718
  }
657
719
 
658
720
  function stopAgentAlertTailer() {
@@ -2085,7 +2147,9 @@ async function handleMonitorFailure(reason, err) {
2085
2147
  // Ensure we retry after safe-mode window if still running.
2086
2148
  if (!shuttingDown) {
2087
2149
  setTimeout(() => {
2088
- if (!shuttingDown) void startProcess();
2150
+ if (!shuttingDown) {
2151
+ runDetached("start-process:hard-cap-retry", startProcess);
2152
+ }
2089
2153
  }, pauseMs + 1000);
2090
2154
  }
2091
2155
  return;
@@ -2192,6 +2256,21 @@ function runGuarded(reason, fn) {
2192
2256
  reportGuardedFailure(reason, err);
2193
2257
  }
2194
2258
  }
2259
+ function runDetached(label, promiseOrFn) {
2260
+ const logFailure = (err) => {
2261
+ const message = formatMonitorError(err);
2262
+ console.warn(`[monitor] detached ${label} failed: ${message}`);
2263
+ };
2264
+ try {
2265
+ const result =
2266
+ typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
2267
+ if (result && typeof result.then === "function") {
2268
+ result.catch((err) => logFailure(err));
2269
+ }
2270
+ } catch (err) {
2271
+ logFailure(err);
2272
+ }
2273
+ }
2195
2274
 
2196
2275
  function safeSetInterval(reason, fn, ms) {
2197
2276
  return setInterval(() => runGuarded(`interval:${reason}`, fn), ms);
@@ -2554,15 +2633,19 @@ function notifyVkError(line) {
2554
2633
  ]
2555
2634
  .filter(Boolean)
2556
2635
  .join("\n");
2557
- void sendTelegramMessage(message, { parseMode: "HTML" });
2558
- triggerVibeKanbanRecovery(line);
2636
+ runDetached("vk-error:telegram", () =>
2637
+ sendTelegramMessage(message, { parseMode: "HTML" }),
2638
+ );
2639
+ runDetached("vk-recovery:notify", () => triggerVibeKanbanRecovery(line));
2559
2640
  }
2560
2641
 
2561
2642
  function notifyCodexTrigger(context) {
2562
2643
  if (!telegramToken || !telegramChatId) {
2563
2644
  return;
2564
2645
  }
2565
- void sendTelegramMessage(`Codex triggered: ${context}`);
2646
+ runDetached("codex-trigger:telegram", () =>
2647
+ sendTelegramMessage(`Codex triggered: ${context}`),
2648
+ );
2566
2649
  }
2567
2650
 
2568
2651
  async function runCodexRecovery(reason) {
@@ -2796,7 +2879,10 @@ function scheduleVibeKanbanRestart() {
2796
2879
  console.log(
2797
2880
  `[monitor] restarting vibe-kanban in ${delay}ms (attempt ${vkRestartCount}/${vkMaxRestarts})`,
2798
2881
  );
2799
- setTimeout(() => void startVibeKanbanProcess(), delay);
2882
+ setTimeout(
2883
+ () => runDetached("vk-restart:scheduled", startVibeKanbanProcess),
2884
+ delay,
2885
+ );
2800
2886
  }
2801
2887
 
2802
2888
  async function canConnectTcp(host, port, timeoutMs = 1200) {
@@ -2894,7 +2980,7 @@ function restartVibeKanbanProcess() {
2894
2980
  /* best effort */
2895
2981
  }
2896
2982
  } else {
2897
- void startVibeKanbanProcess();
2983
+ runDetached("vk-restart:manual", startVibeKanbanProcess);
2898
2984
  }
2899
2985
  }
2900
2986
 
@@ -3109,7 +3195,9 @@ function ensureVkLogStream() {
3109
3195
  }
3110
3196
 
3111
3197
  // Discover any active sessions immediately and keep polling for new sessions
3112
- void refreshVkSessionStreams("startup");
3198
+ runDetached("vk-session-discovery:startup", () =>
3199
+ refreshVkSessionStreams("startup"),
3200
+ );
3113
3201
  ensureVkSessionDiscoveryLoop();
3114
3202
  }
3115
3203
 
@@ -3117,7 +3205,9 @@ function ensureVkSessionDiscoveryLoop() {
3117
3205
  if (vkSessionDiscoveryTimer) return;
3118
3206
  if (!Number.isFinite(vkEnsureIntervalMs) || vkEnsureIntervalMs <= 0) return;
3119
3207
  vkSessionDiscoveryTimer = setInterval(() => {
3120
- void refreshVkSessionStreams("periodic");
3208
+ runDetached("vk-session-discovery:periodic", () =>
3209
+ refreshVkSessionStreams("periodic"),
3210
+ );
3121
3211
  }, vkEnsureIntervalMs);
3122
3212
  }
3123
3213
 
@@ -3299,7 +3389,9 @@ async function triggerVibeKanbanRecovery(reason) {
3299
3389
  const notice = codexEnabled
3300
3390
  ? `Codex recovery triggered: vibe-kanban unreachable. Attempting restart. (${link})`
3301
3391
  : `Vibe-kanban recovery triggered (Codex disabled). Attempting restart. (${link})`;
3302
- void sendTelegramMessage(notice, { parseMode: "HTML" });
3392
+ runDetached("vk-recovery-notify", () =>
3393
+ sendTelegramMessage(notice, { parseMode: "HTML" }),
3394
+ );
3303
3395
  }
3304
3396
  await runCodexRecovery(reason || "vibe-kanban unreachable");
3305
3397
  restartVibeKanbanProcess();
@@ -3409,8 +3501,10 @@ async function fetchVk(path, opts = {}) {
3409
3501
  if (shouldLogVkWarning("network-error")) {
3410
3502
  console.warn(`[monitor] fetchVk ${method} ${path} error: ${msg}`);
3411
3503
  }
3412
- void triggerVibeKanbanRecovery(
3413
- `fetchVk ${method} ${path} network error: ${msg}`,
3504
+ runDetached("vk-recovery:network", () =>
3505
+ triggerVibeKanbanRecovery(
3506
+ `fetchVk ${method} ${path} network error: ${msg}`,
3507
+ ),
3414
3508
  );
3415
3509
  noteVkErrorBurst("network-error");
3416
3510
  }
@@ -3428,8 +3522,10 @@ async function fetchVk(path, opts = {}) {
3428
3522
  `[monitor] fetchVk ${method} ${path} error: invalid response object (res=${!!res}, res.ok=${res?.ok})`,
3429
3523
  );
3430
3524
  }
3431
- void triggerVibeKanbanRecovery(
3432
- `fetchVk ${method} ${path} invalid response object`,
3525
+ runDetached("vk-recovery:invalid-response", () =>
3526
+ triggerVibeKanbanRecovery(
3527
+ `fetchVk ${method} ${path} invalid response object`,
3528
+ ),
3433
3529
  );
3434
3530
  noteVkErrorBurst("invalid-response");
3435
3531
  return null;
@@ -3443,8 +3539,10 @@ async function fetchVk(path, opts = {}) {
3443
3539
  );
3444
3540
  }
3445
3541
  if (res.status >= 500) {
3446
- void triggerVibeKanbanRecovery(
3447
- `fetchVk ${method} ${path} HTTP ${res.status}`,
3542
+ runDetached("vk-recovery:http", () =>
3543
+ triggerVibeKanbanRecovery(
3544
+ `fetchVk ${method} ${path} HTTP ${res.status}`,
3545
+ ),
3448
3546
  );
3449
3547
  noteVkErrorBurst("http-5xx");
3450
3548
  }
@@ -3482,8 +3580,10 @@ async function fetchVk(path, opts = {}) {
3482
3580
  );
3483
3581
  }
3484
3582
  }
3485
- void triggerVibeKanbanRecovery(
3486
- `fetchVk ${method} ${path} non-JSON response`,
3583
+ runDetached("vk-recovery:non-json", () =>
3584
+ triggerVibeKanbanRecovery(
3585
+ `fetchVk ${method} ${path} non-JSON response`,
3586
+ ),
3487
3587
  );
3488
3588
  noteVkErrorBurst("non-json");
3489
3589
  if (now - vkNonJsonNotifiedAt > 10 * 60 * 1000) {
@@ -6986,6 +7086,19 @@ function extractPrNumberFromUrl(prUrl) {
6986
7086
  return match ? parsePositivePrNumber(match[1]) : null;
6987
7087
  }
6988
7088
 
7089
+ function buildFlowGateMergeBody(taskTitle, taskId) {
7090
+ const trailer = getBosunCoAuthorTrailer();
7091
+ const safeTitle = String(taskTitle || "Task").trim() || "Task";
7092
+ const safeId = String(taskId || "").trim();
7093
+ const lines = [
7094
+ `Merged by Bosun flow gate for: ${safeTitle}`,
7095
+ safeId ? `Task: ${safeId}` : "",
7096
+ "",
7097
+ trailer,
7098
+ ].filter(Boolean);
7099
+ return lines.join("\n");
7100
+ }
7101
+
6989
7102
  async function triggerFlowPostReviewMerge(taskId, context = {}) {
6990
7103
  if (!isFlowPrimaryEnabled() || !isFlowReviewGateEnabled()) {
6991
7104
  return false;
@@ -7036,6 +7149,7 @@ async function triggerFlowPostReviewMerge(taskId, context = {}) {
7036
7149
 
7037
7150
  const autoArgs = ["pr", "merge", String(prNumber)];
7038
7151
  if (repoSlug) autoArgs.push("--repo", repoSlug);
7152
+ autoArgs.push("--body", buildFlowGateMergeBody(taskTitle, id));
7039
7153
  autoArgs.push("--auto", "--squash");
7040
7154
 
7041
7155
  const autoResult = spawnSync("gh", autoArgs, {
@@ -7057,6 +7171,7 @@ async function triggerFlowPostReviewMerge(taskId, context = {}) {
7057
7171
  if (/clean status|not in the correct state/i.test(autoErr)) {
7058
7172
  const directArgs = ["pr", "merge", String(prNumber)];
7059
7173
  if (repoSlug) directArgs.push("--repo", repoSlug);
7174
+ directArgs.push("--body", buildFlowGateMergeBody(taskTitle, id));
7060
7175
  directArgs.push("--squash");
7061
7176
  const directResult = spawnSync("gh", directArgs, {
7062
7177
  cwd: repoRoot,
@@ -7227,10 +7342,12 @@ async function runMergeStrategyAnalysis(ctx, opts = {}) {
7227
7342
  // Re-run analysis after the wait period
7228
7343
  setTimeout(
7229
7344
  () => {
7230
- void runMergeStrategyAnalysis({
7231
- ...ctx,
7232
- ciStatus: "re-check",
7233
- });
7345
+ runDetached("merge-strategy:wait-recheck", () =>
7346
+ runMergeStrategyAnalysis({
7347
+ ...ctx,
7348
+ ciStatus: "re-check",
7349
+ }),
7350
+ );
7234
7351
  },
7235
7352
  (execResult.waitSeconds || 300) * 1000,
7236
7353
  );
@@ -7531,10 +7648,12 @@ async function actOnAssessment(ctx, decision) {
7531
7648
  const waitSec = decision.waitSeconds || 300;
7532
7649
  console.log(`[${tag}] → wait ${waitSec}s`);
7533
7650
  setTimeout(() => {
7534
- void runTaskAssessment({
7535
- ...ctx,
7536
- trigger: "reassessment",
7537
- });
7651
+ runDetached("task-assessment:recheck", () =>
7652
+ runTaskAssessment({
7653
+ ...ctx,
7654
+ trigger: "reassessment",
7655
+ }),
7656
+ );
7538
7657
  }, waitSec * 1000);
7539
7658
  break;
7540
7659
  }
@@ -9253,11 +9372,15 @@ function startTaskPlannerStatusLoop() {
9253
9372
  if (!taskPlannerStatus.enabled || plannerMode === "disabled") return;
9254
9373
  taskPlannerStatus.timer = setInterval(() => {
9255
9374
  if (shuttingDown) return;
9256
- void publishTaskPlannerStatus("interval");
9375
+ runDetached("task-planner-status:interval", () =>
9376
+ publishTaskPlannerStatus("interval"),
9377
+ );
9257
9378
  }, taskPlannerStatus.intervalMs);
9258
9379
  setTimeout(() => {
9259
9380
  if (shuttingDown) return;
9260
- void publishTaskPlannerStatus("startup");
9381
+ runDetached("task-planner-status:startup", () =>
9382
+ publishTaskPlannerStatus("startup"),
9383
+ );
9261
9384
  }, 25_000);
9262
9385
  }
9263
9386
 
@@ -9952,6 +10075,7 @@ async function startTelegramNotifier() {
9952
10075
  await checkStatusMilestones();
9953
10076
  };
9954
10077
 
10078
+
9955
10079
  // Suppress "Notifier started" message on rapid restarts (e.g. code-change restarts).
9956
10080
  // If the last start was <60s ago, skip the notification — just log locally.
9957
10081
  const lastStartPath = resolve(
@@ -9976,10 +10100,16 @@ async function startTelegramNotifier() {
9976
10100
  `[monitor] notifier restarted (suppressed telegram notification — rapid restart)`,
9977
10101
  );
9978
10102
  } else {
9979
- void sendTelegramMessage(`${projectName} Orchestrator Notifier started.`);
10103
+ runDetached("telegram-notifier:startup", () =>
10104
+ sendTelegramMessage(`${projectName} Orchestrator Notifier started.`),
10105
+ );
9980
10106
  }
9981
- telegramNotifierTimeout = setTimeout(sendUpdate, intervalMs);
9982
- telegramNotifierInterval = setInterval(sendUpdate, intervalMs);
10107
+ telegramNotifierTimeout = setTimeout(() => {
10108
+ runDetached("telegram-notifier:tick", sendUpdate);
10109
+ }, intervalMs);
10110
+ telegramNotifierInterval = setInterval(() => {
10111
+ runDetached("telegram-notifier:tick", sendUpdate);
10112
+ }, intervalMs);
9983
10113
  }
9984
10114
 
9985
10115
  async function checkStatusMilestones() {
@@ -10467,7 +10597,9 @@ async function triggerTaskPlanner(
10467
10597
  } else {
10468
10598
  throw new Error(`Unknown planner mode: ${effectiveMode}`);
10469
10599
  }
10470
- void publishTaskPlannerStatus("trigger-success");
10600
+ runDetached("task-planner-status:trigger-success", () =>
10601
+ publishTaskPlannerStatus("trigger-success"),
10602
+ );
10471
10603
  return result;
10472
10604
  } catch (err) {
10473
10605
  const message = err && err.message ? err.message : String(err);
@@ -10481,7 +10613,9 @@ async function triggerTaskPlanner(
10481
10613
  `Task planner run failed (${effectiveMode}): ${message}`,
10482
10614
  );
10483
10615
  }
10484
- void publishTaskPlannerStatus("trigger-failed");
10616
+ runDetached("task-planner-status:trigger-failed", () =>
10617
+ publishTaskPlannerStatus("trigger-failed"),
10618
+ );
10485
10619
  throw err; // re-throw so callers (e.g. /plan command) know it failed
10486
10620
  } finally {
10487
10621
  plannerTriggered = false;
@@ -12672,7 +12806,9 @@ async function startProcess() {
12672
12806
  if (!shuttingDown) {
12673
12807
  const retryMs = Math.max(5_000, restartDelayMs || 0);
12674
12808
  safeSetTimeout("startProcess-retry", () => {
12675
- if (!shuttingDown) void startProcess();
12809
+ if (!shuttingDown) {
12810
+ return startProcess();
12811
+ }
12676
12812
  }, retryMs);
12677
12813
  }
12678
12814
  }
@@ -13184,7 +13320,9 @@ function scheduleEnvReload(reason) {
13184
13320
  clearTimeout(envWatcherDebounce);
13185
13321
  }
13186
13322
  envWatcherDebounce = setTimeout(() => {
13187
- void reloadConfig(reason || "env-change");
13323
+ runDetached("config-reload:env-change", () =>
13324
+ reloadConfig(reason || "env-change"),
13325
+ );
13188
13326
  }, 400);
13189
13327
  }
13190
13328
 
@@ -14961,3 +15099,5 @@ export {
14961
15099
  getContainerStatus,
14962
15100
  isContainerEnabled,
14963
15101
  };
15102
+
15103
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.34.2",
3
+ "version": "0.34.4",
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",
@@ -152,6 +152,7 @@
152
152
  "github-oauth-portal.mjs",
153
153
  "github-reconciler.mjs",
154
154
  "get-telegram-chat-id.mjs",
155
+ "git-commit-helpers.mjs",
155
156
  "git-safety.mjs",
156
157
  "kanban-adapter.mjs",
157
158
  "lib/logger.mjs",
@@ -182,6 +182,17 @@ function getCommandVersion(cmd) {
182
182
  }
183
183
  }
184
184
 
185
+ function isWslInteropRuntime() {
186
+ return Boolean(
187
+ process.env.WSL_DISTRO_NAME
188
+ || process.env.WSL_INTEROP
189
+ || (process.platform === "win32"
190
+ && String(process.env.HOME || "")
191
+ .trim()
192
+ .startsWith("/home/")),
193
+ );
194
+ }
195
+
185
196
  /**
186
197
  * Resolve the Bosun Home directory — the single root where bosun stores all its
187
198
  * configs, workspaces, and tooling. Resolution order:
@@ -190,7 +201,7 @@ function getCommandVersion(cmd) {
190
201
  * 3. cwd if it already has a bosun.config.json AND is not inside a git repo
191
202
  * we don't own (safety: prevents "bosun --setup" from contaminating whatever
192
203
  * repo the user happened to be in when they ran the command)
193
- * 4. ~/bosun (cross-platform stable default)
204
+ * 4. platform default config home + /bosun (same as runtime loader)
194
205
  */
195
206
  function resolveConfigDir() {
196
207
  if (process.env.BOSUN_HOME) return resolve(process.env.BOSUN_HOME);
@@ -211,8 +222,22 @@ function resolveConfigDir() {
211
222
  if (isExpectedHome) return cwd;
212
223
  }
213
224
 
214
- // Stable default: ~/bosun — same path on every OS
215
- return resolve(homedir(), "bosun");
225
+ const preferWindowsDirs =
226
+ process.platform === "win32" && !isWslInteropRuntime();
227
+ const baseDir = preferWindowsDirs
228
+ ? process.env.APPDATA
229
+ || process.env.LOCALAPPDATA
230
+ || process.env.USERPROFILE
231
+ || process.env.HOME
232
+ || homedir()
233
+ : process.env.HOME
234
+ || process.env.XDG_CONFIG_HOME
235
+ || process.env.USERPROFILE
236
+ || process.env.APPDATA
237
+ || process.env.LOCALAPPDATA
238
+ || homedir();
239
+
240
+ return resolve(baseDir, "bosun");
216
241
  }
217
242
 
218
243
  /**
@@ -664,7 +689,7 @@ function handleApply(body) {
664
689
  envMap.TELEGRAM_UI_ALLOW_UNSAFE = "false";
665
690
  }
666
691
  if (env.telegramChatId) envMap.TELEGRAM_CHAT_ID = env.telegramChatId;
667
- if (env.jiraUrl) envMap.JIRA_URL = env.jiraUrl;
692
+ if (env.jiraUrl) envMap.JIRA_BASE_URL = env.jiraUrl;
668
693
  if (env.jiraProjectKey) envMap.JIRA_PROJECT_KEY = env.jiraProjectKey;
669
694
  if (env.jiraApiToken) envMap.JIRA_API_TOKEN = env.jiraApiToken;
670
695
  if (env.githubProjectNumber) envMap.GITHUB_PROJECT_NUMBER = String(env.githubProjectNumber);
@@ -674,11 +699,11 @@ function handleApply(body) {
674
699
  if (env.executorDistribution) envMap.EXECUTOR_DISTRIBUTION = env.executorDistribution;
675
700
  if (env.failoverStrategy) envMap.FAILOVER_STRATEGY = env.failoverStrategy;
676
701
  if (env.maxParallel != null) envMap.MAX_PARALLEL = String(env.maxParallel);
677
- if (env.maxRetries != null) envMap.MAX_RETRIES = String(env.maxRetries);
702
+ if (env.maxRetries != null) envMap.FAILOVER_MAX_RETRIES = String(env.maxRetries);
678
703
  if (env.failoverCooldownMinutes != null)
679
- envMap.FAILOVER_COOLDOWN_MINUTES = String(env.failoverCooldownMinutes);
704
+ envMap.FAILOVER_COOLDOWN_MIN = String(env.failoverCooldownMinutes);
680
705
  if (env.failoverDisableOnConsecutive != null)
681
- envMap.FAILOVER_DISABLE_ON_CONSECUTIVE_FAILURES = String(env.failoverDisableOnConsecutive);
706
+ envMap.FAILOVER_DISABLE_AFTER = String(env.failoverDisableOnConsecutive);
682
707
  if (env.primaryAgent) envMap.PRIMARY_AGENT = env.primaryAgent;
683
708
  if (env.projectRequirementsProfile) envMap.PROJECT_REQUIREMENTS_PROFILE = env.projectRequirementsProfile;
684
709
  if (env.internalReplenishEnabled != null)
package/task-executor.mjs CHANGED
@@ -71,6 +71,7 @@ import {
71
71
  getRecentCommits,
72
72
  collectDiffStats,
73
73
  } from "./diff-stats.mjs";
74
+ import { getBosunCoAuthorTrailer } from "./git-commit-helpers.mjs";
74
75
  import { createAnomalyDetector } from "./anomaly-detector.mjs";
75
76
  import { normalizeDedupKey, yieldToEventLoop, withRetry } from "./utils.mjs";
76
77
  import {
@@ -323,26 +324,22 @@ function categorizeError(err) {
323
324
  }
324
325
 
325
326
  /**
326
- * Returns the Co-authored-by trailer for bosun-ve[bot], or empty string
327
- * if the GitHub App ID is not configured. Used to attribute agent commits to
328
- * the Bosun GitHub App so the bot shows up as a contributor.
327
+ * Returns the Co-authored-by trailer for bosun-ve[bot].
329
328
  *
330
- * To enable: set BOSUN_GITHUB_APP_ID=<your-app-id> in .env
331
- * App noreply email format: <id>+bosun-ve[bot]@users.noreply.github.com
329
+ * Attribution should not be gated by auth mode (OAuth/App/env/gh-cli all valid
330
+ * execution paths). This uses the canonical helper so commit attribution stays
331
+ * consistent with the rest of Bosun.
332
332
  */
333
333
  function getBosunCoAuthorLine() {
334
- const appId = String(process.env.BOSUN_GITHUB_APP_ID || "").trim();
335
- if (!appId) return "";
336
- return `Co-authored-by: bosun-ve[bot] <${appId}+bosun-ve[bot]@users.noreply.github.com>`;
334
+ return getBosunCoAuthorTrailer();
337
335
  }
338
336
 
339
337
  /**
340
338
  * Returns a prompt instruction block telling the agent to append the Bosun
341
- * co-author trailer to every commit. Empty string when app ID not configured.
339
+ * co-author trailer to every commit.
342
340
  */
343
341
  function getBosunCoAuthorInstruction() {
344
342
  const line = getBosunCoAuthorLine();
345
- if (!line) return "";
346
343
  return `\n**Attribution (required — do not omit):**
347
344
  Every commit message MUST end with a blank line then this exact trailer:
348
345
  \`\`\`
package/task-store.mjs CHANGED
@@ -34,10 +34,34 @@ function inferRepoRoot(startDir) {
34
34
  }
35
35
  }
36
36
 
37
+ function resolveBosunHomeDir() {
38
+ const explicit = String(
39
+ process.env.BOSUN_HOME || process.env.BOSUN_DIR || "",
40
+ ).trim();
41
+ if (explicit) return resolve(explicit);
42
+
43
+ const base = String(
44
+ process.env.APPDATA ||
45
+ process.env.LOCALAPPDATA ||
46
+ process.env.USERPROFILE ||
47
+ process.env.HOME ||
48
+ "",
49
+ ).trim();
50
+ if (!base) return null;
51
+ if (/[/\\]bosun$/i.test(base)) return resolve(base);
52
+ return resolve(base, "bosun");
53
+ }
54
+
37
55
  function resolveDefaultStorePath() {
38
- const repoRoot =
39
- inferRepoRoot(process.cwd()) || resolve(__dirname, "..", "..");
40
- return resolve(repoRoot, ".bosun", ".cache", "kanban-state.json");
56
+ const repoRoot = inferRepoRoot(process.cwd());
57
+ if (repoRoot) {
58
+ return resolve(repoRoot, ".bosun", ".cache", "kanban-state.json");
59
+ }
60
+ const bosunHome = resolveBosunHomeDir();
61
+ if (bosunHome) {
62
+ return resolve(bosunHome, ".cache", "kanban-state.json");
63
+ }
64
+ return resolve(__dirname, ".cache", "kanban-state.json");
41
65
  }
42
66
 
43
67
  let storePath = resolveDefaultStorePath();
@@ -58,13 +82,15 @@ let _didLogInitialLoad = false;
58
82
 
59
83
  export function configureTaskStore(options = {}) {
60
84
  const baseDir = options.baseDir ? resolve(options.baseDir) : null;
85
+ const repoRoot = inferRepoRoot(process.cwd());
86
+ const homeDir = resolveBosunHomeDir();
87
+ const defaultBase = baseDir || repoRoot || homeDir || resolve(__dirname);
88
+ const needsBosunSubdir = Boolean(baseDir || repoRoot);
61
89
  const nextPath = options.storePath
62
90
  ? resolve(baseDir || process.cwd(), options.storePath)
63
91
  : resolve(
64
- baseDir ||
65
- inferRepoRoot(process.cwd()) ||
66
- resolve(__dirname, "..", ".."),
67
- ".bosun",
92
+ defaultBase,
93
+ needsBosunSubdir ? ".bosun" : "",
68
94
  ".cache",
69
95
  "kanban-state.json",
70
96
  );
@@ -420,14 +446,21 @@ export function setTaskStatus(taskId, status, source) {
420
446
  }
421
447
 
422
448
  const prev = task.status;
449
+ const tsNow = now();
423
450
  task.status = status;
424
- task.updatedAt = now();
425
- task.lastActivityAt = now();
451
+ task.updatedAt = tsNow;
452
+ task.lastActivityAt = tsNow;
453
+
454
+ // No-op transition: keep activity fresh without polluting history/logs.
455
+ if (prev === status) {
456
+ saveStore();
457
+ return { ...task };
458
+ }
426
459
 
427
460
  // Append to history (FIFO, max 50)
428
461
  task.statusHistory.push({
429
462
  status,
430
- timestamp: now(),
463
+ timestamp: tsNow,
431
464
  source: source || "unknown",
432
465
  });
433
466
  if (task.statusHistory.length > MAX_STATUS_HISTORY) {