bosun 0.41.0 → 0.41.2

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.
Files changed (64) hide show
  1. package/.env.example +8 -0
  2. package/README.md +20 -0
  3. package/agent/agent-event-bus.mjs +248 -6
  4. package/agent/agent-pool.mjs +125 -28
  5. package/agent/agent-work-analyzer.mjs +8 -16
  6. package/agent/retry-queue.mjs +164 -0
  7. package/bosun.config.example.json +25 -0
  8. package/bosun.schema.json +825 -183
  9. package/cli.mjs +59 -5
  10. package/config/config.mjs +130 -3
  11. package/infra/monitor.mjs +693 -67
  12. package/infra/runtime-accumulator.mjs +376 -84
  13. package/infra/session-tracker.mjs +82 -25
  14. package/lib/codebase-audit.mjs +133 -18
  15. package/package.json +23 -4
  16. package/server/setup-web-server.mjs +25 -0
  17. package/server/ui-server.mjs +248 -29
  18. package/setup.mjs +27 -24
  19. package/shell/codex-shell.mjs +34 -3
  20. package/shell/copilot-shell.mjs +50 -8
  21. package/task/msg-hub.mjs +193 -0
  22. package/task/pipeline.mjs +544 -0
  23. package/task/task-cli.mjs +38 -2
  24. package/task/task-executor-pipeline.mjs +143 -0
  25. package/task/task-executor.mjs +36 -27
  26. package/telegram/get-telegram-chat-id.mjs +57 -47
  27. package/ui/components/workspace-switcher.js +7 -7
  28. package/ui/demo-defaults.js +15694 -10573
  29. package/ui/modules/settings-schema.js +2 -0
  30. package/ui/modules/state.js +54 -57
  31. package/ui/modules/voice-client-sdk.js +375 -36
  32. package/ui/modules/voice-client.js +140 -31
  33. package/ui/setup.html +68 -2
  34. package/ui/styles/components.css +57 -0
  35. package/ui/styles.css +201 -1
  36. package/ui/tabs/dashboard.js +74 -0
  37. package/ui/tabs/logs.js +10 -0
  38. package/ui/tabs/settings.js +178 -99
  39. package/ui/tabs/tasks.js +31 -1
  40. package/ui/tabs/telemetry.js +34 -0
  41. package/ui/tabs/workflow-canvas-utils.mjs +8 -1
  42. package/ui/tabs/workflows.js +532 -275
  43. package/voice/voice-agents-sdk.mjs +1 -1
  44. package/voice/voice-relay.mjs +6 -6
  45. package/workflow/declarative-workflows.mjs +145 -0
  46. package/workflow/msg-hub.mjs +237 -0
  47. package/workflow/pipeline-workflows.mjs +287 -0
  48. package/workflow/pipeline.mjs +828 -315
  49. package/workflow/workflow-cli.mjs +128 -0
  50. package/workflow/workflow-engine.mjs +329 -17
  51. package/workflow/workflow-nodes/custom-loader.mjs +250 -0
  52. package/workflow/workflow-nodes.mjs +1955 -223
  53. package/workflow/workflow-templates.mjs +26 -8
  54. package/workflow-templates/agents.mjs +0 -1
  55. package/workflow-templates/bosun-native.mjs +212 -2
  56. package/workflow-templates/continuation-loop.mjs +339 -0
  57. package/workflow-templates/github.mjs +516 -40
  58. package/workflow-templates/planning.mjs +446 -17
  59. package/workflow-templates/reliability.mjs +65 -12
  60. package/workflow-templates/task-batch.mjs +24 -8
  61. package/workflow-templates/task-lifecycle.mjs +83 -6
  62. package/workspace/context-cache.mjs +66 -18
  63. package/workspace/workspace-manager.mjs +2 -1
  64. package/workflow-templates/issue-continuation.mjs +0 -243
package/cli.mjs CHANGED
@@ -77,12 +77,14 @@ function showHelp() {
77
77
  bosun [options]
78
78
 
79
79
  COMMANDS
80
- audit <command> [options] Codebase annotation audit workflows (scan/generate/warn/manifest/index/trim/conformity/migrate)
81
- --setup Launch the web-based setup wizard (default)
80
+ workflow list List declarative pipeline workflows
81
+ workflow run <name> Run a declarative pipeline workflow
82
+ --setup Launch the web-based setup wizard (default)
82
83
  --setup-terminal Run the legacy terminal setup wizard
83
84
  --where Show the resolved bosun config directory
84
85
  --doctor Validate bosun .env/config setup
85
86
  --tool-log <ID|list|prune> Retrieve/list/prune cached tool outputs
87
+ node:create <name> Scaffold a custom workflow node in custom-nodes/
86
88
  --context-index [mode] Run context index workflow (run|status|search)
87
89
  --context-index-query <text> Query text for context index search mode
88
90
  --context-index-limit <n> Max results for context index search (default: 25)
@@ -159,6 +161,12 @@ function showHelp() {
159
161
 
160
162
  Run 'bosun task --help' for complete task CLI documentation and examples.
161
163
 
164
+ WORKFLOWS
165
+ workflow list List built-in and configured workflows
166
+ workflow run <name> Run a declarative fresh-context workflow
167
+
168
+ Run 'bosun workflow --help' for workflow CLI examples.
169
+
162
170
  VIBE-KANBAN
163
171
  --no-vk-spawn Don't auto-spawn Vibe-Kanban
164
172
  --vk-ensure-interval <ms> VK health check interval (default: 60000)
@@ -918,6 +926,19 @@ function daemonStatus() {
918
926
  }
919
927
  console.log(` Run --terminate to stop restart owners, then --daemon to restart.`);
920
928
  } else {
929
+ const existingMonitorOwner = detectExistingMonitorLockOwner();
930
+ if (existingMonitorOwner) {
931
+ console.log(
932
+ ` bosun daemon is not running in daemon mode, but bosun monitor is active (PID ${existingMonitorOwner.pid}).`,
933
+ );
934
+ console.log(
935
+ ` Bosun is running in monitor mode with lock file ${existingMonitorOwner.pidFile}.`,
936
+ );
937
+ console.log(
938
+ ` Use 'bosun --terminate' to stop it, or 'bosun --daemon' only after it is fully stopped.`,
939
+ );
940
+ process.exit(0);
941
+ }
921
942
  // Broader scan: portal, monitor, ui-server, etc. (non-daemon bosun processes)
922
943
  const allPids = findAllBosunProcessPids();
923
944
  if (allPids.length > 0) {
@@ -1321,7 +1342,21 @@ async function main() {
1321
1342
  process.exit(0);
1322
1343
  }
1323
1344
 
1324
- // Handle 'audit' subcommand before --help so command-specific help works.
1345
+ const workflowFlagIndex = args.indexOf("--workflow");
1346
+ const workflowCommandIndex =
1347
+ args[0] === "workflow"
1348
+ ? 0
1349
+ : args[0]?.startsWith("--")
1350
+ ? args.indexOf("workflow")
1351
+ : -1;
1352
+ if (workflowCommandIndex >= 0 || workflowFlagIndex >= 0) {
1353
+ const { runWorkflowCli } = await import("./workflow/workflow-cli.mjs");
1354
+ const commandStartIndex = workflowCommandIndex >= 0 ? workflowCommandIndex : workflowFlagIndex;
1355
+ const workflowArgs = args.slice(commandStartIndex + 1);
1356
+ await runWorkflowCli(workflowArgs);
1357
+ process.exit(0);
1358
+ }
1359
+
1325
1360
  const auditFlagIndex = args.indexOf("--audit");
1326
1361
  const auditCommandIndex =
1327
1362
  args[0] === "audit"
@@ -1333,8 +1368,27 @@ async function main() {
1333
1368
  const { runAuditCli } = await import("./lib/codebase-audit.mjs");
1334
1369
  const commandStartIndex = auditCommandIndex >= 0 ? auditCommandIndex : auditFlagIndex;
1335
1370
  const auditArgs = args.slice(commandStartIndex + 1);
1336
- const result = await runAuditCli(auditArgs);
1337
- process.exit(Number.isInteger(result?.exitCode) ? result.exitCode : 0);
1371
+ await runAuditCli(auditArgs);
1372
+ process.exit(0);
1373
+ }
1374
+
1375
+ if (args[0] === "node:create" || (args[0] === "node" && args[1] === "create")) {
1376
+ const name = args[0] === "node:create" ? args[1] : args[2];
1377
+ if (!name) {
1378
+ console.error("Usage: bosun node:create <name>");
1379
+ process.exit(1);
1380
+ }
1381
+ const { scaffoldCustomNodeFile } = await import("./workflow/workflow-nodes.mjs");
1382
+ try {
1383
+ const result = scaffoldCustomNodeFile(name, { repoRoot: runtimeRepoRoot });
1384
+ console.log(`\n ✓ Created custom node \"${result.type}\"`);
1385
+ console.log(` File: ${result.filePath}`);
1386
+ console.log("");
1387
+ } catch (err) {
1388
+ console.error(` Error: ${err.message}`);
1389
+ process.exit(1);
1390
+ }
1391
+ process.exit(0);
1338
1392
  }
1339
1393
 
1340
1394
  // Handle --help
package/config/config.mjs CHANGED
@@ -26,12 +26,13 @@ import {
26
26
  } from "../agent/agent-prompts.mjs";
27
27
  import { resolveAgentRepoRoot, resolveRepoLocalBosunDir } from "./repo-root.mjs";
28
28
  import { applyAllCompatibility } from "../compat.mjs";
29
+ import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
29
30
  import {
30
31
  normalizeExecutorKey,
31
32
  getModelsForExecutor,
32
33
  MODEL_ALIASES,
33
34
  } from "../task/task-complexity.mjs";
34
- import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
35
+ import { normalizePipelineWorkflows } from "../workflow/pipeline-workflows.mjs";
35
36
 
36
37
  const __dirname = dirname(fileURLToPath(import.meta.url));
37
38
 
@@ -635,6 +636,86 @@ function resolveTriggerSystemConfig(configData, defaults) {
635
636
  });
636
637
  }
637
638
 
639
+ function toBoundedInt(value, fallback, min, max) {
640
+ const parsed = Number(value);
641
+ if (!Number.isFinite(parsed)) return fallback;
642
+ const rounded = Math.trunc(parsed);
643
+ return Math.min(max, Math.max(min, rounded));
644
+ }
645
+
646
+ function normalizeStatusList(rawStates) {
647
+ const source = Array.isArray(rawStates)
648
+ ? rawStates
649
+ : String(rawStates || "").split(",");
650
+ const values = [];
651
+ for (const raw of source) {
652
+ const normalized = String(raw || "").trim().toLowerCase();
653
+ if (!normalized || values.includes(normalized)) continue;
654
+ values.push(normalized);
655
+ }
656
+ return values.length > 0 ? values : ["done", "cancelled"];
657
+ }
658
+
659
+ function resolveWorkflowConfig(configData = {}) {
660
+ const rawWorkflows = configData?.workflows;
661
+ const rawEntries = Array.isArray(rawWorkflows) ? rawWorkflows : [];
662
+ const normalized = [];
663
+
664
+ for (const rawEntry of rawEntries) {
665
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
666
+ const type = String(rawEntry.type || "").trim().toLowerCase();
667
+ if (!type) continue;
668
+
669
+ const entryBase = {
670
+ type,
671
+ enabled: rawEntry.enabled !== false,
672
+ name: String(rawEntry.name || "").trim() || null,
673
+ };
674
+
675
+ if (type !== "continuation-loop") {
676
+ normalized.push(Object.freeze(entryBase));
677
+ continue;
678
+ }
679
+
680
+ const onStuckRaw = String(rawEntry.onStuck || "escalate").trim().toLowerCase();
681
+ const onStuck = ["retry", "escalate", "pause"].includes(onStuckRaw)
682
+ ? onStuckRaw
683
+ : "escalate";
684
+
685
+ normalized.push(Object.freeze({
686
+ ...entryBase,
687
+ taskId: String(rawEntry.taskId || "").trim(),
688
+ worktreePath: String(rawEntry.worktreePath || "").trim(),
689
+ maxTurns: toBoundedInt(rawEntry.maxTurns, 8, 1, 1000),
690
+ pollIntervalMs: toBoundedInt(rawEntry.pollIntervalMs, 30000, 1000, 3600000),
691
+ terminalStates: normalizeStatusList(rawEntry.terminalStates),
692
+ stuckThresholdMs: toBoundedInt(rawEntry.stuckThresholdMs, 300000, 1000, 86400000),
693
+ onStuck,
694
+ continuePrompt: String(rawEntry.continuePrompt || "").trim(),
695
+ retryPrompt: String(rawEntry.retryPrompt || "").trim(),
696
+ sdk: String(rawEntry.sdk || "auto").trim() || "auto",
697
+ model: String(rawEntry.model || "").trim(),
698
+ timeoutMs: toBoundedInt(rawEntry.timeoutMs, 1800000, 1000, 21600000),
699
+ }));
700
+ }
701
+
702
+ // Support declarative pipeline workflow maps while keeping backward-compatible
703
+ // array semantics for continuation-loop style entries.
704
+ if (rawWorkflows && typeof rawWorkflows === "object" && !Array.isArray(rawWorkflows)) {
705
+ const namedWorkflows = normalizePipelineWorkflows(rawWorkflows);
706
+ for (const [workflowName, workflowDefinition] of Object.entries(namedWorkflows)) {
707
+ Object.defineProperty(normalized, workflowName, {
708
+ value: workflowDefinition,
709
+ enumerable: true,
710
+ configurable: false,
711
+ writable: false,
712
+ });
713
+ }
714
+ }
715
+
716
+ return Object.freeze(normalized);
717
+ }
718
+
638
719
  // ── Git helpers ──────────────────────────────────────────────────────────────
639
720
 
640
721
  function detectRepoSlug(repoRoot = "") {
@@ -1404,6 +1485,7 @@ export function loadConfig(argv = process.argv, options = {}) {
1404
1485
  process.env.BOSUN_LOAD_REPO_ENV_WITH_EXPLICIT_CONFIG,
1405
1486
  false,
1406
1487
  );
1488
+ const envOverride = reloadEnv || !isEnvEnabled(process.env.BOSUN_ENV_NO_OVERRIDE, false);
1407
1489
  let detectedRepoRoot = "";
1408
1490
  const getFallbackRepoRoot = () => {
1409
1491
  if (normalizedRepoRootOverride) return normalizedRepoRootOverride;
@@ -1412,10 +1494,23 @@ export function loadConfig(argv = process.argv, options = {}) {
1412
1494
  };
1413
1495
 
1414
1496
  // Determine config directory (where bosun stores its config)
1415
- const configDir =
1497
+ let configDir =
1416
1498
  explicitConfigDirRaw ||
1417
1499
  resolveConfigDir(normalizedRepoRootOverride);
1418
1500
 
1501
+ // If BOSUN_HOME/BOSUN_DIR is declared in the repo-local .env, load that first
1502
+ // and then pivot into the explicit config dir before reading config files.
1503
+ if (!hasExplicitConfigDir) {
1504
+ loadDotEnv(configDir, { override: envOverride });
1505
+ const envConfigDirRaw = process.env.BOSUN_HOME || process.env.BOSUN_DIR || "";
1506
+ if (String(envConfigDirRaw).trim()) {
1507
+ const resolvedEnvConfigDir = resolve(envConfigDirRaw);
1508
+ if (resolvedEnvConfigDir !== resolve(configDir)) {
1509
+ configDir = resolvedEnvConfigDir;
1510
+ }
1511
+ }
1512
+ }
1513
+
1419
1514
  const configFile = loadConfigFile(configDir);
1420
1515
  let configData = configFile.data || {};
1421
1516
  const configFileHadInvalidJson = configFile.error === "invalid-json";
@@ -1475,7 +1570,6 @@ export function loadConfig(argv = process.argv, options = {}) {
1475
1570
  // for Bosun-specific configuration, so it should override any stale shell
1476
1571
  // env vars. Users who want shell vars to take precedence can use profiles
1477
1572
  // or set BOSUN_ENV_NO_OVERRIDE=1.
1478
- const envOverride = reloadEnv || !isEnvEnabled(process.env.BOSUN_ENV_NO_OVERRIDE, false);
1479
1573
  loadDotEnv(configDir, { override: envOverride });
1480
1574
 
1481
1575
  const shouldLoadRepoEnv =
@@ -1963,6 +2057,17 @@ export function loadConfig(argv = process.argv, options = {}) {
1963
2057
  internalExecutorConfig.maxRetries ||
1964
2058
  2,
1965
2059
  ),
2060
+ retryReviewThreshold: Number(
2061
+ process.env.INTERNAL_EXECUTOR_RETRY_REVIEW_THRESHOLD ||
2062
+ internalExecutorConfig.retryReviewThreshold ||
2063
+ internalExecutorConfig.maxRetries ||
2064
+ 3,
2065
+ ),
2066
+ retryDelayMs: Number(
2067
+ process.env.INTERNAL_EXECUTOR_RETRY_DELAY_MS ||
2068
+ internalExecutorConfig.retryDelayMs ||
2069
+ 15000,
2070
+ ),
1966
2071
  autoCreatePr: internalExecutorConfig.autoCreatePr !== false,
1967
2072
  projectId:
1968
2073
  process.env.INTERNAL_EXECUTOR_PROJECT_ID ||
@@ -2105,6 +2210,7 @@ export function loadConfig(argv = process.argv, options = {}) {
2105
2210
  configData,
2106
2211
  triggerSystemDefaults,
2107
2212
  );
2213
+ const workflows = resolveWorkflowConfig(configData);
2108
2214
 
2109
2215
  // ── GitHub Reconciler ───────────────────────────────────
2110
2216
  const ghReconcileEnabled = isEnvEnabled(
@@ -2189,6 +2295,21 @@ export function loadConfig(argv = process.argv, options = {}) {
2189
2295
  assessWithSdk,
2190
2296
  });
2191
2297
 
2298
+ const workflowDefaults =
2299
+ configData.workflowDefaults && typeof configData.workflowDefaults === "object"
2300
+ ? {
2301
+ ...configData.workflowDefaults,
2302
+ templates: Array.isArray(configData.workflowDefaults.templates)
2303
+ ? [...configData.workflowDefaults.templates]
2304
+ : configData.workflowDefaults.templates,
2305
+ templateOverridesById:
2306
+ configData.workflowDefaults.templateOverridesById &&
2307
+ typeof configData.workflowDefaults.templateOverridesById === "object"
2308
+ ? { ...configData.workflowDefaults.templateOverridesById }
2309
+ : {},
2310
+ }
2311
+ : {};
2312
+
2192
2313
  // ── Fleet Coordination ─────────────────────────────────────
2193
2314
  // Multi-workstation collaboration: when 2+ bosun instances share
2194
2315
  // the same repo, the fleet system coordinates task planning, dispatch,
@@ -2371,6 +2492,7 @@ export function loadConfig(argv = process.argv, options = {}) {
2371
2492
  telegramVerbosity,
2372
2493
 
2373
2494
  triggerSystem,
2495
+ workflows,
2374
2496
 
2375
2497
  // GitHub Reconciler
2376
2498
  githubReconcile: {
@@ -2392,6 +2514,10 @@ export function loadConfig(argv = process.argv, options = {}) {
2392
2514
  // Fleet Coordination
2393
2515
  fleet,
2394
2516
 
2517
+ // Workflow template defaults + opt-in typed workflow entries
2518
+ workflowDefaults: Object.freeze(workflowDefaults),
2519
+ workflows,
2520
+
2395
2521
  // Paths
2396
2522
  statusPath,
2397
2523
  telegramPollLockPath,
@@ -2407,6 +2533,7 @@ export function loadConfig(argv = process.argv, options = {}) {
2407
2533
  workspacesDir,
2408
2534
  activeWorkspace,
2409
2535
  agentRepoRoot,
2536
+ workflows,
2410
2537
 
2411
2538
  // Agent prompts
2412
2539
  agentPrompts,