bosun 0.37.0 → 0.37.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 (43) hide show
  1. package/.env.example +4 -1
  2. package/agent-tool-config.mjs +338 -0
  3. package/bosun-skills.mjs +59 -4
  4. package/bosun.schema.json +1 -1
  5. package/desktop/launch.mjs +18 -0
  6. package/desktop/main.mjs +52 -13
  7. package/fleet-coordinator.mjs +34 -1
  8. package/kanban-adapter.mjs +30 -3
  9. package/library-manager.mjs +66 -0
  10. package/maintenance.mjs +30 -5
  11. package/monitor.mjs +56 -0
  12. package/package.json +4 -1
  13. package/setup-web-server.mjs +73 -12
  14. package/setup.mjs +3 -3
  15. package/ui/app.js +40 -3
  16. package/ui/components/session-list.js +25 -7
  17. package/ui/components/workspace-switcher.js +48 -1
  18. package/ui/demo.html +176 -0
  19. package/ui/modules/mic-track-registry.js +83 -0
  20. package/ui/modules/settings-schema.js +4 -1
  21. package/ui/modules/state.js +25 -0
  22. package/ui/modules/streaming.js +1 -1
  23. package/ui/modules/voice-barge-in.js +27 -0
  24. package/ui/modules/voice-client-sdk.js +268 -42
  25. package/ui/modules/voice-client.js +665 -61
  26. package/ui/modules/voice-overlay.js +829 -47
  27. package/ui/setup.html +151 -9
  28. package/ui/styles.css +258 -0
  29. package/ui/tabs/chat.js +11 -0
  30. package/ui/tabs/library.js +890 -15
  31. package/ui/tabs/settings.js +51 -11
  32. package/ui/tabs/telemetry.js +327 -105
  33. package/ui/tabs/workflows.js +86 -0
  34. package/ui-server.mjs +1201 -107
  35. package/voice-action-dispatcher.mjs +81 -0
  36. package/voice-agents-sdk.mjs +2 -2
  37. package/voice-relay.mjs +131 -14
  38. package/voice-tools.mjs +475 -9
  39. package/workflow-engine.mjs +54 -0
  40. package/workflow-nodes.mjs +177 -28
  41. package/workflow-templates/github.mjs +205 -94
  42. package/workflow-templates/task-batch.mjs +247 -0
  43. package/workflow-templates.mjs +15 -0
@@ -798,7 +798,9 @@ registerNodeType("trigger.manual", {
798
798
  });
799
799
 
800
800
  registerNodeType("trigger.task_low", {
801
- describe: () => "Fires when backlog task count drops below threshold",
801
+ describe: () =>
802
+ "Fires when backlog task count drops below threshold. Self-queries kanban " +
803
+ "when todoCount is not pre-populated in context data.",
802
804
  schema: {
803
805
  type: "object",
804
806
  properties: {
@@ -809,7 +811,29 @@ registerNodeType("trigger.task_low", {
809
811
  },
810
812
  async execute(node, ctx) {
811
813
  const threshold = node.config?.threshold ?? 3;
812
- const todoCount = ctx.data?.todoCount ?? ctx.data?.backlogCount ?? 0;
814
+ const status = node.config?.status ?? "todo";
815
+ let todoCount = ctx.data?.todoCount ?? ctx.data?.backlogCount ?? null;
816
+
817
+ // Self-query kanban if todoCount not pre-populated
818
+ if (todoCount == null) {
819
+ try {
820
+ const projectId = cfgOrCtx(node, ctx, "projectId") || undefined;
821
+ const kanban = ctx.data?._services?.kanban;
822
+ let tasks;
823
+ if (kanban?.listTasks) {
824
+ tasks = await kanban.listTasks(projectId, { status });
825
+ } else {
826
+ const ka = await ensureKanbanAdapterMod();
827
+ tasks = await ka.listTasks(projectId, { status });
828
+ }
829
+ todoCount = Array.isArray(tasks) ? tasks.length : 0;
830
+ ctx.log(node.id, `Self-queried kanban: ${todoCount} task(s) with status "${status}"`);
831
+ } catch (err) {
832
+ ctx.log(node.id, `Kanban query failed: ${err?.message || err} — using 0`);
833
+ todoCount = 0;
834
+ }
835
+ }
836
+
813
837
  const triggered = todoCount < threshold;
814
838
  ctx.log(node.id, `Task count: ${todoCount}, threshold: ${threshold}, triggered: ${triggered}`);
815
839
  return { triggered, todoCount, threshold };
@@ -2322,7 +2346,9 @@ registerNodeType("action.git_operations", {
2322
2346
  });
2323
2347
 
2324
2348
  registerNodeType("action.create_pr", {
2325
- describe: () => "Hand off pull-request lifecycle to Bosun management (direct creation disabled)",
2349
+ describe: () =>
2350
+ "Create a pull request via GitHub CLI. Falls back to Bosun-managed handoff " +
2351
+ "when gh is unavailable or the operation fails with failOnError=false.",
2326
2352
  schema: {
2327
2353
  type: "object",
2328
2354
  properties: {
@@ -2330,10 +2356,18 @@ registerNodeType("action.create_pr", {
2330
2356
  body: { type: "string", description: "PR body" },
2331
2357
  base: { type: "string", description: "Base branch" },
2332
2358
  baseBranch: { type: "string", description: "Legacy alias for base branch" },
2333
- branch: { type: "string", description: "Head branch for Bosun lifecycle handoff context" },
2359
+ branch: { type: "string", description: "Head branch (source)" },
2334
2360
  draft: { type: "boolean", default: false },
2361
+ labels: {
2362
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
2363
+ description: "Comma-separated or array of labels",
2364
+ },
2365
+ reviewers: {
2366
+ oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
2367
+ description: "Comma-separated or array of reviewer handles",
2368
+ },
2335
2369
  cwd: { type: "string" },
2336
- failOnError: { type: "boolean", default: false, description: "Retained for compatibility; direct PR commands are disabled" },
2370
+ failOnError: { type: "boolean", default: false, description: "If true, throw on gh failure instead of falling back" },
2337
2371
  },
2338
2372
  required: ["title"],
2339
2373
  },
@@ -2342,23 +2376,77 @@ registerNodeType("action.create_pr", {
2342
2376
  const body = ctx.resolve(node.config?.body || "");
2343
2377
  const base = ctx.resolve(node.config?.base || node.config?.baseBranch || "main");
2344
2378
  const branch = ctx.resolve(node.config?.branch || "");
2379
+ const draft = node.config?.draft === true;
2380
+ const failOnError = node.config?.failOnError === true;
2345
2381
  const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
2346
- ctx.log(
2347
- node.id,
2348
- `PR lifecycle handoff recorded for "${title}" (direct PR commands are disabled)`,
2349
- );
2350
- return {
2351
- success: true,
2352
- handedOff: true,
2353
- lifecycle: "bosun_managed",
2354
- action: "pr_handoff",
2355
- message: "Direct PR commands are disabled; Bosun manages pull-request lifecycle.",
2356
- title,
2357
- body,
2358
- base,
2359
- branch: branch || null,
2360
- cwd,
2382
+
2383
+ // Normalize labels/reviewers to arrays
2384
+ const toList = (v) => {
2385
+ if (!v) return [];
2386
+ if (Array.isArray(v)) return v.map(String).filter(Boolean);
2387
+ return String(v).split(",").map((s) => s.trim()).filter(Boolean);
2361
2388
  };
2389
+ const labels = toList(ctx.resolve(node.config?.labels || ""));
2390
+ const reviewers = toList(ctx.resolve(node.config?.reviewers || ""));
2391
+
2392
+ // Build gh pr create command
2393
+ const args = ["gh", "pr", "create"];
2394
+ args.push("--title", JSON.stringify(title));
2395
+ if (body) args.push("--body", JSON.stringify(body));
2396
+ if (base) args.push("--base", base);
2397
+ if (branch) args.push("--head", branch);
2398
+ if (draft) args.push("--draft");
2399
+ if (labels.length) args.push("--label", labels.join(","));
2400
+ if (reviewers.length) args.push("--reviewer", reviewers.join(","));
2401
+
2402
+ const cmd = args.join(" ");
2403
+ ctx.log(node.id, `Creating PR: ${cmd}`);
2404
+
2405
+ try {
2406
+ const output = execSync(cmd, { cwd, encoding: "utf8", timeout: 60000 });
2407
+ const trimmed = (output || "").trim();
2408
+ // gh pr create prints the PR URL on success
2409
+ const urlMatch = trimmed.match(/https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/);
2410
+ const prNumber = urlMatch ? parseInt(urlMatch[1], 10) : null;
2411
+ const prUrl = urlMatch ? urlMatch[0] : trimmed;
2412
+ ctx.log(node.id, `PR created: ${prUrl}`);
2413
+ return {
2414
+ success: true,
2415
+ prUrl,
2416
+ prNumber,
2417
+ title,
2418
+ base,
2419
+ branch: branch || null,
2420
+ draft,
2421
+ labels,
2422
+ reviewers,
2423
+ output: trimmed,
2424
+ };
2425
+ } catch (err) {
2426
+ const errorMsg = err?.stderr?.toString?.()?.trim() || err?.message || String(err);
2427
+ ctx.log(node.id, `PR creation failed: ${errorMsg}`);
2428
+ if (failOnError) {
2429
+ return { success: false, error: errorMsg, command: cmd };
2430
+ }
2431
+ // Graceful fallback — record handoff for Bosun management
2432
+ ctx.log(node.id, `Falling back to Bosun-managed PR lifecycle handoff`);
2433
+ return {
2434
+ success: true,
2435
+ handedOff: true,
2436
+ lifecycle: "bosun_managed",
2437
+ action: "pr_handoff",
2438
+ message: "gh CLI failed; Bosun manages pull-request lifecycle.",
2439
+ title,
2440
+ body,
2441
+ base,
2442
+ branch: branch || null,
2443
+ draft,
2444
+ labels,
2445
+ reviewers,
2446
+ cwd,
2447
+ ghError: errorMsg,
2448
+ };
2449
+ }
2362
2450
  },
2363
2451
  });
2364
2452
 
@@ -3484,17 +3572,23 @@ registerNodeType("flow.gate", {
3484
3572
  // ═══════════════════════════════════════════════════════════════════════════
3485
3573
 
3486
3574
  registerNodeType("loop.for_each", {
3487
- describe: () => "Iterate over an array, executing downstream nodes for each item",
3575
+ describe: () =>
3576
+ "Iterate over an array, executing a sub-workflow for each item. " +
3577
+ "Supports parallel fan-out via maxConcurrent and provides per-item " +
3578
+ "context injection under the configured variable name.",
3488
3579
  schema: {
3489
3580
  type: "object",
3490
3581
  properties: {
3491
3582
  items: { type: "string", description: "Expression that resolves to an array" },
3492
3583
  variable: { type: "string", default: "item", description: "Variable name for current item" },
3493
- maxIterations: { type: "number", default: 50 },
3584
+ indexVariable: { type: "string", default: "index", description: "Variable name for current index" },
3585
+ maxIterations: { type: "number", default: 50, description: "Cap on total iterations" },
3586
+ maxConcurrent: { type: "number", default: 1, description: "Parallel fan-out width (1 = sequential)" },
3587
+ workflowId: { type: "string", description: "Sub-workflow to execute for each item (optional)" },
3494
3588
  },
3495
3589
  required: ["items"],
3496
3590
  },
3497
- async execute(node, ctx) {
3591
+ async execute(node, ctx, engine) {
3498
3592
  const expr = node.config?.items || "[]";
3499
3593
  let items;
3500
3594
  try {
@@ -3507,12 +3601,65 @@ registerNodeType("loop.for_each", {
3507
3601
  const max = node.config?.maxIterations || 50;
3508
3602
  items = items.slice(0, max);
3509
3603
  const varName = node.config?.variable || "item";
3604
+ const indexVar = node.config?.indexVariable || "index";
3605
+ const maxConcurrent = Math.max(1, node.config?.maxConcurrent || 1);
3606
+ const subWorkflowId = node.config?.workflowId || "";
3510
3607
 
3511
- // Store items for downstream processing
3608
+ // Store items for downstream processing (backward compat)
3512
3609
  ctx.data[`_loop_${node.id}_items`] = items;
3513
3610
  ctx.data[`_loop_${node.id}_count`] = items.length;
3514
3611
 
3515
- return { items, count: items.length, variable: varName };
3612
+ const results = [];
3613
+
3614
+ // If a sub-workflow is specified, fan-out execution across items
3615
+ if (subWorkflowId && engine?.execute) {
3616
+ ctx.log(node.id, `Fan-out: ${items.length} item(s), concurrency=${maxConcurrent}, workflow=${subWorkflowId}`);
3617
+
3618
+ // Process items in batches of maxConcurrent
3619
+ for (let batchStart = 0; batchStart < items.length; batchStart += maxConcurrent) {
3620
+ const batch = items.slice(batchStart, batchStart + maxConcurrent);
3621
+ const batchPromises = batch.map(async (item, batchIdx) => {
3622
+ const itemIndex = batchStart + batchIdx;
3623
+ const itemData = {
3624
+ ...ctx.data,
3625
+ [varName]: item,
3626
+ [indexVar]: itemIndex,
3627
+ _loopParentNodeId: node.id,
3628
+ _loopIteration: itemIndex,
3629
+ _loopTotal: items.length,
3630
+ };
3631
+ try {
3632
+ const runCtx = await engine.execute(subWorkflowId, itemData);
3633
+ const ok = !runCtx?.errors?.length;
3634
+ return { index: itemIndex, item, success: ok, runId: runCtx?.id || null };
3635
+ } catch (err) {
3636
+ return { index: itemIndex, item, success: false, error: err?.message || String(err) };
3637
+ }
3638
+ });
3639
+ const batchResults = await Promise.all(batchPromises);
3640
+ results.push(...batchResults);
3641
+ }
3642
+ } else {
3643
+ // No sub-workflow — store items for downstream node access (legacy mode)
3644
+ for (let i = 0; i < items.length; i++) {
3645
+ ctx.data[varName] = items[i];
3646
+ ctx.data[indexVar] = i;
3647
+ results.push({ index: i, item: items[i], success: true });
3648
+ }
3649
+ }
3650
+
3651
+ const successCount = results.filter((r) => r.success).length;
3652
+ const failCount = results.length - successCount;
3653
+ ctx.log(node.id, `Loop complete: ${successCount} succeeded, ${failCount} failed out of ${items.length}`);
3654
+
3655
+ return {
3656
+ items,
3657
+ count: items.length,
3658
+ variable: varName,
3659
+ results,
3660
+ successCount,
3661
+ failCount,
3662
+ };
3516
3663
  },
3517
3664
  });
3518
3665
 
@@ -5088,17 +5235,19 @@ registerNodeType("action.detect_new_commits", {
5088
5235
  schema: {
5089
5236
  type: "object",
5090
5237
  properties: {
5091
- worktreePath: { type: "string", description: "Worktree path" },
5238
+ worktreePath: { type: "string", description: "Worktree path (soft-fails if not set)" },
5092
5239
  preExecHead: { type: "string", description: "HEAD hash before agent (auto from ctx)" },
5093
5240
  baseBranch: { type: "string", description: "Base branch for diff stats" },
5094
5241
  },
5095
- required: ["worktreePath"],
5096
5242
  },
5097
5243
  async execute(node, ctx) {
5098
5244
  const worktreePath = cfgOrCtx(node, ctx, "worktreePath");
5099
5245
  const baseBranch = cfgOrCtx(node, ctx, "baseBranch", "origin/main");
5100
5246
 
5101
- if (!worktreePath) throw new Error("action.detect_new_commits: worktreePath is required");
5247
+ if (!worktreePath) {
5248
+ ctx.log(node.id, "action.detect_new_commits: worktreePath not set — skipping commit detection");
5249
+ return { success: false, error: "worktreePath required", hasCommits: false, hasNewCommits: false, unpushedCount: 0 };
5250
+ }
5102
5251
 
5103
5252
  // Read preExecHead from record-head node output or ctx
5104
5253
  const preExecHead = cfgOrCtx(node, ctx, "preExecHead")