bosun 0.41.2 → 0.41.4

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 (73) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-pool.mjs +9 -2
  3. package/agent/agent-prompt-catalog.mjs +971 -0
  4. package/agent/agent-prompts.mjs +2 -970
  5. package/agent/agent-supervisor.mjs +119 -6
  6. package/agent/autofix-git.mjs +33 -0
  7. package/agent/autofix-prompts.mjs +151 -0
  8. package/agent/autofix.mjs +11 -175
  9. package/agent/bosun-skills.mjs +3 -2
  10. package/bosun.config.example.json +17 -0
  11. package/bosun.schema.json +87 -188
  12. package/cli.mjs +34 -1
  13. package/config/config-doctor.mjs +5 -250
  14. package/config/config-file-names.mjs +5 -0
  15. package/config/config.mjs +89 -493
  16. package/config/executor-config.mjs +493 -0
  17. package/config/repo-root.mjs +1 -2
  18. package/config/workspace-health.mjs +242 -0
  19. package/git/git-safety.mjs +15 -0
  20. package/github/github-oauth-portal.mjs +46 -0
  21. package/infra/library-manager-utils.mjs +22 -0
  22. package/infra/library-manager-well-known-sources.mjs +578 -0
  23. package/infra/library-manager.mjs +512 -1030
  24. package/infra/monitor.mjs +35 -9
  25. package/infra/session-tracker.mjs +10 -7
  26. package/kanban/kanban-adapter.mjs +17 -1
  27. package/lib/codebase-audit-manifests.mjs +117 -0
  28. package/lib/codebase-audit.mjs +18 -115
  29. package/package.json +18 -3
  30. package/server/setup-web-server.mjs +58 -5
  31. package/server/ui-server.mjs +1394 -79
  32. package/shell/codex-config-file.mjs +178 -0
  33. package/shell/codex-config.mjs +538 -575
  34. package/task/task-cli.mjs +54 -3
  35. package/task/task-executor.mjs +143 -13
  36. package/task/task-store.mjs +409 -1
  37. package/telegram/telegram-bot.mjs +127 -0
  38. package/tools/apply-pr-suggestions.mjs +401 -0
  39. package/tools/syntax-check.mjs +28 -9
  40. package/ui/app.js +3 -14
  41. package/ui/components/kanban-board.js +227 -4
  42. package/ui/components/session-list.js +85 -5
  43. package/ui/demo-defaults.js +338 -84
  44. package/ui/demo.html +155 -0
  45. package/ui/modules/session-api.js +96 -0
  46. package/ui/modules/settings-schema.js +1 -2
  47. package/ui/modules/state.js +43 -3
  48. package/ui/setup.html +4 -5
  49. package/ui/styles/components.css +58 -4
  50. package/ui/tabs/agents.js +12 -15
  51. package/ui/tabs/control.js +1 -0
  52. package/ui/tabs/library.js +484 -22
  53. package/ui/tabs/manual-flows.js +105 -29
  54. package/ui/tabs/tasks.js +848 -141
  55. package/ui/tabs/telemetry.js +129 -11
  56. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  57. package/ui/tabs/workflows.js +293 -23
  58. package/voice/voice-tool-definitions.mjs +757 -0
  59. package/voice/voice-tools.mjs +34 -778
  60. package/workflow/manual-flow-audit.mjs +165 -0
  61. package/workflow/manual-flows.mjs +164 -259
  62. package/workflow/workflow-engine.mjs +147 -58
  63. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  64. package/workflow/workflow-nodes/transforms.mjs +612 -0
  65. package/workflow/workflow-nodes.mjs +358 -63
  66. package/workflow/workflow-templates.mjs +313 -191
  67. package/workflow-templates/_helpers.mjs +154 -0
  68. package/workflow-templates/agents.mjs +61 -4
  69. package/workflow-templates/code-quality.mjs +7 -7
  70. package/workflow-templates/github.mjs +20 -10
  71. package/workflow-templates/task-batch.mjs +44 -11
  72. package/workflow-templates/task-lifecycle.mjs +31 -6
  73. package/workspace/worktree-manager.mjs +277 -3
@@ -92,6 +92,16 @@ const MAX_PERSISTED_RUNS = readBoundedEnvInt("WORKFLOW_MAX_PERSISTED_RUNS", 2000
92
92
  min: 20,
93
93
  max: 20000,
94
94
  });
95
+ const MAX_INTERRUPTED_ORPHAN_SCAN_FILES = readBoundedEnvInt(
96
+ "WORKFLOW_INTERRUPTED_ORPHAN_SCAN_MAX_FILES",
97
+ 200,
98
+ { min: 0, max: 5000 },
99
+ );
100
+ const INTERRUPTED_ORPHAN_SCAN_WINDOW_MS = readBoundedEnvInt(
101
+ "WORKFLOW_INTERRUPTED_ORPHAN_SCAN_WINDOW_MS",
102
+ 7 * 24 * 60 * 60 * 1000,
103
+ { min: 0, max: 90 * 24 * 60 * 60 * 1000 },
104
+ );
95
105
  const DEFAULT_RUN_STUCK_THRESHOLD_MS = readBoundedEnvInt(
96
106
  "WORKFLOW_RUN_STUCK_THRESHOLD_MS",
97
107
  5 * 60 * 1000,
@@ -312,6 +322,61 @@ export function isPortConnectionCompatible(sourcePort, targetPort) {
312
322
  };
313
323
  }
314
324
 
325
+ function getExplicitNodeOutputs(node) {
326
+ return Array.isArray(node?.outputs)
327
+ ? Array.from(new Set(node.outputs.map((value) => String(value || "").trim()).filter(Boolean)))
328
+ : undefined;
329
+ }
330
+
331
+ function hydrateWorkflowNode(node, nodeMap) {
332
+ const ports = resolveNodePorts(node);
333
+ const explicitOutputs = getExplicitNodeOutputs(node);
334
+ const nextNode = {
335
+ ...node,
336
+ inputPorts: ports.inputs.map((port) => clonePortDescriptor(port)),
337
+ outputPorts: ports.outputs.map((port) => clonePortDescriptor(port)),
338
+ ...(explicitOutputs !== undefined ? { outputs: explicitOutputs } : {}),
339
+ };
340
+ nodeMap.set(nextNode.id, nextNode);
341
+ return nextNode;
342
+ }
343
+
344
+ function buildPortValidationIssue(edge, sourcePort, targetPort, compatibility) {
345
+ return {
346
+ edgeId: edge.id || `${edge.source}->${edge.target}`,
347
+ source: edge.source,
348
+ target: edge.target,
349
+ sourcePort: sourcePort?.name || "default",
350
+ targetPort: targetPort?.name || "default",
351
+ sourceType: sourcePort?.type || null,
352
+ targetType: targetPort?.type || null,
353
+ severity: "error",
354
+ message: compatibility.reason,
355
+ };
356
+ }
357
+
358
+ function hydrateWorkflowEdge(edge, nodeMap, issues) {
359
+ const sourceNode = nodeMap.get(edge.source);
360
+ const targetNode = nodeMap.get(edge.target);
361
+ const sourcePorts = resolveNodePorts(sourceNode);
362
+ const targetPorts = resolveNodePorts(targetNode);
363
+ const sourcePort = resolvePortByName(sourcePorts.outputs, edge.sourcePort || "default", "output");
364
+ const targetPort = resolvePortByName(targetPorts.inputs, edge.targetPort || "default", "input");
365
+ const compatibility = isPortConnectionCompatible(sourcePort, targetPort);
366
+
367
+ if (!compatibility.compatible) {
368
+ issues.push(buildPortValidationIssue(edge, sourcePort, targetPort, compatibility));
369
+ }
370
+
371
+ return {
372
+ ...edge,
373
+ sourcePort: sourcePort?.name || String(edge.sourcePort || "default").trim() || "default",
374
+ targetPort: targetPort?.name || String(edge.targetPort || "default").trim() || "default",
375
+ sourcePortType: sourcePort?.type || null,
376
+ targetPortType: targetPort?.type || null,
377
+ };
378
+ }
379
+
315
380
  function hydrateWorkflowDefinition(def, { strict = false } = {}) {
316
381
  const normalized = {
317
382
  ...(def || {}),
@@ -321,55 +386,10 @@ function hydrateWorkflowDefinition(def, { strict = false } = {}) {
321
386
  };
322
387
 
323
388
  const nodeMap = new Map();
324
- normalized.nodes = normalized.nodes.map((node) => {
325
- const ports = resolveNodePorts(node);
326
- const explicitOutputs = Array.isArray(node?.outputs)
327
- ? Array.from(new Set(node.outputs.map((value) => String(value || "").trim()).filter(Boolean)))
328
- : undefined;
329
- const nextNode = {
330
- ...node,
331
- inputPorts: ports.inputs.map((port) => clonePortDescriptor(port)),
332
- outputPorts: ports.outputs.map((port) => clonePortDescriptor(port)),
333
- ...(explicitOutputs !== undefined ? { outputs: explicitOutputs } : {}),
334
- };
335
- nodeMap.set(nextNode.id, nextNode);
336
- return nextNode;
337
- });
389
+ normalized.nodes = normalized.nodes.map((node) => hydrateWorkflowNode(node, nodeMap));
338
390
 
339
391
  const issues = [];
340
-
341
- normalized.edges = normalized.edges.map((edge) => {
342
- const sourceNode = nodeMap.get(edge.source);
343
- const targetNode = nodeMap.get(edge.target);
344
- const sourcePorts = resolveNodePorts(sourceNode);
345
- const targetPorts = resolveNodePorts(targetNode);
346
- const sourcePort = resolvePortByName(sourcePorts.outputs, edge.sourcePort || "default", "output");
347
- const targetPort = resolvePortByName(targetPorts.inputs, edge.targetPort || "default", "input");
348
- const compatibility = isPortConnectionCompatible(sourcePort, targetPort);
349
-
350
- if (!compatibility.compatible) {
351
- issues.push({
352
- edgeId: edge.id || `${edge.source}->${edge.target}`,
353
- source: edge.source,
354
- target: edge.target,
355
- sourcePort: sourcePort?.name || "default",
356
- targetPort: targetPort?.name || "default",
357
- sourceType: sourcePort?.type || null,
358
- targetType: targetPort?.type || null,
359
- severity: "error",
360
- message: compatibility.reason,
361
- });
362
- }
363
-
364
- return {
365
- ...edge,
366
- sourcePort: sourcePort?.name || String(edge.sourcePort || "default").trim() || "default",
367
- targetPort: targetPort?.name || String(edge.targetPort || "default").trim() || "default",
368
- sourcePortType: sourcePort?.type || null,
369
- targetPortType: targetPort?.type || null,
370
- };
371
- });
372
-
392
+ normalized.edges = normalized.edges.map((edge) => hydrateWorkflowEdge(edge, nodeMap, issues));
373
393
  normalized.metadata.validationIssues = issues;
374
394
 
375
395
  if (strict && issues.length > 0) {
@@ -378,7 +398,6 @@ function hydrateWorkflowDefinition(def, { strict = false } = {}) {
378
398
 
379
399
  return normalized;
380
400
  }
381
-
382
401
  /**
383
402
  * Register a node type handler.
384
403
  * @param {string} type - Node type identifier (e.g., "trigger.task_low", "action.run_agent")
@@ -982,7 +1001,16 @@ export class WorkflowEngine extends EventEmitter {
982
1001
  /** Get a single workflow definition */
983
1002
  get(id) {
984
1003
  if (!this._loaded) this.load();
985
- return this._workflows.get(id) || null;
1004
+ const workflowId = String(id || "").trim();
1005
+ if (!workflowId) return null;
1006
+ const exact = this._workflows.get(workflowId);
1007
+ if (exact) return exact;
1008
+ for (const workflow of this._workflows.values()) {
1009
+ if (workflow?.metadata?.installedFrom === workflowId) {
1010
+ return workflow;
1011
+ }
1012
+ }
1013
+ return null;
986
1014
  }
987
1015
 
988
1016
  /** Save (create or update) a workflow definition */
@@ -2421,6 +2449,24 @@ export class WorkflowEngine extends EventEmitter {
2421
2449
  }
2422
2450
  }
2423
2451
 
2452
+ const countForwardIncomingEdges = (targetNodeId) =>
2453
+ (def.edges || []).filter((edge) => edge.target === targetNodeId && !edge.backEdge).length;
2454
+
2455
+ const propagateSkippedDependencies = (skippedNodeId) => {
2456
+ if (countForwardIncomingEdges(skippedNodeId) > 1) return;
2457
+ const skippedEdges = adjacency.get(skippedNodeId) || [];
2458
+ for (const skippedEdge of skippedEdges) {
2459
+ if (skippedEdge.backEdge) continue;
2460
+ consumeEdgeDependency(skippedEdge.target, false, {
2461
+ reason: "upstream-skipped",
2462
+ payload: {
2463
+ sourceNodeId: skippedNodeId,
2464
+ edgeId: skippedEdge.id || `${skippedEdge.source}->${skippedEdge.target}`,
2465
+ },
2466
+ });
2467
+ }
2468
+ };
2469
+
2424
2470
  const consumeEdgeDependency = (targetNodeId, matched, skipInfo = null) => {
2425
2471
  const nextDegree = (inDegree.get(targetNodeId) || 1) - 1;
2426
2472
  inDegree.set(targetNodeId, nextDegree);
@@ -2438,6 +2484,7 @@ export class WorkflowEngine extends EventEmitter {
2438
2484
  executed.add(targetNodeId);
2439
2485
  const skippedNode = nodeMap.get(targetNodeId);
2440
2486
  console.log(`${TAG} node:SKIPPED ${targetNodeId} (${skippedNode?.type || "?"}) [${skippedNode?.label || ""}] — no satisfied edges`);
2487
+ propagateSkippedDependencies(targetNodeId);
2441
2488
  }
2442
2489
  }
2443
2490
  };
@@ -2710,6 +2757,51 @@ export class WorkflowEngine extends EventEmitter {
2710
2757
  }
2711
2758
  }
2712
2759
 
2760
+ _getInterruptedOrphanRunCandidates() {
2761
+ if (!existsSync(this.runsDir)) return [];
2762
+ if (MAX_INTERRUPTED_ORPHAN_SCAN_FILES <= 0) return [];
2763
+
2764
+ const cutoffMs = INTERRUPTED_ORPHAN_SCAN_WINDOW_MS > 0
2765
+ ? Date.now() - INTERRUPTED_ORPHAN_SCAN_WINDOW_MS
2766
+ : 0;
2767
+ const candidates = [];
2768
+ let totalCandidates = 0;
2769
+
2770
+ try {
2771
+ for (const file of readdirSync(this.runsDir)) {
2772
+ if (
2773
+ extname(file) !== ".json" ||
2774
+ file === "index.json" ||
2775
+ file === ACTIVE_RUNS_INDEX
2776
+ ) {
2777
+ continue;
2778
+ }
2779
+ const detailPath = resolve(this.runsDir, file);
2780
+ let mtimeMs = 0;
2781
+ try {
2782
+ mtimeMs = statSync(detailPath).mtimeMs || 0;
2783
+ } catch {
2784
+ continue;
2785
+ }
2786
+ if (cutoffMs > 0 && mtimeMs < cutoffMs) continue;
2787
+ totalCandidates += 1;
2788
+ candidates.push({ file, detailPath, mtimeMs });
2789
+ }
2790
+ } catch {
2791
+ return [];
2792
+ }
2793
+
2794
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
2795
+ if (candidates.length > MAX_INTERRUPTED_ORPHAN_SCAN_FILES) {
2796
+ console.warn(
2797
+ `${TAG} Orphan interrupted-run scan limited to ${MAX_INTERRUPTED_ORPHAN_SCAN_FILES} recent files ` +
2798
+ `(${totalCandidates} candidate files in retention window)`,
2799
+ );
2800
+ candidates.length = MAX_INTERRUPTED_ORPHAN_SCAN_FILES;
2801
+ }
2802
+ return candidates;
2803
+ }
2804
+
2713
2805
  _getRunStuckThresholdMs() {
2714
2806
  const raw = Number(process.env.WORKFLOW_RUN_STUCK_THRESHOLD_MS);
2715
2807
  if (Number.isFinite(raw) && raw > 0) return raw;
@@ -3074,17 +3166,14 @@ export class WorkflowEngine extends EventEmitter {
3074
3166
  }
3075
3167
 
3076
3168
  // Tertiary source: orphan detail files with no index entry and no end marker.
3077
- const detailFiles = readdirSync(this.runsDir).filter((file) =>
3078
- extname(file) === ".json" &&
3079
- file !== "index.json" &&
3080
- file !== ACTIVE_RUNS_INDEX,
3081
- );
3082
- for (const file of detailFiles) {
3083
- const runId = basename(file, ".json");
3169
+ // This is bounded to a recent subset so old archived run details cannot
3170
+ // stall startup when workflow-runs contains thousands of historical files.
3171
+ const orphanCandidates = this._getInterruptedOrphanRunCandidates();
3172
+ for (const candidate of orphanCandidates) {
3173
+ const runId = basename(candidate.file, ".json");
3084
3174
  if (!runId || this._activeRuns.has(runId) || runsById.has(runId)) continue;
3085
- const detailPath = resolve(this.runsDir, file);
3086
3175
  try {
3087
- const detail = JSON.parse(readFileSync(detailPath, "utf8"));
3176
+ const detail = JSON.parse(readFileSync(candidate.detailPath, "utf8"));
3088
3177
  const hasRunningNode = Object.values(detail?.nodeStatuses || {}).some(
3089
3178
  (status) => status === NodeStatus.RUNNING || status === NodeStatus.WAITING,
3090
3179
  );