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.
- package/.env.example +4 -1
- package/agent-tool-config.mjs +338 -0
- package/bosun-skills.mjs +59 -4
- package/bosun.schema.json +1 -1
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +66 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +4 -1
- package/setup-web-server.mjs +73 -12
- package/setup.mjs +3 -3
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +176 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +4 -1
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +268 -42
- package/ui/modules/voice-client.js +665 -61
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +890 -15
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +86 -0
- package/ui-server.mjs +1201 -107
- package/voice-action-dispatcher.mjs +81 -0
- package/voice-agents-sdk.mjs +2 -2
- package/voice-relay.mjs +131 -14
- package/voice-tools.mjs +475 -9
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
package/workflow-nodes.mjs
CHANGED
|
@@ -798,7 +798,9 @@ registerNodeType("trigger.manual", {
|
|
|
798
798
|
});
|
|
799
799
|
|
|
800
800
|
registerNodeType("trigger.task_low", {
|
|
801
|
-
describe: () =>
|
|
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
|
|
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: () =>
|
|
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
|
|
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: "
|
|
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
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
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: () =>
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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")
|