bosun 0.36.0 → 0.36.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 (98) hide show
  1. package/.env.example +98 -16
  2. package/README.md +27 -0
  3. package/agent-event-bus.mjs +5 -5
  4. package/agent-pool.mjs +129 -12
  5. package/agent-prompts.mjs +7 -1
  6. package/agent-sdk.mjs +13 -2
  7. package/agent-supervisor.mjs +2 -2
  8. package/agent-work-report.mjs +1 -1
  9. package/anomaly-detector.mjs +6 -6
  10. package/autofix.mjs +15 -15
  11. package/bosun-skills.mjs +4 -4
  12. package/bosun.schema.json +160 -4
  13. package/claude-shell.mjs +11 -11
  14. package/cli.mjs +21 -21
  15. package/codex-config.mjs +19 -19
  16. package/codex-shell.mjs +180 -29
  17. package/config-doctor.mjs +27 -2
  18. package/config.mjs +60 -7
  19. package/copilot-shell.mjs +4 -4
  20. package/error-detector.mjs +1 -1
  21. package/fleet-coordinator.mjs +2 -2
  22. package/gemini-shell.mjs +692 -0
  23. package/github-oauth-portal.mjs +1 -1
  24. package/github-reconciler.mjs +2 -2
  25. package/kanban-adapter.mjs +741 -168
  26. package/merge-strategy.mjs +25 -25
  27. package/monitor.mjs +123 -105
  28. package/opencode-shell.mjs +22 -22
  29. package/package.json +7 -1
  30. package/postinstall.mjs +22 -22
  31. package/pr-cleanup-daemon.mjs +6 -6
  32. package/prepublish-check.mjs +4 -4
  33. package/presence.mjs +2 -2
  34. package/primary-agent.mjs +85 -7
  35. package/publish.mjs +1 -1
  36. package/review-agent.mjs +1 -1
  37. package/session-tracker.mjs +11 -0
  38. package/setup-web-server.mjs +429 -21
  39. package/setup.mjs +367 -12
  40. package/shared-knowledge.mjs +1 -1
  41. package/startup-service.mjs +9 -9
  42. package/stream-resilience.mjs +58 -4
  43. package/sync-engine.mjs +2 -2
  44. package/task-assessment.mjs +9 -9
  45. package/task-cli.mjs +1 -1
  46. package/task-complexity.mjs +71 -2
  47. package/task-context.mjs +1 -2
  48. package/task-executor.mjs +104 -41
  49. package/telegram-bot.mjs +825 -494
  50. package/telegram-sentinel.mjs +28 -28
  51. package/ui/app.js +256 -23
  52. package/ui/app.monolith.js +1 -1
  53. package/ui/components/agent-selector.js +4 -3
  54. package/ui/components/chat-view.js +101 -28
  55. package/ui/components/diff-viewer.js +3 -3
  56. package/ui/components/kanban-board.js +3 -3
  57. package/ui/components/session-list.js +255 -35
  58. package/ui/components/workspace-switcher.js +3 -3
  59. package/ui/demo.html +209 -194
  60. package/ui/index.html +3 -3
  61. package/ui/modules/icon-utils.js +206 -142
  62. package/ui/modules/icons.js +2 -27
  63. package/ui/modules/settings-schema.js +29 -5
  64. package/ui/modules/streaming.js +30 -2
  65. package/ui/modules/vision-stream.js +275 -0
  66. package/ui/modules/voice-client.js +102 -9
  67. package/ui/modules/voice-fallback.js +62 -6
  68. package/ui/modules/voice-overlay.js +594 -59
  69. package/ui/modules/voice.js +31 -38
  70. package/ui/setup.html +284 -34
  71. package/ui/styles/components.css +47 -0
  72. package/ui/styles/sessions.css +75 -0
  73. package/ui/tabs/agents.js +73 -43
  74. package/ui/tabs/chat.js +37 -40
  75. package/ui/tabs/control.js +2 -2
  76. package/ui/tabs/dashboard.js +1 -1
  77. package/ui/tabs/infra.js +10 -10
  78. package/ui/tabs/library.js +8 -8
  79. package/ui/tabs/logs.js +10 -10
  80. package/ui/tabs/settings.js +20 -20
  81. package/ui/tabs/tasks.js +76 -47
  82. package/ui-server.mjs +1761 -124
  83. package/update-check.mjs +13 -13
  84. package/ve-kanban.mjs +1 -1
  85. package/whatsapp-channel.mjs +5 -5
  86. package/workflow-engine.mjs +20 -1
  87. package/workflow-nodes.mjs +904 -4
  88. package/workflow-templates/agents.mjs +321 -7
  89. package/workflow-templates/ci-cd.mjs +6 -6
  90. package/workflow-templates/github.mjs +156 -84
  91. package/workflow-templates/planning.mjs +8 -8
  92. package/workflow-templates/reliability.mjs +8 -8
  93. package/workflow-templates/security.mjs +3 -3
  94. package/workflow-templates.mjs +15 -9
  95. package/workspace-manager.mjs +85 -1
  96. package/workspace-monitor.mjs +2 -2
  97. package/workspace-registry.mjs +2 -2
  98. package/worktree-manager.mjs +1 -1
package/ui/tabs/tasks.js CHANGED
@@ -366,7 +366,7 @@ export function StartTaskModal({
366
366
  onClick=${handleStart}
367
367
  disabled=${starting || !resolvedTaskId}
368
368
  >
369
- ${starting ? "Starting…" : " Start Task"}
369
+ ${starting ? "Starting…" : ":play: Start Task"}
370
370
  </button>
371
371
  </div>
372
372
  </div>
@@ -875,7 +875,7 @@ export function TaskProgressModal({ task, onClose }) {
875
875
  <div class="tp-hero">
876
876
  <div class="tp-pulse-dot"></div>
877
877
  <div class="tp-hero-title">
878
- <div class="tp-hero-status-label">${iconText(" Active — Agent Working")}</div>
878
+ <div class="tp-hero-status-label">${iconText(":zap: Active — Agent Working")}</div>
879
879
  </div>
880
880
  <${Badge} status="inprogress" text="running" />
881
881
  </div>
@@ -924,7 +924,7 @@ export function TaskProgressModal({ task, onClose }) {
924
924
  font-weight:${active ? "600" : "400"};"
925
925
  >
926
926
  <span style="font-size:14px;flex-shrink:0;">
927
- ${done ? resolveIcon("") : active ? resolveIcon("🔄") : ICONS.dot}
927
+ ${done ? resolveIcon(":check:") : active ? resolveIcon(":refresh:") : ICONS.dot}
928
928
  </span>
929
929
  <span>${step.label}</span>
930
930
  ${active && html`
@@ -956,11 +956,11 @@ export function TaskProgressModal({ task, onClose }) {
956
956
  class="btn btn-ghost btn-sm"
957
957
  onClick=${() => { haptic(); sendCommandToChat("/steer " + task.id); onClose(); }}
958
958
  title="Guide the agent mid-task"
959
- >${iconText("💬 Steer")}</button>
959
+ >${iconText(":chat: Steer")}</button>
960
960
  <button
961
961
  class="btn btn-ghost btn-sm"
962
962
  onClick=${() => { haptic(); sendCommandToChat("/logs " + task.id); onClose(); }}
963
- >${iconText("📄 Logs")}</button>
963
+ >${iconText(":file: Logs")}</button>
964
964
  <button class="btn btn-secondary btn-sm" onClick=${handleMarkReview}>
965
965
  → Move to Review
966
966
  </button>
@@ -1085,7 +1085,7 @@ export function TaskReviewModal({ task, onClose, onStart }) {
1085
1085
  >
1086
1086
 
1087
1087
  <div class="tr-hero">
1088
- <span class="tr-review-icon">${resolveIcon("🔍")}</span>
1088
+ <span class="tr-review-icon">${resolveIcon(":search:")}</span>
1089
1089
  <div class="tr-hero-title">
1090
1090
  <div class="tr-hero-status-label">In Review</div>
1091
1091
  ${prNumber && html`
@@ -1094,7 +1094,7 @@ export function TaskReviewModal({ task, onClose, onStart }) {
1094
1094
  href="#"
1095
1095
  onClick=${(e) => { e.preventDefault(); haptic(); sendCommandToChat("/diff " + branchLabel); onClose(); }}
1096
1096
  >
1097
- PR #${prNumber} · View diff
1097
+ PR #${prNumber} · View diff :arrowRight:
1098
1098
  </a>
1099
1099
  `}
1100
1100
  ${!prNumber && html`<span style="font-size:12px;color:var(--text-hint);">No PR yet</span>`}
@@ -1141,12 +1141,12 @@ export function TaskReviewModal({ task, onClose, onStart }) {
1141
1141
 
1142
1142
  <div class="tr-section">
1143
1143
  <div class="tr-section-title">
1144
- Checks ${allPass ? iconText("— All passing") : ""}
1144
+ Checks ${allPass ? iconText("— :check: All passing") : ""}
1145
1145
  </div>
1146
1146
  <div class="tr-checks-row">
1147
1147
  ${checks.map((c) => html`
1148
1148
  <div class="tr-check-item ${c.status}" key=${c.label}>
1149
- ${resolveIcon(c.status === "pass" ? "" : c.status === "fail" ? "" : "")}
1149
+ ${resolveIcon(c.status === "pass" ? ":check:" : c.status === "fail" ? ":close:" : ":clock:")}
1150
1150
  ${c.label}
1151
1151
  </div>
1152
1152
  `)}
@@ -1175,7 +1175,7 @@ export function TaskReviewModal({ task, onClose, onStart }) {
1175
1175
  <div class="task-attachment-item" key=${att.id || `${name}-${index}`}>
1176
1176
  ${isImage && url
1177
1177
  ? html`<img class="task-attachment-thumb" src=${url} alt=${name} />`
1178
- : html`<span class="task-attachment-icon">${resolveIcon("📎")}</span>`}
1178
+ : html`<span class="task-attachment-icon">${resolveIcon(":link:")}</span>`}
1179
1179
  <div class="task-attachment-meta">
1180
1180
  ${url
1181
1181
  ? html`<a class="task-attachment-name" href=${url} target="_blank" rel="noopener">${name}</a>`
@@ -1200,17 +1200,17 @@ export function TaskReviewModal({ task, onClose, onStart }) {
1200
1200
  title="Mark as merged / done"
1201
1201
  >${iconText("✓ Mark Done")}</button>
1202
1202
  <button class="btn btn-secondary btn-sm" onClick=${handleReopen}>
1203
- Reopen as Active
1203
+ :workflow: Reopen as Active
1204
1204
  </button>
1205
1205
  <button
1206
1206
  class="btn btn-ghost btn-sm"
1207
1207
  onClick=${() => { haptic(); sendCommandToChat("/logs " + task.id); onClose(); }}
1208
- >${iconText("📄 Logs")}</button>
1208
+ >${iconText(":file: Logs")}</button>
1209
1209
  ${prNumber && html`
1210
1210
  <button
1211
1211
  class="btn btn-ghost btn-sm"
1212
1212
  onClick=${() => { haptic(); sendCommandToChat("/diff " + branchLabel); onClose(); }}
1213
- >${iconText("🔎 Diff")}</button>
1213
+ >${iconText(":search: Diff")}</button>
1214
1214
  `}
1215
1215
  <button
1216
1216
  class="btn btn-ghost btn-sm"
@@ -1540,7 +1540,7 @@ export function TaskDetailModal({ task, onClose, onStart }) {
1540
1540
  html`
1541
1541
  <div class="task-modal-actions">
1542
1542
  <button class="btn btn-primary btn-sm" onClick=${handleStart}>
1543
- Dispatch Task
1543
+ :play: Dispatch Task
1544
1544
  </button>
1545
1545
  </div>
1546
1546
  `}
@@ -1616,7 +1616,7 @@ export function TaskDetailModal({ task, onClose, onStart }) {
1616
1616
  <div class="task-attachment-item" key=${att.id || `${name}-${index}`}>
1617
1617
  ${isImage && url
1618
1618
  ? html`<img class="task-attachment-thumb" src=${url} alt=${name} />`
1619
- : html`<span class="task-attachment-icon">${resolveIcon("📎")}</span>`}
1619
+ : html`<span class="task-attachment-icon">${resolveIcon(":link:")}</span>`}
1620
1620
  <div class="task-attachment-meta">
1621
1621
  ${url
1622
1622
  ? html`<a class="task-attachment-name" href=${url} target="_blank" rel="noopener">${name}</a>`
@@ -1673,8 +1673,8 @@ export function TaskDetailModal({ task, onClose, onStart }) {
1673
1673
  title="Use AI to expand and improve this task description"
1674
1674
  >
1675
1675
  ${rewriting
1676
- ? html`<span style="display:inline-block;animation:spin 0.8s linear infinite">${resolveIcon("")}</span> Improving…`
1677
- : html`${iconText(" Improve with AI")}`
1676
+ ? html`<span style="display:inline-block;animation:spin 0.8s linear infinite">${resolveIcon(":clock:")}</span> Improving…`
1677
+ : html`${iconText(":star: Improve with AI")}`
1678
1678
  }
1679
1679
  </button>
1680
1680
  <input
@@ -1836,7 +1836,7 @@ export function TaskDetailModal({ task, onClose, onStart }) {
1836
1836
  onClick=${handleSave}
1837
1837
  disabled=${saving}
1838
1838
  >
1839
- ${saving ? "Saving…" : iconText("💾 Save")}
1839
+ ${saving ? "Saving…" : iconText(":save: Save")}
1840
1840
  </button>
1841
1841
  <button
1842
1842
  class="btn btn-ghost btn-sm"
@@ -1871,7 +1871,7 @@ export function TaskDetailModal({ task, onClose, onStart }) {
1871
1871
  sendCommandToChat("/logs " + task.id);
1872
1872
  }}
1873
1873
  >
1874
- ${iconText("📄 View Agent Logs")}
1874
+ ${iconText(":file: View Agent Logs")}
1875
1875
  </button>
1876
1876
  `}
1877
1877
  </div>
@@ -2436,7 +2436,7 @@ export function TasksTab() {
2436
2436
  return html`
2437
2437
  <div class="flex-between mb-sm" style="padding:0 4px">
2438
2438
  <div class="view-toggle">
2439
- <button class="view-toggle-btn ${!isKanban ? 'active' : ''}" onClick=${() => { viewMode.value = 'list'; haptic(); }}>${iconText(" List")}</button>
2439
+ <button class="view-toggle-btn ${!isKanban ? 'active' : ''}" onClick=${() => { viewMode.value = 'list'; haptic(); }}>${iconText(":menu: List")}</button>
2440
2440
  <button class="view-toggle-btn ${isKanban ? 'active' : ''}" onClick=${() => { viewMode.value = 'kanban'; haptic(); }}>▦ Board</button>
2441
2441
  </div>
2442
2442
  <div style="display:flex;gap:8px;align-items:center;">
@@ -2446,7 +2446,7 @@ export function TasksTab() {
2446
2446
  haptic();
2447
2447
  setShowTemplates(true);
2448
2448
  }}
2449
- >${iconText(" Templates")}</button>
2449
+ >${iconText(":zap: Templates")}</button>
2450
2450
  <button
2451
2451
  class="btn btn-ghost btn-sm"
2452
2452
  onClick=${toggleCompletedFilter}
@@ -2510,7 +2510,7 @@ export function TasksTab() {
2510
2510
 
2511
2511
  const viewToggle = html`
2512
2512
  <div class="view-toggle">
2513
- <button class="view-toggle-btn ${!isKanban ? 'active' : ''}" onClick=${() => { viewMode.value = 'list'; haptic(); }}>${iconText(" List")}</button>
2513
+ <button class="view-toggle-btn ${!isKanban ? 'active' : ''}" onClick=${() => { viewMode.value = 'list'; haptic(); }}>${iconText(":menu: List")}</button>
2514
2514
  <button class="view-toggle-btn ${isKanban ? 'active' : ''}" onClick=${() => { viewMode.value = 'kanban'; haptic(); }}>▦ Board</button>
2515
2515
  </div>
2516
2516
  `;
@@ -2547,16 +2547,16 @@ export function TasksTab() {
2547
2547
  class="actions-dropdown-item"
2548
2548
  onClick=${() => { setActionsOpen(false); setStartAnyOpen(true); }}
2549
2549
  >
2550
- ${iconText(" Start Task")}
2550
+ ${iconText(":play: Start Task")}
2551
2551
  </button>
2552
2552
  <button
2553
2553
  class="actions-dropdown-item"
2554
2554
  onClick=${() => { setActionsOpen(false); setShowTemplates(true); }}
2555
2555
  >
2556
- ${iconText(" Trigger Templates")}
2556
+ ${iconText(":zap: Trigger Templates")}
2557
2557
  </button>
2558
- <button class="actions-dropdown-item" onClick=${handleExportCSV}>${iconText("📊 Export CSV")}</button>
2559
- <button class="actions-dropdown-item" onClick=${handleExportJSON}>${iconText("📋 Export JSON")}</button>
2558
+ <button class="actions-dropdown-item" onClick=${handleExportCSV}>${iconText(":chart: Export CSV")}</button>
2559
+ <button class="actions-dropdown-item" onClick=${handleExportJSON}>${iconText(":clipboard: Export JSON")}</button>
2560
2560
  </div>
2561
2561
  `}
2562
2562
  </div>
@@ -2601,7 +2601,7 @@ export function TasksTab() {
2601
2601
  setStartAnyOpen(true);
2602
2602
  }}
2603
2603
  >
2604
- Start Task
2604
+ :play: Start Task
2605
2605
  </button>
2606
2606
  ${actionsMenu}
2607
2607
  `}
@@ -2778,7 +2778,7 @@ export function TasksTab() {
2778
2778
  <span class="snapshot-lbl">${m.label}</span>
2779
2779
  </button>
2780
2780
  `)}
2781
- <span class="snapshot-view-tag">${iconText(isKanban ? " Board" : " List")}</span>
2781
+ <span class="snapshot-view-tag">${iconText(isKanban ? ":dot: Board" : ":menu: List")}</span>
2782
2782
  </div>
2783
2783
 
2784
2784
  <style>
@@ -3006,6 +3006,7 @@ function CreateTaskModalInline({ onClose }) {
3006
3006
  const [rewriting, setRewriting] = useState(false);
3007
3007
  const [workspaceId, setWorkspaceId] = useState(activeWorkspaceId.value || "");
3008
3008
  const [repository, setRepository] = useState("");
3009
+ const [repositories, setRepositories] = useState([]);
3009
3010
  const [showAdvanced, setShowAdvanced] = useState(false);
3010
3011
 
3011
3012
  const handleRewrite = async () => {
@@ -3051,14 +3052,31 @@ function CreateTaskModalInline({ onClose }) {
3051
3052
  useEffect(() => {
3052
3053
  if (!repositoryOptions.length) {
3053
3054
  if (repository) setRepository("");
3055
+ if (repositories.length) setRepositories([]);
3054
3056
  return;
3055
3057
  }
3056
3058
  if (!repositoryOptions.some((repo) => repo?.slug === repository)) {
3057
3059
  const primary = repositoryOptions.find((repo) => repo?.primary);
3058
- setRepository(primary?.slug || repositoryOptions[0]?.slug || "");
3060
+ const defaultSlug = primary?.slug || repositoryOptions[0]?.slug || "";
3061
+ setRepository(defaultSlug);
3062
+ setRepositories(defaultSlug ? [defaultSlug] : []);
3059
3063
  }
3060
3064
  }, [workspaceId, repositoryOptions.length]);
3061
3065
 
3066
+ const toggleRepo = (slug) => {
3067
+ setRepositories((prev) =>
3068
+ prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug],
3069
+ );
3070
+ // Keep single-repo compat: repository = first selected
3071
+ setRepository((prev) => {
3072
+ if (repositories.includes(slug)) {
3073
+ const next = repositories.filter((s) => s !== slug);
3074
+ return next[0] || "";
3075
+ }
3076
+ return prev || slug;
3077
+ });
3078
+ };
3079
+
3062
3080
  const handleSubmit = async () => {
3063
3081
  if (!title.trim()) {
3064
3082
  showToast("Title is required", "error");
@@ -3067,6 +3085,7 @@ function CreateTaskModalInline({ onClose }) {
3067
3085
  setSubmitting(true);
3068
3086
  haptic("medium");
3069
3087
  const tags = normalizeTagInput(tagsInput);
3088
+ const effectiveRepos = repositories.length > 0 ? repositories : (repository ? [repository] : []);
3070
3089
  try {
3071
3090
  await apiFetch("/api/tasks/create", {
3072
3091
  method: "POST",
@@ -3079,7 +3098,8 @@ function CreateTaskModalInline({ onClose }) {
3079
3098
  draft,
3080
3099
  status: draft ? "draft" : "todo",
3081
3100
  workspace: workspaceId || undefined,
3082
- repository: repository || undefined,
3101
+ repository: effectiveRepos[0] || undefined,
3102
+ repositories: effectiveRepos.length > 1 ? effectiveRepos : undefined,
3083
3103
  }),
3084
3104
  });
3085
3105
  showToast("Task created", "success");
@@ -3185,8 +3205,8 @@ function CreateTaskModalInline({ onClose }) {
3185
3205
  title="Use AI to expand and improve this task description"
3186
3206
  >
3187
3207
  ${rewriting
3188
- ? html`<span class="spin-icon" style="display:inline-block;animation:spin 0.8s linear infinite">${resolveIcon("")}</span> Improving…`
3189
- : html`${iconText(" Improve with AI")}`
3208
+ ? html`<span class="spin-icon" style="display:inline-block;animation:spin 0.8s linear infinite">${resolveIcon(":clock:")}</span> Improving…`
3209
+ : html`${iconText(":star: Improve with AI")}`
3190
3210
  }
3191
3211
  </button>
3192
3212
 
@@ -3215,21 +3235,30 @@ function CreateTaskModalInline({ onClose }) {
3215
3235
  (ws) => html`<option value=${ws.id}>${ws.name || ws.id}</option>`,
3216
3236
  )}
3217
3237
  </select>
3218
- <select
3219
- class="input"
3220
- value=${repository}
3221
- onChange=${(e) => setRepository(e.target.value)}
3222
- disabled=${!repositoryOptions.length}
3223
- >
3224
- <option value="">
3225
- ${repositoryOptions.length ? "Auto repo" : "No repos"}
3226
- </option>
3227
- ${repositoryOptions.map(
3228
- (repo) =>
3229
- html`<option value=${repo.slug}>${repo.name}${repo.primary ? " (Primary)" : ""}</option>`,
3230
- )}
3231
- </select>
3232
3238
  </div>
3239
+ ${repositoryOptions.length > 0 && html`
3240
+ <div class="repo-select-group">
3241
+ ${repositoryOptions.length === 1
3242
+ ? html`<div class="repo-auto-label">
3243
+ Repo: <strong>${repositoryOptions[0].name}</strong>
3244
+ ${repositoryOptions[0].primary ? " (Primary)" : ""}
3245
+ </div>`
3246
+ : html`<div class="repo-checkboxes">
3247
+ <span class="repo-checkboxes-label">Repositories</span>
3248
+ ${repositoryOptions.map((repo) => html`
3249
+ <label class="repo-checkbox-item">
3250
+ <input
3251
+ type="checkbox"
3252
+ checked=${repositories.includes(repo.slug)}
3253
+ onChange=${() => toggleRepo(repo.slug)}
3254
+ />
3255
+ ${repo.name}${repo.primary ? " (Primary)" : ""}
3256
+ </label>
3257
+ `)}
3258
+ </div>`
3259
+ }
3260
+ </div>
3261
+ `}
3233
3262
  `}
3234
3263
 
3235
3264
  <!-- Tags -->
@@ -3252,7 +3281,7 @@ function CreateTaskModalInline({ onClose }) {
3252
3281
  onClick=${() => setShowAdvanced(!showAdvanced)}
3253
3282
  type="button"
3254
3283
  >
3255
- <span style="display:inline-block;transition:transform 0.15s;transform:rotate(${showAdvanced ? 90 : 0}deg)">▶</span>
3284
+ <span style="display:inline-block;transition:transform 0.15s;transform:rotate(${showAdvanced ? 90 : 0}deg)">:play:</span>
3256
3285
  Advanced${hasAdvanced && !showAdvanced ? " •" : ""}
3257
3286
  </button>
3258
3287