bosun 0.42.2 → 0.42.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 (49) hide show
  1. package/.env.example +9 -0
  2. package/agent/agent-event-bus.mjs +10 -0
  3. package/agent/agent-supervisor.mjs +20 -0
  4. package/bosun-tui.mjs +107 -105
  5. package/cli.mjs +10 -0
  6. package/config/config.mjs +25 -0
  7. package/config/executor-config.mjs +124 -1
  8. package/infra/container-runner.mjs +565 -1
  9. package/infra/monitor.mjs +18 -0
  10. package/infra/tracing.mjs +544 -240
  11. package/infra/tui-bridge.mjs +13 -1
  12. package/kanban/kanban-adapter.mjs +128 -4
  13. package/lib/repo-map.mjs +114 -3
  14. package/package.json +11 -4
  15. package/server/ui-server.mjs +3 -0
  16. package/task/task-archiver.mjs +18 -6
  17. package/task/task-attachments.mjs +14 -10
  18. package/task/task-cli.mjs +24 -4
  19. package/task/task-executor.mjs +19 -0
  20. package/task/task-store.mjs +194 -37
  21. package/telegram/telegram-bot.mjs +4 -1
  22. package/tui/app.mjs +131 -171
  23. package/tui/components/status-header.mjs +178 -75
  24. package/tui/lib/header-config.mjs +68 -0
  25. package/tui/lib/ws-bridge.mjs +61 -9
  26. package/tui/screens/agents.mjs +127 -0
  27. package/tui/screens/tasks.mjs +1 -48
  28. package/ui/app.js +8 -5
  29. package/ui/components/kanban-board.js +65 -3
  30. package/ui/components/session-list.js +18 -32
  31. package/ui/demo-defaults.js +52 -2
  32. package/ui/modules/session-api.js +100 -0
  33. package/ui/modules/state.js +71 -15
  34. package/ui/tabs/workflows.js +25 -1
  35. package/ui/tui/App.js +298 -0
  36. package/ui/tui/TasksScreen.js +564 -0
  37. package/ui/tui/constants.js +55 -0
  38. package/ui/tui/tasks-screen-helpers.js +301 -0
  39. package/ui/tui/useTasks.js +61 -0
  40. package/ui/tui/useWebSocket.js +166 -0
  41. package/ui/tui/useWorkflows.js +30 -0
  42. package/workflow/workflow-engine.mjs +412 -7
  43. package/workflow/workflow-nodes.mjs +616 -75
  44. package/workflow-templates/agents.mjs +3 -0
  45. package/workflow-templates/planning.mjs +7 -0
  46. package/workflow-templates/sub-workflows.mjs +5 -0
  47. package/workflow-templates/task-execution.mjs +3 -0
  48. package/workspace/command-diagnostics.mjs +1 -1
  49. package/workspace/context-cache.mjs +182 -9
@@ -328,6 +328,140 @@ export function normalizeWorkspaceStorageKey(rawKey) {
328
328
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
329
329
  }
330
330
 
331
+ function isPlainObject(value) {
332
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
333
+ }
334
+
335
+ function isLikelyUrl(value) {
336
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(String(value || "").trim());
337
+ }
338
+
339
+ const PORTABLE_TASK_PATH_KEYS = Object.freeze([
340
+ "archivePath",
341
+ "archiveDir",
342
+ "artifactPath",
343
+ "artifactsDir",
344
+ "attachmentPath",
345
+ "attachmentsDir",
346
+ "exportPath",
347
+ "exportDir",
348
+ "importPath",
349
+ "importDir",
350
+ ]);
351
+
352
+ export function normalizePortableTaskPath(rawPath) {
353
+ if (rawPath == null) return "";
354
+ const value = String(rawPath).trim();
355
+ if (!value || isLikelyUrl(value)) return value;
356
+ return normalizeWorkspaceStorageKey(value).toLowerCase();
357
+ }
358
+
359
+ function normalizePortablePathValue(rawPath) {
360
+ return normalizePortableTaskPath(rawPath);
361
+ }
362
+
363
+ function normalizePortablePathList(rawPaths) {
364
+ const values = Array.isArray(rawPaths) ? rawPaths : [rawPaths];
365
+ const normalized = [];
366
+ const seen = new Set();
367
+ for (const entry of values) {
368
+ const canonical = normalizePortablePathValue(entry);
369
+ if (!canonical || seen.has(canonical)) continue;
370
+ seen.add(canonical);
371
+ normalized.push(canonical);
372
+ }
373
+ return normalized;
374
+ }
375
+
376
+ export function getTaskAttachmentCanonicalKey(attachment = {}) {
377
+ if (!attachment || typeof attachment !== "object") return "";
378
+ const url = String(attachment.url || attachment.uri || "").trim();
379
+ if (url) return `url:${url}`;
380
+ const location = normalizePortablePathValue(
381
+ attachment.filePath || attachment.path || attachment.localPath || "",
382
+ );
383
+ if (location) return `path:${location}`;
384
+ if (attachment.id) return `id:${attachment.id}`;
385
+ return `raw:${JSON.stringify(attachment)}`;
386
+ }
387
+
388
+ export function normalizeTaskAttachmentRecord(rawAttachment) {
389
+ if (!rawAttachment || typeof rawAttachment !== "object") return null;
390
+ const normalized = { ...rawAttachment };
391
+ const canonicalLocation = normalizePortablePathValue(
392
+ normalized.filePath || normalized.path || normalized.localPath || "",
393
+ );
394
+ if (canonicalLocation) {
395
+ normalized.filePath = canonicalLocation;
396
+ if (Object.prototype.hasOwnProperty.call(normalized, "path")) {
397
+ normalized.path = canonicalLocation;
398
+ }
399
+ if (Object.prototype.hasOwnProperty.call(normalized, "localPath")) {
400
+ normalized.localPath = canonicalLocation;
401
+ }
402
+ }
403
+ return normalized;
404
+ }
405
+
406
+ export function normalizeTaskAttachments(rawAttachments, options = {}) {
407
+ const values = Array.isArray(rawAttachments) ? rawAttachments : [];
408
+ const normalized = [];
409
+ const seen = new Set();
410
+ for (const value of values) {
411
+ const attachment = normalizeTaskAttachmentRecord(value);
412
+ if (!attachment) continue;
413
+ const key = getTaskAttachmentCanonicalKey(attachment);
414
+ if (!key || seen.has(key)) continue;
415
+ seen.add(key);
416
+ normalized.push(attachment);
417
+ }
418
+ if (options.limit && normalized.length > options.limit) {
419
+ return normalized.slice(0, options.limit);
420
+ }
421
+ return normalized;
422
+ }
423
+
424
+ function normalizeTaskPathPayload(rawPayload = {}, taskId = "", options = {}) {
425
+ if (!isPlainObject(rawPayload)) return {};
426
+ const normalized = { ...rawPayload };
427
+ for (const key of PORTABLE_TASK_PATH_KEYS) {
428
+ if (typeof normalized[key] === "string") {
429
+ normalized[key] = normalizePortablePathValue(normalized[key]);
430
+ }
431
+ }
432
+ if (options.includeWorkspace === true && typeof normalized.workspace === "string") {
433
+ normalized.workspace = normalizeWorkspaceStorageKey(normalized.workspace) || null;
434
+ }
435
+ if (options.includeRepository === true && typeof normalized.repository === "string") {
436
+ normalized.repository = normalizeWorkspaceStorageKey(normalized.repository) || null;
437
+ }
438
+ if (options.includeRepositories === true && normalized.repositories != null) {
439
+ normalized.repositories = normalizeWorkspaceStorageKeys(normalized.repositories, {
440
+ kind: `task:${taskId || "<unknown-task>"}:repositories`,
441
+ });
442
+ }
443
+ if (normalized.attachments != null) {
444
+ normalized.attachments = normalizeTaskAttachments(normalized.attachments, {
445
+ kind: `task:${taskId || "<unknown-task>"}:${options.attachmentKind || "attachments"}`,
446
+ });
447
+ }
448
+ if (normalized.filePaths != null) {
449
+ normalized.filePaths = normalizePortablePathList(normalized.filePaths);
450
+ }
451
+ if (normalized.paths != null) {
452
+ normalized.paths = normalizePortablePathList(normalized.paths);
453
+ }
454
+ return normalized;
455
+ }
456
+
457
+ function normalizeTaskMeta(rawMeta = {}, taskId = "") {
458
+ return normalizeTaskPathPayload(rawMeta, taskId, {
459
+ includeWorkspace: true,
460
+ includeRepository: true,
461
+ attachmentKind: "meta.attachments",
462
+ });
463
+ }
464
+
331
465
  export function normalizeWorkspaceStorageKeys(rawKeys, options = {}) {
332
466
  const kind = String(options.kind || "workspace-rooted storage").trim();
333
467
  const values = Array.isArray(rawKeys) ? rawKeys : [rawKeys];
@@ -588,68 +722,79 @@ function validateTaskTransition(currentStatus, nextStatus, options = {}) {
588
722
  function normalizeTaskStructure(rawTask = {}) {
589
723
  const base = defaultTask(rawTask);
590
724
  const taskId = String(base?.id || rawTask?.id || "").trim() || "<unknown-task>";
591
- const workspaceKey = normalizeWorkspaceStorageKey(base.workspace);
592
- const repositoryKey = normalizeWorkspaceStorageKey(base.repository);
593
- const repositoryKeys = normalizeWorkspaceStorageKeys(base.repositories || [], {
725
+ const normalizedBase = normalizeTaskPathPayload(base, taskId, {
726
+ attachmentKind: "attachments",
727
+ });
728
+ const workspaceKey = normalizeWorkspaceStorageKey(normalizedBase.workspace);
729
+ const repositoryKey = normalizeWorkspaceStorageKey(normalizedBase.repository);
730
+ const repositoryKeys = normalizeWorkspaceStorageKeys(normalizedBase.repositories || [], {
594
731
  kind: `task:${taskId}:repositories`,
595
732
  });
596
733
  const scopedRepositoryKeys = normalizeWorkspaceStorageKeys(
597
734
  [repositoryKey, ...repositoryKeys],
598
735
  { kind: `task:${taskId}:workspace-rooted` },
599
736
  );
737
+ const attachments = normalizeTaskAttachments(normalizedBase.attachments, {
738
+ kind: `task:${taskId}:attachments`,
739
+ });
740
+ const normalizedMeta = normalizeTaskMeta(normalizedBase.meta, taskId);
600
741
  const normalized = {
601
- ...base,
602
- status: normalizeTaskStatus(base.status),
603
- type: normalizeTaskType(base.type),
604
- epicId: base.epicId ? String(base.epicId) : null,
605
- parentTaskId: base.parentTaskId ? String(base.parentTaskId) : null,
606
- childTaskIds: uniqueStringList(base.childTaskIds || []),
607
- dependencyTaskIds: uniqueStringList(base.dependencyTaskIds || []),
608
- blockedByTaskIds: uniqueStringList(base.blockedByTaskIds || []),
609
- dependsOn: uniqueStringList(base.dependsOn || base.dependencyTaskIds || []),
610
- assignees: uniqueStringList(base.assignees || []),
611
- watchers: uniqueStringList(base.watchers || []),
742
+ ...normalizedBase,
743
+ status: normalizeTaskStatus(normalizedBase.status),
744
+ type: normalizeTaskType(normalizedBase.type),
745
+ epicId: normalizedBase.epicId ? String(normalizedBase.epicId) : null,
746
+ parentTaskId: normalizedBase.parentTaskId ? String(normalizedBase.parentTaskId) : null,
747
+ childTaskIds: uniqueStringList(normalizedBase.childTaskIds || []),
748
+ dependencyTaskIds: uniqueStringList(normalizedBase.dependencyTaskIds || []),
749
+ blockedByTaskIds: uniqueStringList(normalizedBase.blockedByTaskIds || []),
750
+ dependsOn: uniqueStringList(
751
+ normalizedBase.dependsOn || normalizedBase.dependencyTaskIds || [],
752
+ ),
753
+ assignees: uniqueStringList(normalizedBase.assignees || []),
754
+ watchers: uniqueStringList(normalizedBase.watchers || []),
612
755
  links: {
613
- branches: uniqueStringList(base.links?.branches || []),
614
- prs: uniqueStringList(base.links?.prs || []),
615
- workflows: uniqueStringList(base.links?.workflows || []),
756
+ branches: uniqueStringList(normalizedBase.links?.branches || []),
757
+ prs: uniqueStringList(normalizedBase.links?.prs || []),
758
+ workflows: uniqueStringList(normalizedBase.links?.workflows || []),
616
759
  },
617
760
  comments: normalizeTaskComments(
618
- Array.isArray(base.comments) && base.comments.length
619
- ? base.comments
620
- : Array.isArray(base.meta?.comments)
621
- ? base.meta.comments
761
+ Array.isArray(normalizedBase.comments) && normalizedBase.comments.length
762
+ ? normalizedBase.comments
763
+ : Array.isArray(normalizedBase.meta?.comments)
764
+ ? normalizedBase.meta.comments
622
765
  : [],
623
766
  ),
624
767
  timeline: normalizeTimelineEvents(
625
- Array.isArray(base.timeline)
626
- ? base.timeline
627
- : Array.isArray(base.meta?.timeline)
628
- ? base.meta.timeline
768
+ Array.isArray(normalizedBase.timeline)
769
+ ? normalizedBase.timeline
770
+ : Array.isArray(normalizedBase.meta?.timeline)
771
+ ? normalizedBase.meta.timeline
629
772
  : [],
630
773
  ),
631
774
  workflowRuns: normalizeWorkflowRunLinks(
632
- Array.isArray(base.workflowRuns)
633
- ? base.workflowRuns
634
- : Array.isArray(base.meta?.workflowRuns)
635
- ? base.meta.workflowRuns
775
+ Array.isArray(normalizedBase.workflowRuns)
776
+ ? normalizedBase.workflowRuns
777
+ : Array.isArray(normalizedBase.meta?.workflowRuns)
778
+ ? normalizedBase.meta.workflowRuns
636
779
  : [],
637
780
  ),
638
781
  runs: normalizeTaskRuns(
639
- Array.isArray(base.runs)
640
- ? base.runs
641
- : Array.isArray(base.meta?.runs)
642
- ? base.meta.runs
782
+ Array.isArray(normalizedBase.runs)
783
+ ? normalizedBase.runs
784
+ : Array.isArray(normalizedBase.meta?.runs)
785
+ ? normalizedBase.meta.runs
643
786
  : [],
644
787
  ),
645
- stateVersion: Number.isFinite(Number(base.stateVersion))
646
- ? Number(base.stateVersion)
788
+ stateVersion: Number.isFinite(Number(normalizedBase.stateVersion))
789
+ ? Number(normalizedBase.stateVersion)
647
790
  : 2,
648
- sprintId: normalizeSprintId(base.sprintId),
649
- sprintOrder: normalizeSprintOrder(base.sprintOrder),
791
+ sprintId: normalizeSprintId(normalizedBase.sprintId),
792
+ sprintOrder: normalizeSprintOrder(normalizedBase.sprintOrder),
650
793
  workspace: workspaceKey || null,
651
794
  repository: repositoryKey || null,
652
795
  repositories: scopedRepositoryKeys,
796
+ attachments,
797
+ meta: normalizedMeta,
653
798
  };
654
799
  if (normalized.status === "draft") {
655
800
  normalized.draft = true;
@@ -657,6 +802,10 @@ function normalizeTaskStructure(rawTask = {}) {
657
802
  return normalized;
658
803
  }
659
804
 
805
+ export function normalizeTaskStorageRecord(rawTask = {}) {
806
+ return normalizeTaskStructure(rawTask);
807
+ }
808
+
660
809
  function pushTaskTimeline(task, event = {}) {
661
810
  task.timeline = normalizeTimelineEvents([...(Array.isArray(task.timeline) ? task.timeline : []), event]);
662
811
  }
@@ -1367,6 +1516,7 @@ export function updateTask(taskId, updates) {
1367
1516
  repositories: (next) => { task.repositories = next; },
1368
1517
  baseBranch: (next) => { task.baseBranch = next; },
1369
1518
  branchName: (next) => { task.branchName = next; },
1519
+ prLinkage: (next) => { task.prLinkage = next; },
1370
1520
  prNumber: (next) => { task.prNumber = next; },
1371
1521
  prUrl: (next) => { task.prUrl = next; },
1372
1522
  epicId: (next) => { task.epicId = next; },
@@ -1404,6 +1554,12 @@ export function updateTask(taskId, updates) {
1404
1554
  task.tags = normalizeTags(value);
1405
1555
  continue;
1406
1556
  }
1557
+ if (key === "attachments") {
1558
+ task.attachments = normalizeTaskAttachments(value, {
1559
+ kind: `task:${taskId}:attachments`,
1560
+ });
1561
+ continue;
1562
+ }
1407
1563
  if (key === "status") {
1408
1564
  task.status = normalizeTaskStatus(value);
1409
1565
  continue;
@@ -3034,3 +3190,4 @@ export function getStaleInReviewTasks(maxAgeMs) {
3034
3190
  (t) => t.status === "inreview" && t.lastActivityAt < cutoff,
3035
3191
  );
3036
3192
  }
3193
+
@@ -1264,6 +1264,7 @@ let _getErrorDetector = null;
1264
1264
  let _getPrCleanupDaemon = null;
1265
1265
  let _getWorkspaceMonitor = null;
1266
1266
  let _getMonitorMonitorStatus = null;
1267
+ let _getTuiMonitorStats = null;
1267
1268
  let _getTaskStoreStats = null;
1268
1269
  let _getTasksPendingReview = null;
1269
1270
  let _triggerTaskPlanner = null;
@@ -1300,6 +1301,7 @@ export function injectMonitorFunctions({
1300
1301
  getPrCleanupDaemon,
1301
1302
  getWorkspaceMonitor,
1302
1303
  getMonitorMonitorStatus,
1304
+ getTuiMonitorStats,
1303
1305
  getTaskStoreStats,
1304
1306
  getTasksPendingReview,
1305
1307
  triggerTaskPlanner,
@@ -1332,6 +1334,7 @@ export function injectMonitorFunctions({
1332
1334
  _getPrCleanupDaemon = getPrCleanupDaemon || null;
1333
1335
  _getWorkspaceMonitor = getWorkspaceMonitor || null;
1334
1336
  _getMonitorMonitorStatus = getMonitorMonitorStatus || null;
1337
+ _getTuiMonitorStats = getTuiMonitorStats || null;
1335
1338
  _getTaskStoreStats = getTaskStoreStats || null;
1336
1339
  _getTasksPendingReview = getTasksPendingReview || null;
1337
1340
  _triggerTaskPlanner = triggerTaskPlanner || null;
@@ -11633,6 +11636,7 @@ export async function startTelegramBot(options = {}) {
11633
11636
  dependencies: {
11634
11637
  execPrimaryPrompt,
11635
11638
  getInternalExecutor: _getInternalExecutor,
11639
+ getTuiMonitorStats: _getTuiMonitorStats,
11636
11640
  getExecutorMode: _getExecutorMode,
11637
11641
  getAgentEventBus: _getAgentEventBus,
11638
11642
  handleUiCommand: handleUiCommand,
@@ -12080,4 +12084,3 @@ export function stopStatusFileWriter() {
12080
12084
  _statusWriterTimer = null;
12081
12085
  }
12082
12086
  }
12083
-