bosun 0.42.0 → 0.42.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 (63) hide show
  1. package/.env.example +12 -0
  2. package/README.md +2 -0
  3. package/agent/agent-pool.mjs +34 -1
  4. package/agent/agent-work-report.mjs +89 -3
  5. package/agent/analyze-agent-work-helpers.mjs +14 -0
  6. package/agent/analyze-agent-work.mjs +23 -3
  7. package/agent/primary-agent.mjs +23 -1
  8. package/bosun-tui.mjs +4 -3
  9. package/bosun.schema.json +1 -1
  10. package/config/config.mjs +58 -0
  11. package/config/workspace-health.mjs +36 -6
  12. package/git/diff-stats.mjs +550 -124
  13. package/github/github-app-auth.mjs +9 -5
  14. package/infra/maintenance.mjs +13 -6
  15. package/infra/monitor.mjs +398 -10
  16. package/infra/runtime-accumulator.mjs +9 -1
  17. package/infra/session-tracker.mjs +163 -1
  18. package/infra/tui-bridge.mjs +415 -0
  19. package/infra/worktree-recovery-state.mjs +159 -0
  20. package/kanban/kanban-adapter.mjs +41 -8
  21. package/lib/repo-map.mjs +411 -0
  22. package/package.json +140 -137
  23. package/server/ui-server.mjs +953 -59
  24. package/shell/codex-config.mjs +34 -8
  25. package/task/task-cli.mjs +93 -19
  26. package/task/task-executor.mjs +397 -8
  27. package/task/task-store.mjs +194 -1
  28. package/telegram/telegram-bot.mjs +267 -18
  29. package/tools/vitest-runner.mjs +108 -0
  30. package/tui/app.mjs +252 -148
  31. package/tui/components/status-header.mjs +88 -131
  32. package/tui/lib/ws-bridge.mjs +125 -35
  33. package/tui/screens/agents-screen-helpers.mjs +219 -0
  34. package/tui/screens/agents.mjs +287 -270
  35. package/tui/screens/status.mjs +51 -189
  36. package/tui/screens/tasks.mjs +41 -253
  37. package/ui/app.js +52 -23
  38. package/ui/components/chat-view.js +263 -84
  39. package/ui/components/diff-viewer.js +324 -140
  40. package/ui/components/kanban-board.js +13 -9
  41. package/ui/components/session-list.js +111 -41
  42. package/ui/demo-defaults.js +481 -59
  43. package/ui/demo.html +32 -0
  44. package/ui/modules/session-api.js +320 -5
  45. package/ui/modules/stream-timeline.js +356 -0
  46. package/ui/modules/telegram.js +5 -2
  47. package/ui/modules/worktree-recovery.js +85 -0
  48. package/ui/styles.css +44 -0
  49. package/ui/tabs/chat.js +19 -4
  50. package/ui/tabs/dashboard.js +22 -0
  51. package/ui/tabs/infra.js +25 -0
  52. package/ui/tabs/tasks.js +119 -11
  53. package/voice/voice-auth-manager.mjs +10 -5
  54. package/workflow/workflow-engine.mjs +179 -1
  55. package/workflow/workflow-nodes.mjs +872 -16
  56. package/workflow/workflow-templates.mjs +4 -0
  57. package/workflow-templates/github.mjs +2 -1
  58. package/workflow-templates/planning.mjs +2 -1
  59. package/workflow-templates/sub-workflows.mjs +10 -0
  60. package/workflow-templates/task-batch.mjs +9 -8
  61. package/workflow-templates/task-execution.mjs +30 -12
  62. package/workflow-templates/task-lifecycle.mjs +59 -4
  63. package/workspace/shared-knowledge.mjs +409 -155
@@ -114,6 +114,7 @@ function resolveSessionMaxMessages(type, metadata, explicitMax, fallbackMax) {
114
114
  const FLUSH_INTERVAL_MS = 2000;
115
115
 
116
116
  const SESSION_EVENT_LISTENERS = new Set();
117
+ const SESSION_STATE_LISTENERS = new Set();
117
118
 
118
119
  export function addSessionEventListener(listener) {
119
120
  if (typeof listener !== "function") return () => {};
@@ -121,6 +122,12 @@ export function addSessionEventListener(listener) {
121
122
  return () => SESSION_EVENT_LISTENERS.delete(listener);
122
123
  }
123
124
 
125
+ export function addSessionStateListener(listener) {
126
+ if (typeof listener !== "function") return () => {};
127
+ SESSION_STATE_LISTENERS.add(listener);
128
+ return () => SESSION_STATE_LISTENERS.delete(listener);
129
+ }
130
+
124
131
  function emitSessionEvent(session, message) {
125
132
  if (!session || !message || SESSION_EVENT_LISTENERS.size === 0) return;
126
133
  const payload = {
@@ -145,6 +152,37 @@ function emitSessionEvent(session, message) {
145
152
  }
146
153
  }
147
154
 
155
+ function emitSessionStateEvent(session, reason, extra = {}) {
156
+ if (!session || SESSION_STATE_LISTENERS.size === 0) return;
157
+ const normalizedReason = String(reason || "updated").trim() || "updated";
158
+ const payload = {
159
+ sessionId: session.id || session.taskId,
160
+ taskId: session.taskId || session.id,
161
+ reason: normalizedReason,
162
+ session: {
163
+ id: session.id || session.taskId,
164
+ taskId: session.taskId || session.id,
165
+ type: session.type || "task",
166
+ status: session.status || "active",
167
+ lastActiveAt: session.lastActiveAt || new Date().toISOString(),
168
+ turnCount: session.turnCount || 0,
169
+ title: session.taskTitle || session.title || null,
170
+ },
171
+ event: {
172
+ kind: "state",
173
+ reason: normalizedReason,
174
+ ...extra,
175
+ },
176
+ };
177
+ for (const listener of SESSION_STATE_LISTENERS) {
178
+ try {
179
+ listener(payload);
180
+ } catch {
181
+ // best-effort listeners
182
+ }
183
+ }
184
+ }
185
+
148
186
  // ── SessionTracker Class ────────────────────────────────────────────────────
149
187
 
150
188
  export class SessionTracker {
@@ -226,8 +264,12 @@ export class SessionTracker {
226
264
  lastActivityAt: Date.now(),
227
265
  metadata: {},
228
266
  insights: buildSessionInsights({ messages: [] }),
267
+ trajectory: { version: 1, replayable: true, steps: [] },
268
+ summary: null,
229
269
  });
270
+ const session = this.#sessions.get(taskId);
230
271
  this.#markDirty(taskId);
272
+ emitSessionStateEvent(session, "session-created", { title: taskTitle || taskId });
231
273
  }
232
274
 
233
275
  /**
@@ -279,6 +321,7 @@ export class SessionTracker {
279
321
  if (Number.isFinite(maxMessages) && maxMessages > 0) {
280
322
  while (session.messages.length > maxMessages) session.messages.shift();
281
323
  }
324
+ this.#appendTrajectoryStep(session, event);
282
325
  this.#refreshDerivedState(session);
283
326
  this.#markDirty(taskId);
284
327
  emitSessionEvent(session, msg);
@@ -312,6 +355,7 @@ export class SessionTracker {
312
355
  if (Number.isFinite(maxMessages) && maxMessages > 0) {
313
356
  while (session.messages.length > maxMessages) session.messages.shift();
314
357
  }
358
+ this.#appendTrajectoryStep(session, event);
315
359
  this.#refreshDerivedState(session);
316
360
  this.#markDirty(taskId);
317
361
  emitSessionEvent(session, msg);
@@ -329,6 +373,7 @@ export class SessionTracker {
329
373
  if (Number.isFinite(maxMessages) && maxMessages > 0) {
330
374
  while (session.messages.length > maxMessages) session.messages.shift();
331
375
  }
376
+ this.#appendTrajectoryStep(session, event);
332
377
  this.#refreshDerivedState(session);
333
378
  this.#markDirty(taskId);
334
379
  emitSessionEvent(session, msg);
@@ -348,6 +393,7 @@ export class SessionTracker {
348
393
  this.#refreshDerivedState(session);
349
394
  this.#accumulateCompletedSession(session, taskId);
350
395
  this.#markDirty(taskId);
396
+ emitSessionStateEvent(session, "session-ended", { status });
351
397
  }
352
398
 
353
399
  /**
@@ -578,10 +624,13 @@ export class SessionTracker {
578
624
  metadata,
579
625
  maxMessages: resolvedMax,
580
626
  insights: buildSessionInsights({ messages: [] }),
627
+ trajectory: { version: 1, replayable: true, steps: [] },
628
+ summary: null,
581
629
  };
582
630
  this.#sessions.set(id, session);
583
631
  this.#markDirty(id);
584
632
  this.#flushDirty(); // immediate write for create
633
+ emitSessionStateEvent(session, "session-created");
585
634
  return session;
586
635
  }
587
636
 
@@ -599,18 +648,23 @@ export class SessionTracker {
599
648
  const derivedStatus = progress?.status === "ended"
600
649
  ? "completed"
601
650
  : (progress?.status || s.status);
651
+ const lastActiveAt = s.lastActiveAt || new Date(s.lastActivityAt).toISOString();
602
652
  list.push({
603
653
  id: s.id || s.taskId,
604
654
  taskId: s.taskId,
605
655
  title: s.taskTitle || s.title || null,
606
656
  type: s.type || "task",
607
657
  status: derivedStatus,
658
+ lifecycleStatus: s.status || "active",
659
+ runtimeState: progress?.status || null,
660
+ runtimeUpdatedAt: lastActiveAt,
661
+ runtimeIsLive: Boolean(progress && progress.status !== "ended" && progress.status !== "not_found"),
608
662
  workspaceId: String(s?.metadata?.workspaceId || "").trim() || null,
609
663
  workspaceDir: String(s?.metadata?.workspaceDir || "").trim() || null,
610
664
  branch: String(s?.metadata?.branch || "").trim() || null,
611
665
  turnCount: s.turnCount || 0,
612
666
  createdAt: s.createdAt || new Date(s.startedAt).toISOString(),
613
- lastActiveAt: s.lastActiveAt || new Date(s.lastActivityAt).toISOString(),
667
+ lastActiveAt,
614
668
  idleMs: progress?.idleMs ?? 0,
615
669
  elapsedMs: progress?.elapsedMs ?? Math.max(0, Date.now() - Number(s.startedAt || Date.now())),
616
670
  recommendation: progress?.recommendation || "none",
@@ -658,6 +712,7 @@ export class SessionTracker {
658
712
  this.#refreshDerivedState(session);
659
713
  this.#accumulateCompletedSession(session, sessionId);
660
714
  this.#markDirty(sessionId);
715
+ emitSessionStateEvent(session, "session-status", { status });
661
716
  }
662
717
 
663
718
  /**
@@ -671,6 +726,7 @@ export class SessionTracker {
671
726
  session.taskTitle = newTitle;
672
727
  session.title = newTitle;
673
728
  this.#markDirty(sessionId);
729
+ emitSessionStateEvent(session, "session-renamed", { title: newTitle });
674
730
  }
675
731
 
676
732
  /**
@@ -748,6 +804,13 @@ export class SessionTracker {
748
804
  this.#flushDirty();
749
805
  }
750
806
 
807
+ /**
808
+ * Flush all dirty sessions to disk immediately (alias for flush).
809
+ */
810
+ flushNow() {
811
+ this.#flushDirty();
812
+ }
813
+
751
814
  /**
752
815
  * Stop all timers and flush pending writes (for cleanup).
753
816
  */
@@ -894,6 +957,7 @@ export class SessionTracker {
894
957
  this.#refreshDerivedState(session);
895
958
  this.#accumulateCompletedSession(session, id);
896
959
  this.#markDirty(id);
960
+ emitSessionStateEvent(session, "session-idle-timeout", { status: session.status, idleMs });
897
961
  reaped++;
898
962
  }
899
963
  }
@@ -953,6 +1017,84 @@ export class SessionTracker {
953
1017
  return true;
954
1018
  }
955
1019
 
1020
+ #appendTrajectoryStep(session, event) {
1021
+ if (!session) return;
1022
+ if (!session.trajectory) {
1023
+ session.trajectory = { version: 1, replayable: true, steps: [] };
1024
+ }
1025
+ const step = this.#extractTrajectoryStep(event, session);
1026
+ if (step) {
1027
+ session.trajectory.steps.push(step);
1028
+ }
1029
+ }
1030
+
1031
+ #extractTrajectoryStep(event, session) {
1032
+ const ts = new Date().toISOString();
1033
+ const id = `step-${Date.now()}-${randomToken(6)}`;
1034
+
1035
+ // String event
1036
+ if (typeof event === "string") {
1037
+ return { id, kind: "system", summary: event.trim().slice(0, 200), timestamp: ts };
1038
+ }
1039
+
1040
+ // Direct message format (role/content)
1041
+ if (event?.role && event?.content !== undefined) {
1042
+ const content = String(event.content).slice(0, 200);
1043
+ const timestamp = event.timestamp || ts;
1044
+ if (event.role === "user") return { id, kind: "user_message", summary: content, timestamp };
1045
+ if (event.role === "assistant") return { id, kind: "assistant", summary: content, timestamp };
1046
+ return { id, kind: event.role, summary: content, timestamp };
1047
+ }
1048
+
1049
+ // SDK item.started events
1050
+ if (event?.type === "item.started" && event?.item) {
1051
+ const item = event.item;
1052
+ if (item.type === "command_execution") {
1053
+ return { id, kind: "tool_call", summary: `Ran ${item.command || "unknown"}`, timestamp: ts };
1054
+ }
1055
+ if (item.type === "reasoning") {
1056
+ return { id, kind: "reasoning", summary: item.text || "", timestamp: ts };
1057
+ }
1058
+ if (item.type === "function_call" || item.type === "mcp_tool_call") {
1059
+ return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: ts };
1060
+ }
1061
+ return null;
1062
+ }
1063
+
1064
+ // SDK item.completed events
1065
+ if (event?.type === "item.completed" && event?.item) {
1066
+ const item = event.item;
1067
+ if (item.type === "reasoning") {
1068
+ return { id, kind: "reasoning", summary: item.text || "", timestamp: ts };
1069
+ }
1070
+ if (item.type === "function_call" || item.type === "mcp_tool_call") {
1071
+ return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: ts };
1072
+ }
1073
+ if (item.type === "command_execution") {
1074
+ const cmd = item.command || "";
1075
+ const hasPriorStart = (session?.trajectory?.steps || []).some(
1076
+ (s) => s.kind === "tool_call" && s.summary === `Ran ${cmd}`,
1077
+ );
1078
+ if (hasPriorStart) {
1079
+ return { id, kind: "tool_result", summary: `${cmd} (exit ${item.exit_code ?? "?"})`, timestamp: ts };
1080
+ }
1081
+ return { id, kind: "command", summary: cmd, timestamp: ts };
1082
+ }
1083
+ if (item.type === "agent_message") {
1084
+ return { id, kind: "assistant", summary: item.text || "", timestamp: ts };
1085
+ }
1086
+ return null;
1087
+ }
1088
+
1089
+ // Assistant message events
1090
+ if (event?.type === "assistant.message") {
1091
+ const content = event?.data?.content || event?.content || "";
1092
+ return { id, kind: "agent_message", summary: content.slice(0, 200), timestamp: ts };
1093
+ }
1094
+
1095
+ return null;
1096
+ }
1097
+
956
1098
  #refreshDerivedState(session) {
957
1099
  if (!session) return;
958
1100
  try {
@@ -963,6 +1105,22 @@ export class SessionTracker {
963
1105
  } catch {
964
1106
  // Inspector insights are best-effort only.
965
1107
  }
1108
+ try {
1109
+ const steps = session.trajectory?.steps || [];
1110
+ const totalSteps = steps.length;
1111
+ const isFailed = session.status === "failed";
1112
+ const isLong = totalSteps > 12;
1113
+ const failedOrLongRun = isFailed || isLong;
1114
+ const resumable = failedOrLongRun;
1115
+ const shortSteps = steps.slice(-12).map((s) => ({ kind: s.kind, summary: s.summary }));
1116
+ const latestStep =
1117
+ steps.length > 0
1118
+ ? { kind: steps[steps.length - 1].kind, summary: steps[steps.length - 1].summary }
1119
+ : null;
1120
+ session.summary = { failedOrLongRun, resumable, totalSteps, shortSteps, latestStep };
1121
+ } catch {
1122
+ // Summary computation is best-effort only.
1123
+ }
966
1124
  }
967
1125
 
968
1126
  #ensureDir() {
@@ -1002,6 +1160,8 @@ export class SessionTracker {
1002
1160
  messages: session.messages || [],
1003
1161
  metadata: session.metadata || {},
1004
1162
  insights: session.insights || null,
1163
+ trajectory: session.trajectory || null,
1164
+ summary: session.summary || null,
1005
1165
  };
1006
1166
  writeFileSync(filePath, JSON.stringify(data, null, 2));
1007
1167
  } catch (err) {
@@ -1081,6 +1241,8 @@ export class SessionTracker {
1081
1241
  lastActivityAt: lastActive || Date.now(),
1082
1242
  metadata: data.metadata || {},
1083
1243
  insights: data.insights || buildSessionInsights({ messages: data.messages || [] }),
1244
+ trajectory: data.trajectory || { version: 1, replayable: true, steps: [] },
1245
+ summary: data.summary || null,
1084
1246
  });
1085
1247
  const restored = this.#sessions.get(id);
1086
1248
  if (restored && isTerminalSessionStatus(restored.status) && !restored.accumulatedAt) {