bosun 0.41.8 → 0.41.10

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 (58) hide show
  1. package/.env.example +1 -1
  2. package/README.md +23 -1
  3. package/agent/agent-event-bus.mjs +31 -2
  4. package/agent/agent-pool.mjs +275 -40
  5. package/agent/agent-prompts.mjs +9 -1
  6. package/agent/agent-supervisor.mjs +22 -0
  7. package/agent/autofix.mjs +1 -1
  8. package/agent/primary-agent.mjs +115 -5
  9. package/cli.mjs +3 -2
  10. package/config/config.mjs +47 -33
  11. package/config/context-shredding-config.mjs +1 -1
  12. package/config/repo-root.mjs +41 -33
  13. package/desktop/main.mjs +350 -25
  14. package/desktop/preload.cjs +8 -0
  15. package/desktop/preload.mjs +19 -0
  16. package/entrypoint.mjs +332 -0
  17. package/git/sdk-conflict-resolver.mjs +1 -1
  18. package/infra/health-status.mjs +72 -0
  19. package/infra/library-manager.mjs +58 -1
  20. package/infra/maintenance.mjs +1 -2
  21. package/infra/monitor.mjs +26 -8
  22. package/infra/session-tracker.mjs +30 -3
  23. package/package.json +12 -4
  24. package/server/bosun-mcp-server.mjs +1004 -0
  25. package/server/setup-web-server.mjs +288 -259
  26. package/server/ui-server.mjs +1323 -26
  27. package/shell/claude-shell.mjs +14 -1
  28. package/shell/codex-config.mjs +1 -1
  29. package/shell/codex-model-profiles.mjs +170 -30
  30. package/shell/codex-shell.mjs +63 -18
  31. package/shell/opencode-providers.mjs +20 -8
  32. package/task/task-executor.mjs +28 -0
  33. package/task/task-store.mjs +13 -4
  34. package/telegram/telegram-sentinel.mjs +54 -3
  35. package/tools/list-todos.mjs +7 -1
  36. package/ui/app.js +3 -2
  37. package/ui/components/agent-selector.js +127 -0
  38. package/ui/components/session-list.js +15 -10
  39. package/ui/demo-defaults.js +334 -336
  40. package/ui/modules/router.js +2 -0
  41. package/ui/modules/state.js +13 -5
  42. package/ui/tabs/chat.js +3 -0
  43. package/ui/tabs/library.js +284 -52
  44. package/ui/tabs/tasks.js +5 -13
  45. package/ui/tabs/workflows.js +766 -3
  46. package/workflow/workflow-engine.mjs +246 -5
  47. package/workflow/workflow-nodes/definitions.mjs +37 -0
  48. package/workflow/workflow-nodes.mjs +1014 -184
  49. package/workflow/workflow-templates.mjs +0 -5
  50. package/workflow-templates/_helpers.mjs +253 -0
  51. package/workflow-templates/agents.mjs +199 -226
  52. package/workflow-templates/github.mjs +106 -16
  53. package/workflow-templates/sub-workflows.mjs +233 -0
  54. package/workflow-templates/task-execution.mjs +125 -471
  55. package/workflow-templates/task-lifecycle.mjs +11 -48
  56. package/workspace/command-diagnostics.mjs +460 -0
  57. package/workspace/context-cache.mjs +396 -28
  58. package/workspace/worktree-manager.mjs +1 -1
@@ -1422,8 +1422,3 @@ export function installRecommendedTemplates(engine, overridesById = {}) {
1422
1422
  .map((template) => template.id);
1423
1423
  return installTemplateSet(engine, recommendedIds, overridesById);
1424
1424
  }
1425
-
1426
-
1427
-
1428
-
1429
-
@@ -3,6 +3,18 @@
3
3
  *
4
4
  * These utilities generate positioned nodes and edges in a consistent format.
5
5
  * All template module files import from here.
6
+ *
7
+ * ## Sub-Workflow Composition
8
+ *
9
+ * Templates can be composed from reusable sub-workflows using:
10
+ *
11
+ * subWorkflow(id, nodes, edges) — define a reusable node+edge fragment
12
+ * embedSubWorkflow(sub, prefix, overrides) — embed a fragment into a parent
13
+ * agentPhase(id, label, prompt, extra) — shorthand for action.run_agent node
14
+ * makeAgentPipeline(opts) — factory for trigger → phase₁ → … → done
15
+ *
16
+ * Sub-workflows use prefixed node IDs to avoid collisions when embedded
17
+ * multiple times in the same parent workflow.
6
18
  */
7
19
 
8
20
  // ── Layout state ────────────────────────────────────────────────────────────
@@ -217,3 +229,244 @@ export function normalizeTemplateLayoutInPlace(template, opts = {}) {
217
229
 
218
230
  return template;
219
231
  }
232
+
233
+ // ═══════════════════════════════════════════════════════════════════════════
234
+ // Sub-Workflow Composition Primitives
235
+ // ═══════════════════════════════════════════════════════════════════════════
236
+
237
+ /**
238
+ * Standard boilerplate config for action.run_agent nodes.
239
+ * Templates merge this with a `prompt` to avoid repeating 10 identical fields.
240
+ * @param {object} [extra] - Optional overrides (e.g. resolveMode, failOnError)
241
+ * @returns {object} Config object with template variables
242
+ */
243
+ export function agentDefaults(extra = {}) {
244
+ return {
245
+ taskId: "{{taskId}}",
246
+ sdk: "{{resolvedSdk}}",
247
+ model: "{{resolvedModel}}",
248
+ agentProfile: "{{agentProfile}}",
249
+ cwd: "{{worktreePath}}",
250
+ timeoutMs: "{{taskTimeoutMs}}",
251
+ maxRetries: "{{maxRetries}}",
252
+ maxContinues: "{{maxContinues}}",
253
+ resolveMode: "library",
254
+ failOnError: false,
255
+ ...extra,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Create an action.run_agent node with standard boilerplate merged in.
261
+ * Only the prompt (and optional extra config) needs to be specified.
262
+ * @param {string} id - Node ID
263
+ * @param {string} label - Display label
264
+ * @param {string} prompt - The agent prompt for this phase
265
+ * @param {object} [extra] - Additional config overrides
266
+ * @param {object} [opts] - Position / output overrides
267
+ * @returns {object} Node definition
268
+ */
269
+ export function agentPhase(id, label, prompt, extra = {}, opts = {}) {
270
+ return node(id, "action.run_agent", label, {
271
+ prompt,
272
+ ...agentDefaults(extra),
273
+ }, opts);
274
+ }
275
+
276
+ /**
277
+ * Define a reusable sub-workflow fragment (a group of nodes + edges).
278
+ *
279
+ * A sub-workflow is not a standalone workflow — it's a composable fragment
280
+ * that can be embedded into parent workflows via embedSubWorkflow().
281
+ *
282
+ * @param {string} id - Unique identifier for this sub-workflow definition
283
+ * @param {Array} nodes - Array of node definitions (from node() / agentPhase())
284
+ * @param {Array} edges - Array of edge definitions (from edge())
285
+ * @param {object} [meta] - Optional metadata (description, entryNode, exitNode)
286
+ * @returns {object} Sub-workflow definition { id, nodes, edges, meta }
287
+ */
288
+ export function subWorkflow(id, nodes, edges, meta = {}) {
289
+ if (!id || typeof id !== "string") {
290
+ throw new Error("subWorkflow: id must be a non-empty string");
291
+ }
292
+ if (!Array.isArray(nodes) || nodes.length === 0) {
293
+ throw new Error(`subWorkflow(${id}): must have at least one node`);
294
+ }
295
+ if (!Array.isArray(edges)) {
296
+ throw new Error(`subWorkflow(${id}): edges must be an array`);
297
+ }
298
+ // Validate that edge sources/targets reference defined nodes
299
+ const nodeIds = new Set(nodes.map((n) => n.id));
300
+ for (const e of edges) {
301
+ if (!nodeIds.has(e.source)) {
302
+ throw new Error(`subWorkflow(${id}): edge source "${e.source}" not found in nodes`);
303
+ }
304
+ if (!nodeIds.has(e.target)) {
305
+ throw new Error(`subWorkflow(${id}): edge target "${e.target}" not found in nodes`);
306
+ }
307
+ }
308
+ return {
309
+ id,
310
+ nodes,
311
+ edges,
312
+ meta: {
313
+ entryNode: meta.entryNode || nodes[0].id,
314
+ exitNode: meta.exitNode || nodes[nodes.length - 1].id,
315
+ description: meta.description || "",
316
+ ...meta,
317
+ },
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Embed a sub-workflow into a parent workflow by prefixing all node/edge IDs.
323
+ *
324
+ * This is the core composition mechanism — like calling a function.
325
+ * Each embed gets a unique prefix so the same sub-workflow can appear
326
+ * multiple times in one parent without ID collisions.
327
+ *
328
+ * Returns { nodes, edges, entryNodeId, exitNodeId } ready to spread
329
+ * into a parent template's nodes/edges arrays and wire with edges.
330
+ *
331
+ * @param {object} sub - Sub-workflow from subWorkflow()
332
+ * @param {string} prefix - Unique prefix for this embed (e.g. "backend-")
333
+ * @param {object} [opts] - Embedding options
334
+ * @param {object} [opts.configOverrides] - Config values to merge into every node
335
+ * @param {object} [opts.nodeOverrides] - Per-node config patches keyed by ORIGINAL (unprefixed) node ID
336
+ * @returns {{ nodes: Array, edges: Array, entryNodeId: string, exitNodeId: string }}
337
+ */
338
+ export function embedSubWorkflow(sub, prefix, opts = {}) {
339
+ if (!sub || !Array.isArray(sub.nodes)) {
340
+ throw new Error("embedSubWorkflow: first argument must be a sub-workflow");
341
+ }
342
+ if (!prefix || typeof prefix !== "string") {
343
+ throw new Error("embedSubWorkflow: prefix must be a non-empty string");
344
+ }
345
+
346
+ // Support legacy positional: embedSubWorkflow(sub, prefix, configOverrides)
347
+ const options = (opts && typeof opts === "object" && !Array.isArray(opts))
348
+ ? (opts.configOverrides || opts.nodeOverrides ? opts : { configOverrides: opts })
349
+ : {};
350
+ const configOverrides = options.configOverrides || {};
351
+ const nodeOverrides = options.nodeOverrides || {};
352
+
353
+ const prefixedId = (id) => `${prefix}${id}`;
354
+
355
+ const nodes = sub.nodes.map((n) => ({
356
+ ...n,
357
+ id: prefixedId(n.id),
358
+ config: { ...n.config, ...configOverrides, ...(nodeOverrides[n.id] || {}) },
359
+ }));
360
+
361
+ const edges = sub.edges.map((e) => ({
362
+ ...e,
363
+ id: `${prefixedId(e.source)}->${prefixedId(e.target)}`,
364
+ source: prefixedId(e.source),
365
+ target: prefixedId(e.target),
366
+ }));
367
+
368
+ return {
369
+ nodes,
370
+ edges,
371
+ entryNodeId: prefixedId(sub.meta.entryNode),
372
+ exitNodeId: prefixedId(sub.meta.exitNode),
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Connect two sub-workflow embeds (or any two nodes) with an edge.
378
+ * Convenience for wiring exitNodeId → entryNodeId between composed fragments.
379
+ *
380
+ * @param {string} fromNodeId - Source node ID (typically exitNodeId of previous embed)
381
+ * @param {string} toNodeId - Target node ID (typically entryNodeId of next embed)
382
+ * @param {object} [opts] - Edge options (condition, port, backEdge, etc.)
383
+ * @returns {object} Edge definition
384
+ */
385
+ export function wire(fromNodeId, toNodeId, opts = {}) {
386
+ return edge(fromNodeId, toNodeId, opts);
387
+ }
388
+
389
+ /**
390
+ * Factory: build a complete agent pipeline template from phase definitions.
391
+ *
392
+ * Replaces the repetitive trigger → plan → implement → verify → done pattern
393
+ * found across all task-execution templates. Only the phases differ.
394
+ *
395
+ * @param {object} opts
396
+ * @param {string} opts.id - Template ID
397
+ * @param {string} opts.name - Template display name
398
+ * @param {string} opts.description - Template description
399
+ * @param {string} opts.taskPattern - Regex for trigger.task_assigned matching
400
+ * @param {Array<{id:string, label:string, prompt:string}>} opts.phases - Agent phases
401
+ * @param {string[]} [opts.tags] - Metadata tags
402
+ * @param {object} [opts.variables] - Variable overrides (merged with defaults)
403
+ * @param {object} [opts.metadata] - Metadata overrides
404
+ * @returns {object} Complete workflow template definition
405
+ */
406
+ export function makeAgentPipeline(opts) {
407
+ if (!opts?.id) throw new Error("makeAgentPipeline: id is required");
408
+ if (!opts?.taskPattern) throw new Error("makeAgentPipeline: taskPattern is required");
409
+ if (!Array.isArray(opts?.phases) || opts.phases.length === 0) {
410
+ throw new Error("makeAgentPipeline: at least one phase is required");
411
+ }
412
+
413
+ resetLayout();
414
+
415
+ const defaultVariables = {
416
+ taskTimeoutMs: 21600000,
417
+ maxRetries: 2,
418
+ maxContinues: 3,
419
+ testCommand: "auto",
420
+ buildCommand: "auto",
421
+ lintCommand: "auto",
422
+ };
423
+
424
+ const triggerNode = node("trigger", "trigger.task_assigned", "Task Assigned", {
425
+ taskPattern: opts.taskPattern,
426
+ }, { x: 400, y: 50 });
427
+
428
+ const yStart = 180;
429
+ const yStep = 160;
430
+ const phaseNodes = opts.phases.map((phase, i) =>
431
+ agentPhase(phase.id, phase.label, phase.prompt, phase.extra || {}, {
432
+ x: 400,
433
+ y: yStart + i * yStep,
434
+ }),
435
+ );
436
+
437
+ const doneNode = node("done", "notify.log", "Complete", {
438
+ message: opts.doneMessage || `${opts.name} completed.`,
439
+ }, { x: 400, y: yStart + opts.phases.length * yStep });
440
+
441
+ const allNodes = [triggerNode, ...phaseNodes, doneNode];
442
+
443
+ // Linear pipeline: trigger → phase0 → phase1 → … → done
444
+ const edges = [];
445
+ edges.push(edge("trigger", opts.phases[0].id));
446
+ for (let i = 0; i < opts.phases.length - 1; i++) {
447
+ edges.push(edge(opts.phases[i].id, opts.phases[i + 1].id));
448
+ }
449
+ edges.push(edge(opts.phases[opts.phases.length - 1].id, "done"));
450
+
451
+ return {
452
+ id: opts.id,
453
+ name: opts.name,
454
+ description: opts.description || "",
455
+ category: opts.category || "task-execution",
456
+ enabled: true,
457
+ recommended: opts.recommended !== false,
458
+ trigger: "trigger.task_assigned",
459
+ variables: { ...defaultVariables, ...opts.variables },
460
+ metadata: {
461
+ author: "bosun",
462
+ version: 1,
463
+ createdAt: "2025-06-01T00:00:00Z",
464
+ templateVersion: "1.0.0",
465
+ tags: opts.tags || [],
466
+ resolveMode: "library",
467
+ ...(opts.metadata || {}),
468
+ },
469
+ nodes: allNodes,
470
+ edges,
471
+ };
472
+ }