bosun 0.40.3 → 0.40.5

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
@@ -181,6 +181,17 @@ npm run hooks:install
181
181
  - `docs/` and `_docs/` — product docs, deep technical references, and long-form source material
182
182
  - `tools/` and `tests/` — build utilities, release checks, and regression coverage
183
183
 
184
+ If you find this project useful or would like to stay up to date with new releases, a star is appreciated!
185
+
186
+ ## Star History
187
+
188
+ <a href="https://www.star-history.com/?repos=VirtEngine%2FBosun&type=date&legend=top-left">
189
+ <picture>
190
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=VirtEngine/Bosun&type=date&theme=dark&legend=top-left" />
191
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=VirtEngine/Bosun&type=date&legend=top-left" />
192
+ <img alt="Star History Chart" src="https://api.star-history.com/image?repos=VirtEngine/Bosun&type=date&legend=top-left" />
193
+ </picture>
194
+ </a>
184
195
  ---
185
196
 
186
197
  ## License
@@ -122,24 +122,36 @@ function extractRepoSlug(prUrl) {
122
122
 
123
123
  /**
124
124
  * Get the PR diff using `gh pr diff` or `git diff`.
125
- * @param {{ prUrl?: string, branchName?: string }} opts
125
+ * @param {{ prUrl?: string, prNumber?: number|string, repoSlug?: string, branchName?: string, cwd?: string|null }} opts
126
126
  * @returns {{ diff: string, truncated: boolean }}
127
127
  */
128
- function getPrDiff({ prUrl, branchName }) {
128
+ function getPrDiff({ prUrl, prNumber, repoSlug, branchName, cwd }) {
129
129
  let diff = "";
130
+ const commandOptions = {
131
+ encoding: "utf8",
132
+ timeout: 30_000,
133
+ stdio: ["pipe", "pipe", "pipe"],
134
+ ...(cwd ? { cwd } : {}),
135
+ };
130
136
 
131
137
  // Strategy 1: gh pr diff
132
- const prNumber = extractPrNumber(prUrl);
133
- const repoSlug = extractRepoSlug(prUrl);
134
- if (prNumber && repoSlug) {
138
+ const resolvedPrNumber = Number.isFinite(Number(prNumber))
139
+ ? Number(prNumber)
140
+ : extractPrNumber(prUrl);
141
+ const resolvedRepoSlug = String(repoSlug || "").trim() || extractRepoSlug(prUrl);
142
+ if (resolvedPrNumber) {
135
143
  try {
136
- const result = spawnSync(
137
- "gh",
138
- ["pr", "diff", String(prNumber), "--repo", repoSlug],
139
- { encoding: "utf8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"] },
140
- );
141
- if (result.status === 0 && result.stdout?.trim()) {
142
- diff = result.stdout;
144
+ const attempts = [];
145
+ if (resolvedRepoSlug) {
146
+ attempts.push(["pr", "diff", String(resolvedPrNumber), "--repo", resolvedRepoSlug]);
147
+ }
148
+ attempts.push(["pr", "diff", String(resolvedPrNumber)]);
149
+ for (const args of attempts) {
150
+ const result = spawnSync("gh", args, commandOptions);
151
+ if (result.status === 0 && result.stdout?.trim()) {
152
+ diff = result.stdout;
153
+ break;
154
+ }
143
155
  }
144
156
  } catch {
145
157
  /* fall through to git diff */
@@ -149,13 +161,16 @@ function getPrDiff({ prUrl, branchName }) {
149
161
  // Strategy 2: git diff main...<branch>
150
162
  if (!diff && branchName) {
151
163
  try {
152
- const result = spawnSync("git", ["diff", `main...${branchName}`], {
153
- encoding: "utf8",
154
- timeout: 30_000,
155
- stdio: ["pipe", "pipe", "pipe"],
156
- });
157
- if (result.status === 0 && result.stdout?.trim()) {
158
- diff = result.stdout;
164
+ const attempts = [
165
+ ["diff", `origin/main...${branchName}`],
166
+ ["diff", `main...${branchName}`],
167
+ ];
168
+ for (const args of attempts) {
169
+ const result = spawnSync("git", args, commandOptions);
170
+ if (result.status === 0 && result.stdout?.trim()) {
171
+ diff = result.stdout;
172
+ break;
173
+ }
159
174
  }
160
175
  } catch {
161
176
  /* ignore */
@@ -343,6 +358,17 @@ export class ReviewAgent {
343
358
  console.warn(`${TAG} queueReview called without task id — skipping`);
344
359
  return;
345
360
  }
361
+ const hasReviewReference = Boolean(
362
+ String(task.prUrl || "").trim() ||
363
+ String(task.prNumber || "").trim() ||
364
+ String(task.branchName || "").trim(),
365
+ );
366
+ if (!hasReviewReference) {
367
+ console.warn(
368
+ `${TAG} queueReview skipped for ${task.id}: no prUrl, prNumber, or branchName`,
369
+ );
370
+ return;
371
+ }
346
372
 
347
373
  if (this.#seen.has(task.id)) {
348
374
  console.log(`${TAG} task ${task.id} already queued/in-flight — skipping`);
@@ -463,7 +489,10 @@ export class ReviewAgent {
463
489
  // 1. Get PR diff
464
490
  const { diff, truncated } = getPrDiff({
465
491
  prUrl: task.prUrl,
492
+ prNumber: task.prNumber,
493
+ repoSlug: task.repoSlug,
466
494
  branchName: task.branchName,
495
+ cwd: task.worktreePath || null,
467
496
  });
468
497
 
469
498
  if (!diff) {
package/cli.mjs CHANGED
@@ -16,7 +16,7 @@
16
16
  * 3. Configuration loading from config.mjs
17
17
  */
18
18
 
19
- import { resolve, dirname } from "node:path";
19
+ import { isAbsolute, resolve, dirname } from "node:path";
20
20
  import {
21
21
  existsSync,
22
22
  readFileSync,
@@ -237,6 +237,9 @@ function isWslInteropRuntime() {
237
237
  }
238
238
 
239
239
  function resolveConfigDirForCli() {
240
+ const configDirArg = getArgValue("--config-dir");
241
+ if (configDirArg) return resolve(configDirArg);
242
+ if (process.env.BOSUN_HOME) return resolve(process.env.BOSUN_HOME);
240
243
  if (process.env.BOSUN_DIR) return resolve(process.env.BOSUN_DIR);
241
244
  const preferWindowsDirs =
242
245
  process.platform === "win32" && !isWslInteropRuntime();
@@ -612,6 +615,40 @@ function removePidFile() {
612
615
  }
613
616
  }
614
617
 
618
+ function absolutizeDaemonArgPath(value) {
619
+ const raw = String(value || "").trim();
620
+ if (!raw) return raw;
621
+ return isAbsolute(raw) ? raw : resolve(process.cwd(), raw);
622
+ }
623
+
624
+ function normalizeDetachedDaemonArgs(rawArgs = []) {
625
+ const normalized = Array.isArray(rawArgs) ? [...rawArgs] : [];
626
+ const pathFlags = new Set(["--config-dir", "--repo-root", "--log-dir"]);
627
+ for (let i = 0; i < normalized.length; i += 1) {
628
+ const arg = String(normalized[i] || "").trim();
629
+ if (!arg.startsWith("--")) continue;
630
+
631
+ const eq = arg.indexOf("=");
632
+ if (eq > 0) {
633
+ const flag = arg.slice(0, eq);
634
+ const value = arg.slice(eq + 1);
635
+ if (pathFlags.has(flag)) {
636
+ normalized[i] = flag + "=" + absolutizeDaemonArgPath(value);
637
+ }
638
+ continue;
639
+ }
640
+
641
+ if (pathFlags.has(arg) && i + 1 < normalized.length) {
642
+ const value = String(normalized[i + 1] || "").trim();
643
+ if (value && !value.startsWith("--")) {
644
+ normalized[i + 1] = absolutizeDaemonArgPath(value);
645
+ i += 1;
646
+ }
647
+ }
648
+ }
649
+ return normalized;
650
+ }
651
+
615
652
  function startDaemon() {
616
653
  const existing = getDaemonPid();
617
654
  if (existing) {
@@ -656,7 +693,9 @@ function startDaemon() {
656
693
  ...runAsNode,
657
694
  "--max-old-space-size=4096",
658
695
  fileURLToPath(new URL("./cli.mjs", import.meta.url)),
659
- ...process.argv.slice(2).filter((a) => a !== "--daemon" && a !== "-d"),
696
+ ...normalizeDetachedDaemonArgs(
697
+ process.argv.slice(2).filter((a) => a !== "--daemon" && a !== "-d"),
698
+ ),
660
699
  "--daemon-child",
661
700
  ],
662
701
  {
package/config/config.mjs CHANGED
@@ -1292,6 +1292,13 @@ export function loadConfig(argv = process.argv, options = {}) {
1292
1292
  const normalizedRepoRootOverride = repoRootOverride
1293
1293
  ? resolve(repoRootOverride)
1294
1294
  : "";
1295
+ const explicitConfigDirRaw =
1296
+ cli["config-dir"] || process.env.BOSUN_HOME || process.env.BOSUN_DIR || "";
1297
+ const hasExplicitConfigDir = String(explicitConfigDirRaw || "").trim() !== "";
1298
+ const allowRepoEnvWithExplicitConfig = isEnvEnabled(
1299
+ process.env.BOSUN_LOAD_REPO_ENV_WITH_EXPLICIT_CONFIG,
1300
+ false,
1301
+ );
1295
1302
  let detectedRepoRoot = "";
1296
1303
  const getFallbackRepoRoot = () => {
1297
1304
  if (normalizedRepoRootOverride) return normalizedRepoRootOverride;
@@ -1301,8 +1308,7 @@ export function loadConfig(argv = process.argv, options = {}) {
1301
1308
 
1302
1309
  // Determine config directory (where bosun stores its config)
1303
1310
  const configDir =
1304
- cli["config-dir"] ||
1305
- process.env.BOSUN_DIR ||
1311
+ explicitConfigDirRaw ||
1306
1312
  resolveConfigDir(normalizedRepoRootOverride);
1307
1313
 
1308
1314
  const configFile = loadConfigFile(configDir);
@@ -1345,16 +1351,20 @@ export function loadConfig(argv = process.argv, options = {}) {
1345
1351
  repositories[0] ||
1346
1352
  null;
1347
1353
 
1348
- // Resolve repoRoot with workspace-awareness:
1349
- // When workspaces configured and the workspace repo has .git, prefer it
1350
- // over REPO_ROOT (env); REPO_ROOT becomes "developer root" for config only.
1354
+ // Resolve repo root. Explicit repo-root/REPO_ROOT must win over workspace clones
1355
+ // so source-based runs can pin execution to the developer working tree.
1356
+ const explicitRepoRoot = normalizedRepoRootOverride ||
1357
+ (process.env.REPO_ROOT ? resolve(process.env.REPO_ROOT) : "");
1351
1358
  const selectedRepoPath = selectedRepository?.path || "";
1352
1359
  const selectedRepoHasGit = selectedRepoPath && existsSync(resolve(selectedRepoPath, ".git"));
1353
1360
  let repoRoot =
1354
- (selectedRepoHasGit ? selectedRepoPath : null) || getFallbackRepoRoot();
1361
+ explicitRepoRoot ||
1362
+ (selectedRepoHasGit ? selectedRepoPath : null) ||
1363
+ getFallbackRepoRoot();
1355
1364
 
1356
- // Resolve agent execution root (workspace-aware, separate from developer root)
1357
- const agentRepoRoot = resolveAgentRepoRoot();
1365
+ // Resolve agent execution root. Keep workspace-aware behavior by default,
1366
+ // but honor explicit repo-root/REPO_ROOT overrides.
1367
+ const agentRepoRoot = explicitRepoRoot || resolveAgentRepoRoot();
1358
1368
 
1359
1369
  // Load .env from config dir — Bosun's .env is the primary source of truth
1360
1370
  // for Bosun-specific configuration, so it should override any stale shell
@@ -1363,8 +1373,14 @@ export function loadConfig(argv = process.argv, options = {}) {
1363
1373
  const envOverride = reloadEnv || !isEnvEnabled(process.env.BOSUN_ENV_NO_OVERRIDE, false);
1364
1374
  loadDotEnv(configDir, { override: envOverride });
1365
1375
 
1366
- // Also load .env from repo root if different
1367
- if (resolve(repoRoot) !== resolve(configDir)) {
1376
+ const shouldLoadRepoEnv =
1377
+ resolve(repoRoot) !== resolve(configDir) &&
1378
+ (!hasExplicitConfigDir || allowRepoEnvWithExplicitConfig);
1379
+
1380
+ // Also load .env from repo root if different.
1381
+ // When config-dir/BOSUN_HOME is explicit, keep that environment isolated
1382
+ // from the repo root unless explicitly re-enabled.
1383
+ if (shouldLoadRepoEnv) {
1368
1384
  loadDotEnv(repoRoot, { override: envOverride });
1369
1385
  }
1370
1386
 
@@ -1419,14 +1435,17 @@ export function loadConfig(argv = process.argv, options = {}) {
1419
1435
  repoRoot = (selHasGit ? selPath : null) || getFallbackRepoRoot();
1420
1436
  }
1421
1437
 
1422
- if (resolve(repoRoot) !== resolve(initialRepoRoot)) {
1438
+ if (
1439
+ shouldLoadRepoEnv &&
1440
+ resolve(repoRoot) !== resolve(initialRepoRoot)
1441
+ ) {
1423
1442
  loadDotEnv(repoRoot, { override: envOverride });
1424
1443
  }
1425
1444
 
1426
- const envPaths = [
1427
- resolve(configDir, ".env"),
1428
- resolve(repoRoot, ".env"),
1429
- ].filter((p, i, arr) => arr.indexOf(p) === i);
1445
+ const envPaths = [resolve(configDir, ".env")];
1446
+ if (shouldLoadRepoEnv) {
1447
+ envPaths.push(resolve(repoRoot, ".env"));
1448
+ }
1430
1449
  const kanbanSource = resolveKanbanBackendSource({
1431
1450
  envPaths,
1432
1451
  configFilePath: configFile.path,
@@ -2396,3 +2415,4 @@ export {
2396
2415
  resolveAgentRepoRoot,
2397
2416
  };
2398
2417
  export default loadConfig;
2418
+
@@ -22,8 +22,8 @@
22
22
  "electron-updater": "^6.3.9"
23
23
  },
24
24
  "devDependencies": {
25
- "electron": "^30.0.0",
26
- "electron-builder": "^24.13.3"
25
+ "electron": "^35.7.5",
26
+ "electron-builder": "^26.8.1"
27
27
  },
28
28
  "build": {
29
29
  "appId": "com.virtengine.bosun",
package/infra/monitor.mjs CHANGED
@@ -1198,6 +1198,8 @@ const isMonitorTestRuntime =
1198
1198
  .trim()
1199
1199
  .toLowerCase(),
1200
1200
  ) || String(process.env.NODE_ENV || "").trim().toLowerCase() === "test";
1201
+ // Shared schedule poll hook used across startup/timer sections.
1202
+ let pollWorkflowSchedulesOnce = async () => {};
1201
1203
 
1202
1204
  // ── Load unified configuration ──────────────────────────────────────────────
1203
1205
  let config;
@@ -4905,9 +4907,13 @@ function getConfiguredKanbanProjectId(backend) {
4905
4907
  );
4906
4908
  }
4907
4909
 
4910
+ function hasUnresolvedTemplateToken(value) {
4911
+ return /{{[^{}]+}}/.test(String(value || ""));
4912
+ }
4913
+
4908
4914
  function resolveTaskIdForBackend(taskId, backend) {
4909
4915
  const rawId = String(taskId || "").trim();
4910
- if (!rawId) return null;
4916
+ if (!rawId || hasUnresolvedTemplateToken(rawId)) return null;
4911
4917
  if (backend !== "github") return rawId;
4912
4918
  const directMatch = parseGitHubIssueNumber(rawId);
4913
4919
  if (directMatch) return directMatch;
@@ -13518,10 +13524,14 @@ safeSetInterval("flush-error-queue", () => flushErrorQueue(), 60 * 1000);
13518
13524
  // This keeps scheduled and task-poll lifecycle templates executing without hardcoded
13519
13525
  // per-workflow timers.
13520
13526
  const scheduleCheckIntervalMs = 60 * 1000; // check every 60s
13521
- safeSetInterval("workflow-schedule-check", async () => {
13527
+ pollWorkflowSchedulesOnce = async function pollWorkflowSchedulesOnce(
13528
+ triggerSource = "schedule-poll",
13529
+ opts = {},
13530
+ ) {
13522
13531
  try {
13523
13532
  const engine = await ensureWorkflowAutomationEngine();
13524
13533
  if (!engine?.evaluateScheduleTriggers) return;
13534
+ const includeTaskPoll = opts?.includeTaskPoll !== false;
13525
13535
 
13526
13536
  const triggered = engine.evaluateScheduleTriggers();
13527
13537
  if (!Array.isArray(triggered) || triggered.length === 0) return;
@@ -13529,9 +13539,18 @@ safeSetInterval("workflow-schedule-check", async () => {
13529
13539
  for (const match of triggered) {
13530
13540
  const workflowId = String(match?.workflowId || "").trim();
13531
13541
  if (!workflowId) continue;
13542
+ if (!includeTaskPoll) {
13543
+ const workflow = typeof engine.get === "function" ? engine.get(workflowId) : null;
13544
+ const triggerNode = Array.isArray(workflow?.nodes)
13545
+ ? workflow.nodes.find((node) => node?.id === match?.triggeredBy)
13546
+ : null;
13547
+ if (triggerNode?.type === "trigger.task_available" || triggerNode?.type === "trigger.task_low") {
13548
+ continue;
13549
+ }
13550
+ }
13532
13551
  void engine
13533
13552
  .execute(workflowId, {
13534
- _triggerSource: "schedule-poll",
13553
+ _triggerSource: triggerSource,
13535
13554
  _triggeredBy: match?.triggeredBy || null,
13536
13555
  _lastRunAt: Date.now(),
13537
13556
  repoRoot,
@@ -13559,9 +13578,12 @@ safeSetInterval("workflow-schedule-check", async () => {
13559
13578
  );
13560
13579
  }
13561
13580
  } catch (err) {
13562
- // Schedule evaluation must not crash the monitor
13563
13581
  console.warn(`[workflows] schedule-check error: ${err?.message || err}`);
13564
13582
  }
13583
+ };
13584
+
13585
+ safeSetInterval("workflow-schedule-check", async () => {
13586
+ await pollWorkflowSchedulesOnce();
13565
13587
  }, scheduleCheckIntervalMs);
13566
13588
 
13567
13589
  // Legacy merged PR check removed (workflow-only control).
@@ -13658,12 +13680,15 @@ if (telegramWeeklyReportEnabled) {
13658
13680
  }
13659
13681
 
13660
13682
  // ── Self-updating: poll npm every 10 min, auto-install + restart ────────────
13683
+ const isDaemonChildForAutoUpdate =
13684
+ process.argv.includes("--daemon-child") || process.env.BOSUN_DAEMON === "1";
13661
13685
  startAutoUpdateLoop({
13662
13686
  onRestart: (reason) => restartSelf(reason),
13663
13687
  onNotify: (msg) =>
13664
13688
  // Priority 1 (critical) bypasses the live digest so the user gets a
13665
13689
  // direct push notification for update-detected and restarting events.
13666
13690
  sendTelegramMessage(msg, { priority: 1, skipDedup: true }).catch(() => {}),
13691
+ trackParent: !isDaemonChildForAutoUpdate,
13667
13692
  });
13668
13693
 
13669
13694
  startWatcher();
@@ -13827,13 +13852,16 @@ let errorDetector = null;
13827
13852
  let agentSupervisor = null;
13828
13853
 
13829
13854
  if (!isMonitorTestRuntime) {
13830
- if (workflowAutomationEnabled) {
13831
- await ensureWorkflowAutomationEngine().catch(() => {});
13832
- } else {
13833
- console.log(
13834
- "[workflows] automation disabled (set WORKFLOW_AUTOMATION_ENABLED=true to enable event-driven workflow triggers)",
13835
- );
13836
- }
13855
+ if (workflowAutomationEnabled) {
13856
+ await ensureWorkflowAutomationEngine().catch(() => {});
13857
+ void pollWorkflowSchedulesOnce("startup", { includeTaskPoll: false }).catch((err) => {
13858
+ console.warn(`[workflows] startup poll error: ${err?.message || err}`);
13859
+ });
13860
+ } else {
13861
+ console.log(
13862
+ "[workflows] automation disabled (set WORKFLOW_AUTOMATION_ENABLED=true to enable event-driven workflow triggers)",
13863
+ );
13864
+ }
13837
13865
  // ── Task Management Subsystem Initialization ────────────────────────────────
13838
13866
  try {
13839
13867
  mkdirSync(monitorStateCacheDir, { recursive: true });
@@ -14070,6 +14098,11 @@ if (isExecutorDisabled()) {
14070
14098
  };
14071
14099
  internalTaskExecutor = getTaskExecutor(execOpts);
14072
14100
  internalTaskExecutor.start();
14101
+ if (workflowOwnsTaskExecutorLifecycle) {
14102
+ void pollWorkflowSchedulesOnce("startup").catch((err) => {
14103
+ console.warn(`[workflows] startup poll error: ${err?.message || err}`);
14104
+ });
14105
+ }
14073
14106
 
14074
14107
  // Write executor slots to status file every 30s for Telegram /tasks
14075
14108
  startStatusFileWriter(30000);
@@ -14634,5 +14667,3 @@ export {
14634
14667
  // Workflow event bridge — for fleet/kanban modules to emit events
14635
14668
  queueWorkflowEvent,
14636
14669
  };
14637
-
14638
-
@@ -551,6 +551,7 @@ export class SessionTracker {
551
551
  status: s.status,
552
552
  workspaceId: String(s?.metadata?.workspaceId || "").trim() || null,
553
553
  workspaceDir: String(s?.metadata?.workspaceDir || "").trim() || null,
554
+ branch: String(s?.metadata?.branch || "").trim() || null,
554
555
  turnCount: s.turnCount || 0,
555
556
  createdAt: s.createdAt || new Date(s.startedAt).toISOString(),
556
557
  lastActiveAt: s.lastActiveAt || new Date(s.lastActivityAt).toISOString(),
@@ -1018,8 +1018,13 @@ export class SyncEngine {
1018
1018
  }
1019
1019
 
1020
1020
  async #createExternalTask(task, baseBranchCandidate = null) {
1021
+ const normalizedTitle = String(task?.title || "").trim();
1022
+ if (!normalizedTitle) {
1023
+ throw new Error(`cannot create external task for ${task?.id || "<unknown>"}: missing title`);
1024
+ }
1025
+
1021
1026
  const payload = {
1022
- title: task.title || "Untitled task",
1027
+ title: normalizedTitle,
1023
1028
  description: task.description || "",
1024
1029
  status: "todo",
1025
1030
  assignee: task.assignee || null,
@@ -554,6 +554,7 @@ function isSuppressedStreamNoiseError(err) {
554
554
  * @param {function} [opts.onNotify] - Called with message string for Telegram/log
555
555
  * @param {number} [opts.intervalMs] - Poll interval (default: 10 min)
556
556
  * @param {number} [opts.parentPid] - Parent process PID to monitor (default: process.ppid)
557
+ * @param {boolean} [opts.trackParent] - Monitor parent liveness (default: true)
557
558
  */
558
559
  export function startAutoUpdateLoop(opts = {}) {
559
560
  if (process.env.BOSUN_SKIP_AUTO_UPDATE === "1") {
@@ -594,14 +595,21 @@ export function startAutoUpdateLoop(opts = {}) {
594
595
 
595
596
  // Register cleanup handlers to prevent zombie processes
596
597
  registerCleanupHandlers();
597
-
598
- // Track parent process if provided
599
- if (opts.parentPid) {
600
- parentPid = opts.parentPid;
601
- console.log(`[auto-update] Monitoring parent process PID ${parentPid}`);
598
+ const trackParent = opts.trackParent !== false;
599
+
600
+ // Track parent process when enabled. Detached daemon-child monitor sessions
601
+ // should opt out so they do not self-terminate when the launcher exits.
602
+ if (trackParent) {
603
+ if (opts.parentPid) {
604
+ parentPid = opts.parentPid;
605
+ console.log(`[auto-update] Monitoring parent process PID ${parentPid}`);
606
+ } else {
607
+ parentPid = process.ppid; // Track parent by default
608
+ console.log(`[auto-update] Monitoring parent process PID ${parentPid}`);
609
+ }
602
610
  } else {
603
- parentPid = process.ppid; // Track parent by default
604
- console.log(`[auto-update] Monitoring parent process PID ${parentPid}`);
611
+ parentPid = null;
612
+ console.log("[auto-update] Parent process monitoring disabled");
605
613
  }
606
614
 
607
615
  console.log(
@@ -792,11 +792,18 @@ class InternalAdapter {
792
792
  taskUrl: task.taskUrl || null,
793
793
  createdAt: task.createdAt || null,
794
794
  updatedAt: task.updatedAt || null,
795
+ lastActivityAt: task.lastActivityAt || task.updatedAt || null,
796
+ timeline: Array.isArray(task.timeline) ? task.timeline : [],
797
+ workflowRuns: Array.isArray(task.workflowRuns) ? task.workflowRuns : [],
798
+ statusHistory: Array.isArray(task.statusHistory) ? task.statusHistory : [],
795
799
  backend: "internal",
796
800
  attachments: mergedAttachments,
797
801
  comments: normalizedComments,
798
802
  meta: {
799
803
  ...(task.meta || {}),
804
+ timeline: Array.isArray(task.timeline) ? task.timeline : (Array.isArray(task.meta?.timeline) ? task.meta.timeline : []),
805
+ workflowRuns: Array.isArray(task.workflowRuns) ? task.workflowRuns : (Array.isArray(task.meta?.workflowRuns) ? task.meta.workflowRuns : []),
806
+ statusHistory: Array.isArray(task.statusHistory) ? task.statusHistory : (Array.isArray(task.meta?.statusHistory) ? task.meta.statusHistory : []),
800
807
  comments: normalizedComments,
801
808
  attachments: mergedAttachments,
802
809
  },
@@ -3501,6 +3508,10 @@ class GitHubIssuesAdapter {
3501
3508
  }
3502
3509
 
3503
3510
  async addProjectV2DraftIssue(projectNumber, title, body = "") {
3511
+ const normalizedTitle = String(title || "").trim();
3512
+ if (!normalizedTitle) {
3513
+ throw new Error("[kanban] github createTask requires non-empty title");
3514
+ }
3504
3515
  const projectId = await this.getProjectNodeId(projectNumber);
3505
3516
  if (!projectId) return null;
3506
3517
  const mutation = `
@@ -3508,7 +3519,7 @@ class GitHubIssuesAdapter {
3508
3519
  addProjectV2DraftIssue(
3509
3520
  input: {
3510
3521
  projectId: ${this._escapeGraphQLString(projectId)},
3511
- title: ${this._escapeGraphQLString(title || "New task")},
3522
+ title: ${this._escapeGraphQLString(normalizedTitle)},
3512
3523
  body: ${this._escapeGraphQLString(body)}
3513
3524
  }
3514
3525
  ) {
@@ -3550,6 +3561,10 @@ class GitHubIssuesAdapter {
3550
3561
  }
3551
3562
 
3552
3563
  async createTask(_projectId, taskData) {
3564
+ const normalizedTitle = String(taskData?.title || "").trim();
3565
+ if (!normalizedTitle) {
3566
+ throw new Error("[kanban] github createTask requires non-empty title");
3567
+ }
3553
3568
  const wantsDraftCreate = Boolean(taskData?.draft || taskData?.createDraft);
3554
3569
  const shouldConvertDraft = Boolean(
3555
3570
  taskData?.convertDraft || taskData?.convertToIssue,
@@ -3563,7 +3578,7 @@ class GitHubIssuesAdapter {
3563
3578
  if (wantsDraftCreate && projectNumber) {
3564
3579
  const draftItemId = await this.addProjectV2DraftIssue(
3565
3580
  projectNumber,
3566
- taskData.title || "New task",
3581
+ normalizedTitle,
3567
3582
  taskData.description || "",
3568
3583
  );
3569
3584
  if (!draftItemId) {
@@ -3572,7 +3587,7 @@ class GitHubIssuesAdapter {
3572
3587
  if (!shouldConvertDraft) {
3573
3588
  return {
3574
3589
  id: `draft:${draftItemId}`,
3575
- title: taskData.title || "New task",
3590
+ title: normalizedTitle,
3576
3591
  description: taskData.description || "",
3577
3592
  status: requestedStatus,
3578
3593
  assignee: null,
@@ -3665,7 +3680,7 @@ class GitHubIssuesAdapter {
3665
3680
  "--repo",
3666
3681
  `${this._owner}/${this._repo}`,
3667
3682
  "--title",
3668
- taskData.title || "New task",
3683
+ normalizedTitle,
3669
3684
  "--body",
3670
3685
  taskData.description || "",
3671
3686
  ];
@@ -640,6 +640,30 @@ function Get-OrchestratorMutexName {
640
640
  return "$BaseName.$suffix"
641
641
  }
642
642
 
643
+ function Repair-MainRepoGitConfigCorruption {
644
+ $repoRoot = $null
645
+ try {
646
+ $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
647
+ }
648
+ catch {
649
+ return
650
+ }
651
+ if (-not $repoRoot) {
652
+ return
653
+ }
654
+ try {
655
+ $bareValue = git -C $repoRoot config --bool --get core.bare 2>$null
656
+ if ($LASTEXITCODE -eq 0 -and "$bareValue".Trim().ToLowerInvariant() -eq "true") {
657
+ Write-Log "Detected core.bare=true on main repo; repairing git config corruption" -Level "WARN"
658
+ git -C $repoRoot config --local core.bare false 2>&1 | Out-Null
659
+ git -C $repoRoot config --local --unset-all core.worktree 2>&1 | Out-Null
660
+ }
661
+ }
662
+ catch {
663
+ Write-Log "Failed to repair main repo git config corruption: $($_.Exception.Message)" -Level "WARN"
664
+ }
665
+ }
666
+
643
667
  function Enter-OrchestratorMutex {
644
668
  param([string]$Name = (Get-OrchestratorMutexName))
645
669
  $createdNew = $false
@@ -3274,6 +3298,7 @@ function Invoke-DirectRebaseIsolatedWorktree {
3274
3298
  throw "git worktree add failed: $addOut"
3275
3299
  }
3276
3300
  $addedWorktree = $true
3301
+ Repair-MainRepoGitConfigCorruption
3277
3302
 
3278
3303
  # Hard-clean the isolated worktree to remove any stale filesystem residue.
3279
3304
  git -C $tempWorktreePath reset --hard HEAD 2>&1 | Out-Null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.40.3",
3
+ "version": "0.40.5",
4
4
  "description": "Bosun Autonomous Engineering — manages AI agent executors with failover, extremely powerful workflow builder, and a massive amount of included default workflow templates for autonomous engineering, 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",
@@ -129,6 +129,7 @@
129
129
  "bench:swebench:import": "node bench/swebench/bosun-swebench.mjs import",
130
130
  "bench:swebench:export": "node bench/swebench/bosun-swebench.mjs export",
131
131
  "bench:swebench:eval": "node bench/swebench/bosun-swebench.mjs eval",
132
+ "bench:library:resolver": "node bench/library/library-resolver-bench.mjs",
132
133
  "mutate": "npx stryker run",
133
134
  "mutate:incremental": "npx stryker run --incremental",
134
135
  "mutate:report": "node scripts/mutation-report.mjs",