agent-relay-server 0.110.0 → 0.112.0

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 (83) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +2 -2
  3. package/public/assets/{MessageBubble-CmqJMGYF.js → MessageBubble-DtLlfBAK.js} +2 -2
  4. package/public/assets/{MessageBubble-CmqJMGYF.js.map → MessageBubble-DtLlfBAK.js.map} +1 -1
  5. package/public/assets/PlanGraphPanel-pcKJ4q_4.js +2 -0
  6. package/public/assets/PlanGraphPanel-pcKJ4q_4.js.map +1 -0
  7. package/public/assets/{activity-DyqKrJe_.js → activity-DRAeL0Jq.js} +2 -2
  8. package/public/assets/{activity-DyqKrJe_.js.map → activity-DRAeL0Jq.js.map} +1 -1
  9. package/public/assets/{agent-profiles-DRQ5vbPo.js → agent-profiles-cnWPANzx.js} +2 -2
  10. package/public/assets/{agent-profiles-DRQ5vbPo.js.map → agent-profiles-cnWPANzx.js.map} +1 -1
  11. package/public/assets/{agents-BJ9sm23O.js → agents-DgUjmk_D.js} +2 -2
  12. package/public/assets/{agents-BJ9sm23O.js.map → agents-DgUjmk_D.js.map} +1 -1
  13. package/public/assets/{automation-CJ2-uiYt.js → automation-CuTlgY1E.js} +2 -2
  14. package/public/assets/{automation-CJ2-uiYt.js.map → automation-CuTlgY1E.js.map} +1 -1
  15. package/public/assets/{chat-D0VtHShz.js → chat-C7XCmaZz.js} +2 -2
  16. package/public/assets/{chat-D0VtHShz.js.map → chat-C7XCmaZz.js.map} +1 -1
  17. package/public/assets/{formatted-body-impl-ChCxpVtI.js → formatted-body-impl-rLDPgdmJ.js} +2 -2
  18. package/public/assets/{formatted-body-impl-ChCxpVtI.js.map → formatted-body-impl-rLDPgdmJ.js.map} +1 -1
  19. package/public/assets/{index-BIRtwnUI.js → index-DGyeTLPg.js} +5 -5
  20. package/public/assets/index-DGyeTLPg.js.map +1 -0
  21. package/public/assets/{insights-BGuB8DWZ.js → insights-C0dDL4El.js} +2 -2
  22. package/public/assets/{insights-BGuB8DWZ.js.map → insights-C0dDL4El.js.map} +1 -1
  23. package/public/assets/{maintenance-BFyuZuv5.js → maintenance-DkOkcEkb.js} +2 -2
  24. package/public/assets/{maintenance-BFyuZuv5.js.map → maintenance-DkOkcEkb.js.map} +1 -1
  25. package/public/assets/{managed-agents-BIXZUkrA.js → managed-agents-C7Czinaz.js} +2 -2
  26. package/public/assets/{managed-agents-BIXZUkrA.js.map → managed-agents-C7Czinaz.js.map} +1 -1
  27. package/public/assets/{markdown-preview-impl-DTI--VmH.js → markdown-preview-impl-BZ7sigEc.js} +2 -2
  28. package/public/assets/{markdown-preview-impl-DTI--VmH.js.map → markdown-preview-impl-BZ7sigEc.js.map} +1 -1
  29. package/public/assets/{memory-OoM2E30h.js → memory-Bys-M7GK.js} +2 -2
  30. package/public/assets/{memory-OoM2E30h.js.map → memory-Bys-M7GK.js.map} +1 -1
  31. package/public/assets/{messages-BHvC3SNR.js → messages-COSJ6trN.js} +2 -2
  32. package/public/assets/{messages-BHvC3SNR.js.map → messages-COSJ6trN.js.map} +1 -1
  33. package/public/assets/{orchestrators-9pzQqf6-.js → orchestrators-BjLl1Z0L.js} +2 -2
  34. package/public/assets/{orchestrators-9pzQqf6-.js.map → orchestrators-BjLl1Z0L.js.map} +1 -1
  35. package/public/assets/{overview-L8iYOYmR.js → overview-BmU7twic.js} +2 -2
  36. package/public/assets/{overview-L8iYOYmR.js.map → overview-BmU7twic.js.map} +1 -1
  37. package/public/assets/{pairs-BCejxa6_.js → pairs-xclXjw-c.js} +2 -2
  38. package/public/assets/{pairs-BCejxa6_.js.map → pairs-xclXjw-c.js.map} +1 -1
  39. package/public/assets/{prompt-settings-D9y9hhSy.js → prompt-settings-CtQd0nz0.js} +2 -2
  40. package/public/assets/{prompt-settings-D9y9hhSy.js.map → prompt-settings-CtQd0nz0.js.map} +1 -1
  41. package/public/assets/{registry-BwBg64Vj.js → registry-CDe6vjKI.js} +2 -2
  42. package/public/assets/{registry-BwBg64Vj.js.map → registry-CDe6vjKI.js.map} +1 -1
  43. package/public/assets/{security-upd2zf8l.js → security-C9mBNQMZ.js} +2 -2
  44. package/public/assets/{security-upd2zf8l.js.map → security-C9mBNQMZ.js.map} +1 -1
  45. package/public/assets/{select-Cu55Vi-R.js → select-BJDKQk4q.js} +2 -2
  46. package/public/assets/{select-Cu55Vi-R.js.map → select-BJDKQk4q.js.map} +1 -1
  47. package/public/assets/{settings-Dk5iNqCZ.js → settings-BmZjWJkI.js} +2 -2
  48. package/public/assets/{settings-Dk5iNqCZ.js.map → settings-BmZjWJkI.js.map} +1 -1
  49. package/public/assets/{tasks-BoDuvHnZ.js → tasks-DNjZKSM2.js} +2 -2
  50. package/public/assets/tasks-DNjZKSM2.js.map +1 -0
  51. package/public/assets/{teams-C6Pn-gpw.js → teams-KNzmdolE.js} +2 -2
  52. package/public/assets/{teams-C6Pn-gpw.js.map → teams-KNzmdolE.js.map} +1 -1
  53. package/public/assets/{terminal-viewer-impl-afW96xEF.js → terminal-viewer-impl-DnFJDl9N.js} +2 -2
  54. package/public/assets/{terminal-viewer-impl-afW96xEF.js.map → terminal-viewer-impl-DnFJDl9N.js.map} +1 -1
  55. package/public/assets/{user-avatar-tSPnqEk5.js → user-avatar-C8sbjQke.js} +2 -2
  56. package/public/assets/{user-avatar-tSPnqEk5.js.map → user-avatar-C8sbjQke.js.map} +1 -1
  57. package/public/assets/{work-queue-DP3UdDwC.js → work-queue-DINvPVPn.js} +2 -2
  58. package/public/assets/{work-queue-DP3UdDwC.js.map → work-queue-DINvPVPn.js.map} +1 -1
  59. package/public/assets/{workspaces-Dt2xndG4.js → workspaces-CSNdUg15.js} +2 -2
  60. package/public/assets/{workspaces-Dt2xndG4.js.map → workspaces-CSNdUg15.js.map} +1 -1
  61. package/public/index.html +1 -1
  62. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  63. package/src/db/agents.ts +3 -0
  64. package/src/db/notification-subscriptions.ts +15 -0
  65. package/src/db/task-entity.ts +2 -0
  66. package/src/mcp-notification-subscription-tools.ts +2 -1
  67. package/src/mcp-task-tools.ts +2 -1
  68. package/src/notification-rules.ts +17 -1
  69. package/src/notification-types.ts +18 -0
  70. package/src/reviewer-pipeline.ts +13 -0
  71. package/src/routes/commands.ts +14 -0
  72. package/src/routes/tasks.ts +1 -0
  73. package/src/services/spawn-agent.ts +2 -0
  74. package/src/services/spawn-registration-watchdog.ts +5 -5
  75. package/src/services/task-done-when.ts +21 -0
  76. package/src/services/task-entity.ts +30 -7
  77. package/src/services/task-lifecycle.ts +45 -46
  78. package/src/services/task-semantic-events.ts +241 -13
  79. package/src/workspace-auto-merge.ts +17 -0
  80. package/public/assets/PlanGraphPanel-K6aFJmdd.js +0 -2
  81. package/public/assets/PlanGraphPanel-K6aFJmdd.js.map +0 -1
  82. package/public/assets/index-BIRtwnUI.js.map +0 -1
  83. package/public/assets/tasks-BoDuvHnZ.js.map +0 -1
@@ -1,30 +1,75 @@
1
1
  import { getAgent } from "../db";
2
+ import { addSubscription } from "../db/notification-subscriptions";
2
3
  import { emitRelayEvent } from "../events";
4
+ import { notifySystemMessage } from "../notify";
3
5
  import type { NotificationScope, NotificationType } from "../notification-types";
4
6
  import { routeNotification } from "../notification-router";
5
7
  import { resolveAudience } from "../notification-rules";
6
- import type { Task, TaskBlocker, TaskStatus } from "../types";
8
+ import type { Task, TaskBlocker, TaskDeliverableStage, TaskDetail, TaskIssueLink, TaskStatus } from "../types";
7
9
 
8
- export type TaskSemanticTransition = "created" | "started" | "blocked" | "needs-decision" | "landed" | "closed";
10
+ export type TaskSemanticTransition =
11
+ | KnownTaskSemanticTransition
12
+ | (string & {});
9
13
 
10
- const TASK_NOTIFICATION_TYPE: Record<TaskSemanticTransition, NotificationType> = {
14
+ type KnownTaskSemanticTransition =
15
+ | "created"
16
+ | "working"
17
+ | "blocked"
18
+ | "needs-decision"
19
+ | "gate-failed"
20
+ | "committed-on-branch"
21
+ | "review-requested"
22
+ | "land-conflict"
23
+ | "landed-on-main"
24
+ | "done"
25
+ | "deploying"
26
+ | "deployed";
27
+
28
+ export type TaskSemanticClassification = "action-required" | "status-pulse" | "suppressed";
29
+
30
+ const TASK_NOTIFICATION_TYPE: Record<KnownTaskSemanticTransition, NotificationType> = {
11
31
  created: "task.created",
12
- started: "task.started",
32
+ working: "task.working",
13
33
  blocked: "task.blocked",
14
34
  "needs-decision": "task.needs_decision",
15
- landed: "task.landed",
16
- closed: "task.closed",
35
+ "gate-failed": "task.gate_failed",
36
+ "committed-on-branch": "task.committed_on_branch",
37
+ "review-requested": "task.review_requested",
38
+ "land-conflict": "task.land_conflict",
39
+ "landed-on-main": "task.landed_on_main",
40
+ done: "task.done",
41
+ deploying: "task.deploying",
42
+ deployed: "task.deployed",
17
43
  };
44
+ const UNKNOWN_TASK_NOTIFICATION_TYPE: NotificationType = "task.working";
45
+ export const TASK_SEMANTIC_NOTIFICATION_TYPES = Object.values(TASK_NOTIFICATION_TYPE);
18
46
 
19
47
  export function taskTransitionForStatus(status: TaskStatus, blockers: TaskBlocker[] | undefined): TaskSemanticTransition | null {
20
- if (status === "claimed" || status === "in_progress") return "started";
48
+ if (status === "claimed" || status === "in_progress") return "working";
21
49
  if (status === "blocked") {
22
50
  return blockers?.some((blocker) => blocker.type === "decision") ? "needs-decision" : "blocked";
23
51
  }
24
- if (status === "done" || status === "failed" || status === "canceled") return "closed";
52
+ if (status === "done") return "done";
53
+ if (status === "failed") return "gate-failed";
54
+ return null;
55
+ }
56
+
57
+ export function taskTransitionForStage(stage: TaskDeliverableStage | undefined): TaskSemanticTransition | null {
58
+ if (stage === "committed-on-branch") return "committed-on-branch";
59
+ if (stage === "landed-on-main") return "landed-on-main";
60
+ if (stage === "deploying") return "deploying";
61
+ if (stage === "deployed") return "deployed";
25
62
  return null;
26
63
  }
27
64
 
65
+ export function classifyTaskSemanticTransition(transition: TaskSemanticTransition): TaskSemanticClassification {
66
+ if (transition === "blocked" || transition === "needs-decision" || transition === "gate-failed" || transition === "review-requested" || transition === "land-conflict") {
67
+ return "action-required";
68
+ }
69
+ if (transition === "created") return "suppressed";
70
+ return "status-pulse";
71
+ }
72
+
28
73
  export interface TaskSemanticNotificationInput {
29
74
  task: Task;
30
75
  transition: TaskSemanticTransition;
@@ -38,15 +83,55 @@ export interface TaskSemanticNotificationInput {
38
83
  outcome?: Record<string, unknown>;
39
84
  }
40
85
 
86
+ const TASK_STATUS_PULSE_BATCH_MS = 1000;
87
+ const TASK_NOTIFICATION_TRANSITIONS = Object.keys(TASK_NOTIFICATION_TYPE) as KnownTaskSemanticTransition[];
88
+
89
+ interface StatusPulseDigestItem {
90
+ taskId: number;
91
+ title: string;
92
+ transition: TaskSemanticTransition;
93
+ classification: "status-pulse";
94
+ issue?: Pick<TaskIssueLink, "provider" | "repo" | "externalKey" | "url" | "title">;
95
+ branch?: string;
96
+ sha?: string;
97
+ outcome?: Record<string, unknown>;
98
+ occurredAt: number;
99
+ }
100
+
101
+ interface StatusPulseBatch {
102
+ timer?: ReturnType<typeof setTimeout>;
103
+ items: StatusPulseDigestItem[];
104
+ }
105
+
106
+ const statusPulseBatches = new Map<string, StatusPulseBatch>();
107
+
108
+ export function autoSubscribeTaskCoordinator(agentId: string | undefined, taskId: number): boolean {
109
+ const cleaned = agentId?.trim();
110
+ if (!cleaned || !getAgent(cleaned)) return false;
111
+ for (const transition of TASK_NOTIFICATION_TRANSITIONS) {
112
+ addSubscription({
113
+ agentId: cleaned,
114
+ type: TASK_NOTIFICATION_TYPE[transition],
115
+ mode: "opt_in",
116
+ scopeMatch: { taskId: String(taskId) },
117
+ });
118
+ }
119
+ return true;
120
+ }
121
+
41
122
  export function emitTaskSemanticNotification(input: TaskSemanticNotificationInput): void {
42
- const type = TASK_NOTIFICATION_TYPE[input.transition];
123
+ const type = TASK_NOTIFICATION_TYPE[input.transition as KnownTaskSemanticTransition] ?? UNKNOWN_TASK_NOTIFICATION_TYPE;
43
124
  const scope = taskNotificationScope(input);
44
125
  const semanticEvent = `task.${input.transition}`;
126
+ const classification = classifyTaskSemanticTransition(input.transition);
127
+ const issue = primaryIssue(input.task);
45
128
  const eventData = {
46
129
  taskId: input.task.id,
47
130
  transition: input.transition,
131
+ classification,
48
132
  semanticEvent,
49
133
  title: input.task.title,
134
+ ...(issue ? { issue } : {}),
50
135
  status: input.task.status,
51
136
  deliverableStage: input.task.deliverableStage,
52
137
  ...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
@@ -72,7 +157,24 @@ export function emitTaskSemanticNotification(input: TaskSemanticNotificationInpu
72
157
  .map((agentId) => ({ agentId, matchedRule: "task-stakeholder" }));
73
158
 
74
159
  const recipients = [...subscriptionAudience, ...explicit];
75
- if (recipients.length === 0) return;
160
+ if (recipients.length === 0 || classification === "suppressed") return;
161
+
162
+ if (classification === "status-pulse") {
163
+ for (const recipient of dedupeRecipients(recipients)) {
164
+ enqueueStatusPulse(recipient.agentId, {
165
+ taskId: input.task.id,
166
+ title: input.task.title,
167
+ transition: input.transition,
168
+ classification,
169
+ ...(issue ? { issue } : {}),
170
+ ...(input.branch ? { branch: input.branch } : {}),
171
+ ...(input.sha ? { sha: input.sha } : {}),
172
+ ...(input.outcome ? { outcome: input.outcome } : {}),
173
+ occurredAt: Date.now(),
174
+ });
175
+ }
176
+ return;
177
+ }
76
178
 
77
179
  routeNotification({
78
180
  type,
@@ -85,9 +187,13 @@ export function emitTaskSemanticNotification(input: TaskSemanticNotificationInpu
85
187
  payload: {
86
188
  kind: "task.transition",
87
189
  notification: "task-transition",
190
+ classification,
191
+ actionRequired: true,
88
192
  taskId: input.task.id,
193
+ ...(issue ? { issue } : {}),
89
194
  transition: input.transition,
90
195
  semanticEvent,
196
+ needed: taskNotificationNeeded(input),
91
197
  ...(input.outcome ? { outcome: input.outcome } : {}),
92
198
  ...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
93
199
  ...(input.branch ? { branch: input.branch } : {}),
@@ -99,6 +205,82 @@ export function emitTaskSemanticNotification(input: TaskSemanticNotificationInpu
99
205
  });
100
206
  }
101
207
 
208
+ function dedupeRecipients(recipients: Array<{ agentId: string; matchedRule: string }>): Array<{ agentId: string; matchedRule: string }> {
209
+ const seen = new Set<string>();
210
+ const out: Array<{ agentId: string; matchedRule: string }> = [];
211
+ for (const recipient of recipients) {
212
+ if (seen.has(recipient.agentId)) continue;
213
+ seen.add(recipient.agentId);
214
+ out.push(recipient);
215
+ }
216
+ return out;
217
+ }
218
+
219
+ function enqueueStatusPulse(agentId: string, item: StatusPulseDigestItem): void {
220
+ const batch = statusPulseBatches.get(agentId) ?? { items: [] };
221
+ batch.items.push(item);
222
+ if (!batch.timer) {
223
+ batch.timer = setTimeout(() => flushTaskStatusPulses(agentId), TASK_STATUS_PULSE_BATCH_MS);
224
+ }
225
+ statusPulseBatches.set(agentId, batch);
226
+ }
227
+
228
+ export function flushTaskStatusPulses(agentId?: string): void {
229
+ const targets = agentId ? [agentId] : [...statusPulseBatches.keys()];
230
+ for (const target of targets) {
231
+ const batch = statusPulseBatches.get(target);
232
+ if (!batch) continue;
233
+ if (batch.timer) clearTimeout(batch.timer);
234
+ statusPulseBatches.delete(target);
235
+ if (batch.items.length === 0) continue;
236
+ const items = compactStatusPulseItems(batch.items);
237
+ notifySystemMessage(target, {
238
+ subject: `Task status pulse (${items.length})`,
239
+ body: taskStatusPulseBody(items),
240
+ payload: {
241
+ kind: "task.status-pulse",
242
+ notification: "task-status-pulse",
243
+ classification: "status-pulse",
244
+ actionRequired: false,
245
+ taskIds: [...new Set(items.map((item) => item.taskId))],
246
+ events: items.map((item) => ({
247
+ taskId: item.taskId,
248
+ title: item.title,
249
+ transition: item.transition,
250
+ classification: item.classification,
251
+ ...(item.issue ? { issue: item.issue } : {}),
252
+ ...(item.branch ? { branch: item.branch } : {}),
253
+ ...(item.sha ? { sha: item.sha } : {}),
254
+ ...(item.outcome ? { outcome: item.outcome } : {}),
255
+ occurredAt: item.occurredAt,
256
+ })),
257
+ },
258
+ idempotencyKey: `task:status-pulse:${target}:${items.map((item) => `${item.taskId}:${item.transition}:${item.occurredAt}`).join("|")}`,
259
+ replyExpected: false,
260
+ maxAgeSeconds: null,
261
+ });
262
+ }
263
+ }
264
+
265
+ function compactStatusPulseItems(items: StatusPulseDigestItem[]): StatusPulseDigestItem[] {
266
+ const byKey = new Map<string, StatusPulseDigestItem>();
267
+ for (const item of items) byKey.set(`${item.taskId}:${item.transition}`, item);
268
+ return [...byKey.values()].sort((a, b) => a.occurredAt - b.occurredAt);
269
+ }
270
+
271
+ function taskStatusPulseBody(items: StatusPulseDigestItem[]): string {
272
+ const lines = ["Task status pulse:"];
273
+ for (const item of items) {
274
+ const bits = [`#${item.taskId}`, item.transition, item.title];
275
+ const issue = issueLabel(item.issue);
276
+ if (issue) bits.push(issue);
277
+ if (item.branch) bits.push(`branch ${item.branch}`);
278
+ if (item.sha) bits.push(`sha ${item.sha.slice(0, 12)}`);
279
+ lines.push(`- ${bits.join(" - ")}`);
280
+ }
281
+ return lines.join("\n");
282
+ }
283
+
102
284
  function taskNotificationIdempotencyKey(input: TaskSemanticNotificationInput): string | undefined {
103
285
  if (input.landKey) return `task:${input.task.id}:transition:${input.transition}:${input.landKey}`;
104
286
  if (input.occurrenceKey) return `task:${input.task.id}:transition:${input.transition}:occurrence:${input.occurrenceKey}`;
@@ -125,13 +307,59 @@ function taskNotificationSubject(input: TaskSemanticNotificationInput): string {
125
307
  }
126
308
 
127
309
  function taskNotificationBody(input: TaskSemanticNotificationInput): string {
128
- if (input.transition === "landed") {
129
- const bits = [`Task #${input.task.id} landed: ${input.task.title}`];
310
+ const issue = issueLabel(primaryIssue(input.task));
311
+ if (input.transition === "landed-on-main") {
312
+ const bits = [`Task #${input.task.id} landed on main: ${input.task.title}`];
130
313
  if (input.sha) bits.push(`sha ${input.sha.slice(0, 12)}`);
131
314
  const gatesPassed = input.outcome?.gatesPassed;
132
315
  if (typeof gatesPassed === "boolean") bits.push(gatesPassed ? "gates passed" : "gates not verified");
133
316
  if (input.outcome?.statusAdvanced === true) bits.push("closed");
317
+ if (issue) bits.push(issue);
134
318
  return bits.join("; ");
135
319
  }
136
- return `Task #${input.task.id} ${input.transition}: ${input.task.title}`;
320
+ const needed = taskNotificationNeeded(input);
321
+ return `Task #${input.task.id} ${input.transition}: ${input.task.title}${issue ? ` (${issue})` : ""}${needed ? `\nNeeded: ${needed}` : ""}`;
322
+ }
323
+
324
+ function taskNotificationNeeded(input: TaskSemanticNotificationInput): string {
325
+ if (input.transition === "needs-decision") return "coordinator decision";
326
+ if (input.transition === "blocked") return blockedReason(input.task) ?? "unblock the worker";
327
+ if (input.transition === "gate-failed") return gateFailureReason(input.outcome) ?? "inspect failed gate";
328
+ if (input.transition === "review-requested") return "review requested";
329
+ if (input.transition === "land-conflict") return "land conflict needs resolution";
330
+ return "";
331
+ }
332
+
333
+ function blockedReason(task: Task): string | undefined {
334
+ const blockers = task.blockedBy ?? [];
335
+ if (blockers.length === 0) return undefined;
336
+ return blockers
337
+ .map((blocker) => [blocker.type, blocker.ref, blocker.note].filter(Boolean).join(": "))
338
+ .join("; ");
339
+ }
340
+
341
+ function gateFailureReason(outcome: Record<string, unknown> | undefined): string | undefined {
342
+ const gate = typeof outcome?.gate === "string" ? outcome.gate : undefined;
343
+ const detail = typeof outcome?.detail === "string" ? outcome.detail : undefined;
344
+ if (gate && detail) return `${gate} failed: ${detail}`;
345
+ if (gate) return `${gate} failed`;
346
+ return detail;
347
+ }
348
+
349
+ function primaryIssue(task: Task): StatusPulseDigestItem["issue"] | undefined {
350
+ const issues = (task as Partial<TaskDetail>).issues;
351
+ const issue = Array.isArray(issues) ? issues[0] : undefined;
352
+ if (!issue) return undefined;
353
+ return {
354
+ provider: issue.provider,
355
+ repo: issue.repo,
356
+ externalKey: issue.externalKey,
357
+ url: issue.url,
358
+ ...(issue.title ? { title: issue.title } : {}),
359
+ };
360
+ }
361
+
362
+ function issueLabel(issue: StatusPulseDigestItem["issue"] | undefined): string {
363
+ if (!issue) return "";
364
+ return `${issue.repo}#${issue.externalKey}`;
137
365
  }
@@ -33,6 +33,7 @@ import { getLifecycleManager } from "./lifecycle-manager";
33
33
  import { routeNotification } from "./notification-router";
34
34
  import { renderTemplate } from "./prompt-resolver";
35
35
  import { isAwaitingReviewerApproval } from "./reviewer-pipeline";
36
+ import { emitWorkspaceTaskSemanticNotifications } from "./services/task-lifecycle";
36
37
  import { ensureRepoSteward } from "./steward";
37
38
  import { requestWorkspaceMerge } from "./workspace-merge";
38
39
  import { workspaceActiveClaim } from "./workspace-claim";
@@ -186,6 +187,13 @@ function escalateNoProgressMerge(ws: WorkspaceRecord): void {
186
187
  if (getStewardConfig().enabled) {
187
188
  wakeRepoSteward(ws, `auto-merge backed off after ${MAX_NO_PROGRESS_MERGE_ATTEMPTS} no-progress attempts (${mergeError})`);
188
189
  }
190
+ emitWorkspaceTaskSemanticNotifications({
191
+ workspace: ws,
192
+ transition: "land-conflict",
193
+ actor: "workspace-auto-merge",
194
+ occurrenceKey: `workspace-merge-backoff:${ws.id}:${mergeNoProgressCount(ws.metadata)}:${lastEscalated || "first"}`,
195
+ outcome: { reason: mergeError, attempts: MAX_NO_PROGRESS_MERGE_ATTEMPTS },
196
+ });
189
197
  // Emit the owner-facing `landed-failure` wake — the SAME actionable notice the live
190
198
  // merge_planned→failed settle fires (routes/commands.ts → notifyBranchLandFailed). The
191
199
  // loop-breaker stops dispatching once a lane hits the no-progress cap, so without this
@@ -307,6 +315,15 @@ async function landCandidates(
307
315
  const woke = wakeRepoSteward(ws, p.conflict === true ? "conflict" : "merge state unknown");
308
316
  if (woke) wokeStewards.push(woke);
309
317
  }
318
+ if (p.conflict === true) {
319
+ emitWorkspaceTaskSemanticNotifications({
320
+ workspace: ws,
321
+ transition: "land-conflict",
322
+ actor: "workspace-auto-merge",
323
+ occurrenceKey: `workspace-conflict:${ws.id}:${p.baseRef ?? ws.baseRef ?? "base"}:${p.ahead ?? "?"}:${p.behind ?? "?"}`,
324
+ outcome: { reason: "conflict", ahead: p.ahead, behind: p.behind, baseRef: p.baseRef },
325
+ });
326
+ }
310
327
  continue;
311
328
  }
312
329
 
@@ -1,2 +0,0 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{$n as t,Cn as n,Zn as r,bn as i,fr as a,jt as o}from"./lucide-react-DmbfpRvJ.js";import{i as s,r as c,t as l}from"./store-DDWg67Pg.js";import{_ as u,f as d}from"./index-BIRtwnUI.js";var f=e(a(),1),p=`(max-width: 767px)`;function m(e){if(typeof window>`u`)return()=>{};let t=window.matchMedia(p);return t.addEventListener(`change`,e),()=>t.removeEventListener(`change`,e)}function h(){return(0,f.useSyncExternalStore)(m,()=>typeof window<`u`?window.matchMedia(p).matches:!1,()=>!1)}var g=t(),_=196,v=84,y=52,b=16,x=14,S=[`landed`,`in-progress`,`pending`,`blocked`],C={landed:{label:`landed`,dot:`bg-emerald-400`,accent:`border-l-emerald-400`,text:`text-emerald-300`,wire:`#34d399`},"in-progress":{label:`in progress`,dot:`bg-sky-400`,accent:`border-l-sky-400`,text:`text-sky-300`,wire:`#38bdf8`},pending:{label:`pending`,dot:`bg-amber-400`,accent:`border-l-amber-400`,text:`text-amber-300`,wire:`#fbbf24`},blocked:{label:`blocked`,dot:`bg-red-400`,accent:`border-l-red-400`,text:`text-red-300`,wire:`#f87171`}};function w(e){return e?e.label||(e.policyName?`policy:${e.policyName}`:e.agentId||``):``}function T(e){let t=new Map(e.nodes.map(e=>[e.id,e])),n=new Map(e.nodes.map((e,t)=>[e.id,t])),r=new Map,i=new Map(e.nodes.map(e=>[e.id,0]));for(let n of e.edges)!t.has(n.from)||!t.has(n.to)||(r.set(n.from,[...r.get(n.from)??[],n.to]),i.set(n.to,(i.get(n.to)??0)+1));let a=new Map(e.nodes.map(e=>[e.id,0])),o=e.nodes.filter(e=>(i.get(e.id)??0)===0).map(e=>e.id),s=new Set;for(;o.length;){let e=o.shift();s.add(e);let t=a.get(e)??0;for(let n of r.get(e)??[])a.set(n,Math.max(a.get(n)??0,t+1)),i.set(n,(i.get(n)??0)-1),(i.get(n)??0)===0&&o.push(n)}let c=Math.max(0,...Array.from(a.values()))+1;for(let t of e.nodes)s.has(t.id)||a.set(t.id,c++);let l=new Map;for(let t of e.nodes){let e=a.get(t.id)??0;l.set(e,[...l.get(e)??[],t])}let u=[...l.entries()].sort(([e],[t])=>e-t),d=Math.max(1,...u.map(([,e])=>e.length)),f=v+b,p=[];u.forEach(([e,t],r)=>{let i=[...t].sort((e,t)=>(e.lane||``).localeCompare(t.lane||``)||(n.get(e.id)??0)-(n.get(t.id)??0)),a=(d-i.length)*f/2;i.forEach((t,n)=>{p.push({node:t,layer:e,x:x+r*(_+y),y:x+a+n*f})})});let m=u.length;return{nodes:p,width:Math.max(320,x*2+m*_+Math.max(0,m-1)*y),height:Math.max(v+x*2,x*2+d*v+Math.max(0,d-1)*b)}}function E(e,t){let n=e.x+_,r=e.y+v/2,i=t.x,a=t.y+v/2,o=n+Math.max(20,(i-n)/2);return`M ${n} ${r} C ${o} ${r}, ${o} ${a}, ${i} ${a}`}function D({threadId:e,teamId:t,channel:a}){let p=t?s(t,a):e?c(e,a):``,m=l(e=>t?e.plansByTeam[p]:p?e.plansByThread[p]:null),y=l(e=>e.fetchPlan),b=l(e=>e.planCollapsed),x=l(e=>e.setPlanCollapsed),D=l(e=>e.issueRefsById),O=l(e=>e.deliverableTasks),k=l(e=>e.openTaskDetailById),A=h(),j=b??A;(0,f.useEffect)(()=>{if(t){y({teamId:t,channel:a});return}e&&y({threadId:e,channel:a})},[a,y,t,e]);let M=(0,f.useMemo)(()=>m?T(m):null,[m]),N=(0,f.useMemo)(()=>new Map(M?.nodes.map(e=>[e.node.id,e])??[]),[M]),P=(0,f.useMemo)(()=>{if(!m)return[];let e=new Map;for(let t of m.nodes)e.set(t.status,(e.get(t.status)??0)+1);return S.filter(t=>e.has(t)).map(t=>({status:t,count:e.get(t)??0}))},[m]);if(!m||!M)return null;let F=m.nodes.length,I=P.find(e=>e.status===`landed`)?.count??0,L=`${F} node${F===1?``:`s`}${I>0?` · ${I} landed`:``}`;return(0,g.jsxs)(`section`,{className:`shrink-0 border-b border-border bg-card/45 px-3 py-2`,children:[(0,g.jsxs)(`button`,{type:`button`,onClick:()=>x(!j),"aria-expanded":!j,"aria-label":j?`Expand plan graph`:`Collapse plan graph`,className:r(`flex w-full min-w-0 items-center gap-2 text-left`,!j&&`mb-2`),children:[(0,g.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60`,children:(0,g.jsx)(o,{className:`h-3 w-3 text-muted-foreground`})}),(0,g.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,g.jsx)(`div`,{className:`truncate text-[13px] font-semibold leading-tight text-foreground`,children:m.title||`Plan`}),(0,g.jsx)(`div`,{className:`truncate text-[11px] leading-tight text-muted-foreground`,children:j?L:m.objective||L})]}),P.length>0&&(0,g.jsx)(`div`,{className:`hidden shrink-0 items-center gap-2.5 sm:flex`,children:P.map(({status:e,count:t})=>(0,g.jsxs)(`span`,{className:`flex items-center gap-1 text-[10px] text-muted-foreground`,children:[(0,g.jsx)(`span`,{className:r(`h-1.5 w-1.5 rounded-full`,C[e].dot)}),(0,g.jsx)(`span`,{className:r(`font-medium tabular-nums`,C[e].text),children:t}),(0,g.jsx)(`span`,{children:C[e].label})]},e))}),(0,g.jsxs)(`span`,{className:`shrink-0 rounded bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-muted-foreground`,children:[`v`,m.version]}),(0,g.jsx)(`span`,{className:`flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground`,children:j?(0,g.jsx)(n,{className:`h-3.5 w-3.5`}):(0,g.jsx)(i,{className:`h-3.5 w-3.5`})})]}),j?null:m.nodes.length===0?(0,g.jsx)(`div`,{className:`rounded-md border border-dashed border-border px-3 py-3 text-center text-xs text-muted-foreground`,children:`No nodes yet — the plan graph fills in as the team is composed.`}):(0,g.jsx)(`div`,{className:`max-h-[min(46vh,360px)] overflow-auto`,children:(0,g.jsxs)(`div`,{className:`relative`,style:{width:M.width,height:M.height},children:[(0,g.jsxs)(`svg`,{className:`pointer-events-none absolute inset-0`,width:M.width,height:M.height,"aria-hidden":`true`,children:[(0,g.jsx)(`defs`,{children:S.map(e=>(0,g.jsx)(`marker`,{id:`plan-arrow-${e}`,viewBox:`0 0 8 8`,refX:`6.5`,refY:`4`,markerWidth:`6`,markerHeight:`6`,orient:`auto-start-reverse`,children:(0,g.jsx)(`path`,{d:`M0 0 L8 4 L0 8 z`,fill:C[e].wire})},e))}),m.edges.map(e=>{let t=N.get(e.from),n=N.get(e.to);if(!t||!n)return null;let r=C[t.node.status];return(0,g.jsx)(`path`,{d:E(t,n),stroke:r.wire,strokeOpacity:.7,strokeWidth:2,fill:`none`,strokeLinecap:`round`,markerEnd:`url(#plan-arrow-${t.node.status})`},`${e.from}->${e.to}`)})]}),M.nodes.map(({node:e,x:t,y:n})=>{let i=C[e.status],a=w(e.ownerRef),o=e.refs?.issueId?D[e.refs.issueId]:null,s=e.refs?.taskId?parseInt(e.refs.taskId,10):null,c=s==null?void 0:O.find(e=>e.id===s)?.title;return(0,g.jsxs)(`article`,{className:r(`group absolute flex flex-col justify-between rounded-md border border-l-[3px] bg-background/95 p-2`,`shadow-sm transition-all hover:-translate-y-px hover:border-border hover:shadow-md hover:bg-background`,i.accent),style:{left:t,top:n,width:_,height:v},title:e.scopeBlurb||e.label,children:[(0,g.jsx)(`div`,{className:`truncate text-xs font-semibold leading-tight text-foreground`,children:e.label}),(e.scopeBlurb||e.id)&&(0,g.jsx)(`div`,{className:`truncate text-[10px] leading-tight text-muted-foreground`,children:e.scopeBlurb||e.id}),(o||s!=null)&&(0,g.jsxs)(`div`,{className:`flex min-w-0 flex-wrap gap-1`,children:[o&&(0,g.jsx)(u,{issueRef:o,compact:!0,className:`max-w-full`}),s!=null&&(0,g.jsx)(d,{id:s,title:c,onClick:()=>k(s),className:`max-w-full`})]}),(0,g.jsxs)(`div`,{className:`flex min-w-0 items-center justify-between gap-1.5`,children:[(0,g.jsxs)(`div`,{className:`flex shrink-0 items-center gap-1.5`,children:[(0,g.jsx)(`span`,{className:r(`h-1.5 w-1.5 shrink-0 rounded-full`,i.dot)}),(0,g.jsx)(`span`,{className:r(`text-[10px] font-medium`,i.text),children:i.label})]}),(0,g.jsxs)(`div`,{className:`flex min-w-0 items-center gap-1.5`,children:[a&&(0,g.jsx)(`span`,{className:`truncate text-[10px] text-muted-foreground`,children:a}),e.lane&&(0,g.jsx)(`span`,{className:`shrink-0 rounded-sm bg-muted/70 px-1 py-px text-[9px] font-medium uppercase tracking-wide text-muted-foreground`,children:e.lane})]})]})]},e.id)})]})})]})}function O({agentId:e}){let t=l(e=>e.deliverableTasks),n=l(e=>e.openTaskDetailById),r=t.filter(t=>t.createdBy===e);return r.length===0?null:(0,g.jsxs)(`section`,{className:`shrink-0 border-b border-border bg-card/45 px-3 py-2`,children:[(0,g.jsxs)(`div`,{className:`mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground`,children:[`Tasks from this session (`,r.length,`)`]}),(0,g.jsx)(`div`,{className:`flex flex-wrap gap-1.5`,children:r.map(e=>(0,g.jsx)(d,{id:e.id,title:e.title,status:e.status,onClick:()=>n(e.id)},e.id))})]})}export{O as n,D as t};
2
- //# sourceMappingURL=PlanGraphPanel-K6aFJmdd.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PlanGraphPanel-K6aFJmdd.js","names":[],"sources":["../../dashboard/src/components/chat/hooks/useIsMobile.ts","../../dashboard/src/components/chat/PlanGraphPanel.tsx"],"sourcesContent":["import { useSyncExternalStore } from 'react'\n\n// Reactive narrow-viewport flag. Subscribes to the same `max-width: 767px`\n// breakpoint the chat single-pane layout uses, via useSyncExternalStore — an\n// external-store subscription, not effect-driven derived state — so it stays\n// correct across resize/rotate without a useEffect.\nconst QUERY = '(max-width: 767px)'\n\nfunction subscribe(cb: () => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const mql = window.matchMedia(QUERY)\n mql.addEventListener('change', cb)\n return () => mql.removeEventListener('change', cb)\n}\n\nexport function useIsMobile(): boolean {\n return useSyncExternalStore(\n subscribe,\n () => (typeof window !== 'undefined' ? window.matchMedia(QUERY).matches : false),\n () => false,\n )\n}\n","import { useEffect, useMemo } from 'react'\nimport { ChevronDown, ChevronUp, GitBranch } from 'lucide-react'\nimport { useRelayStore } from '@/store'\nimport type { Plan, PlanNode, PlanOwnerRef, PlanStatus } from '@/types'\nimport { cn } from '@/lib/utils'\nimport { planLookupKey, planTeamLookupKey } from '@/store/slices/plans'\nimport { IssueRefChip } from '@/components/shared/issue-ref-chip'\nimport { TaskChip } from '@/components/shared/task-chip'\nimport { useIsMobile } from './hooks/useIsMobile'\n\n// Left-to-right DAG: dependency depth flows along x (sequential gates), nodes\n// that share a depth stack along y (parallel lanes). This keeps the panel SHORT\n// — it's an operator overlay above the chat, not a full view — and lets the\n// gate-vs-lane structure read at a glance. Height is capped + scrollable below.\nconst NODE_W = 196\nconst NODE_H = 84\nconst COL_GAP = 52\nconst ROW_GAP = 16\nconst PAD = 14\n\nconst STATUS_ORDER: PlanStatus[] = ['landed', 'in-progress', 'pending', 'blocked']\n\nconst STATUS_STYLE: Record<\n PlanStatus,\n { label: string; dot: string; accent: string; text: string; wire: string }\n> = {\n landed: { label: 'landed', dot: 'bg-emerald-400', accent: 'border-l-emerald-400', text: 'text-emerald-300', wire: '#34d399' },\n 'in-progress': { label: 'in progress', dot: 'bg-sky-400', accent: 'border-l-sky-400', text: 'text-sky-300', wire: '#38bdf8' },\n pending: { label: 'pending', dot: 'bg-amber-400', accent: 'border-l-amber-400', text: 'text-amber-300', wire: '#fbbf24' },\n blocked: { label: 'blocked', dot: 'bg-red-400', accent: 'border-l-red-400', text: 'text-red-300', wire: '#f87171' },\n}\n\ninterface LayoutNode {\n node: PlanNode\n x: number\n y: number\n layer: number\n}\n\nfunction ownerLabel(owner?: PlanOwnerRef): string {\n if (!owner) return ''\n return owner.label || (owner.policyName ? `policy:${owner.policyName}` : owner.agentId || '')\n}\n\nfunction layerPlan(plan: Plan): { nodes: LayoutNode[]; width: number; height: number } {\n const nodeById = new Map(plan.nodes.map((node) => [node.id, node]))\n const indexById = new Map(plan.nodes.map((node, index) => [node.id, index]))\n const outgoing = new Map<string, string[]>()\n const indegree = new Map(plan.nodes.map((node) => [node.id, 0]))\n for (const edge of plan.edges) {\n if (!nodeById.has(edge.from) || !nodeById.has(edge.to)) continue\n outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to])\n indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1)\n }\n\n const layerById = new Map(plan.nodes.map((node) => [node.id, 0]))\n const queue = plan.nodes\n .filter((node) => (indegree.get(node.id) ?? 0) === 0)\n .map((node) => node.id)\n const visited = new Set<string>()\n\n while (queue.length) {\n const id = queue.shift()!\n visited.add(id)\n const fromLayer = layerById.get(id) ?? 0\n for (const to of outgoing.get(id) ?? []) {\n layerById.set(to, Math.max(layerById.get(to) ?? 0, fromLayer + 1))\n indegree.set(to, (indegree.get(to) ?? 0) - 1)\n if ((indegree.get(to) ?? 0) === 0) queue.push(to)\n }\n }\n\n let fallbackLayer = Math.max(0, ...Array.from(layerById.values())) + 1\n for (const node of plan.nodes) {\n if (visited.has(node.id)) continue\n layerById.set(node.id, fallbackLayer++)\n }\n\n const layers = new Map<number, PlanNode[]>()\n for (const node of plan.nodes) {\n const layer = layerById.get(node.id) ?? 0\n layers.set(layer, [...(layers.get(layer) ?? []), node])\n }\n\n const orderedLayers = [...layers.entries()].sort(([a], [b]) => a - b)\n const maxRows = Math.max(1, ...orderedLayers.map(([, nodes]) => nodes.length))\n const rowPitch = NODE_H + ROW_GAP\n\n const layoutNodes: LayoutNode[] = []\n orderedLayers.forEach(([layer, nodes], col) => {\n const ordered = [...nodes].sort((a, b) => {\n const lane = (a.lane || '').localeCompare(b.lane || '')\n return lane || ((indexById.get(a.id) ?? 0) - (indexById.get(b.id) ?? 0))\n })\n // Center each column vertically against the tallest column so the spine\n // runs through the middle and the parallel fan reads as a fan.\n const offset = ((maxRows - ordered.length) * rowPitch) / 2\n ordered.forEach((node, row) => {\n layoutNodes.push({\n node,\n layer,\n x: PAD + col * (NODE_W + COL_GAP),\n y: PAD + offset + row * rowPitch,\n })\n })\n })\n\n const cols = orderedLayers.length\n return {\n nodes: layoutNodes,\n width: Math.max(320, PAD * 2 + cols * NODE_W + Math.max(0, cols - 1) * COL_GAP),\n height: Math.max(NODE_H + PAD * 2, PAD * 2 + maxRows * NODE_H + Math.max(0, maxRows - 1) * ROW_GAP),\n }\n}\n\nfunction edgePath(from: LayoutNode, to: LayoutNode): string {\n const x1 = from.x + NODE_W\n const y1 = from.y + NODE_H / 2\n const x2 = to.x\n const y2 = to.y + NODE_H / 2\n const midX = x1 + Math.max(20, (x2 - x1) / 2)\n return `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`\n}\n\nexport function PlanGraphPanel({ threadId, teamId, channel }: { threadId?: string | number | null; teamId?: string | null; channel?: string }) {\n const key = teamId ? planTeamLookupKey(teamId, channel) : threadId ? planLookupKey(threadId, channel) : ''\n const plan = useRelayStore((s) => teamId ? s.plansByTeam[key] : key ? s.plansByThread[key] : null)\n const fetchPlan = useRelayStore((s) => s.fetchPlan)\n const planCollapsed = useRelayStore((s) => s.planCollapsed)\n const setPlanCollapsed = useRelayStore((s) => s.setPlanCollapsed)\n const issueRefsById = useRelayStore((s) => s.issueRefsById)\n const deliverableTasks = useRelayStore((s) => s.deliverableTasks)\n const openTaskDetailById = useRelayStore((s) => s.openTaskDetailById)\n const isMobile = useIsMobile()\n // null = no manual toggle yet → follow the viewport default. The graph\n // dominates a phone screen, so it starts collapsed there and expanded on\n // desktop; an explicit user toggle (persisted) overrides the default.\n const collapsed = planCollapsed ?? isMobile\n\n useEffect(() => {\n if (teamId) {\n void fetchPlan({ teamId, channel })\n return\n }\n if (threadId) void fetchPlan({ threadId, channel })\n }, [channel, fetchPlan, teamId, threadId])\n\n const layout = useMemo(() => plan ? layerPlan(plan) : null, [plan])\n const nodeById = useMemo(() => new Map(layout?.nodes.map((node) => [node.node.id, node]) ?? []), [layout])\n\n const legend = useMemo(() => {\n if (!plan) return []\n const counts = new Map<PlanStatus, number>()\n for (const node of plan.nodes) counts.set(node.status, (counts.get(node.status) ?? 0) + 1)\n return STATUS_ORDER.filter((status) => counts.has(status)).map((status) => ({\n status,\n count: counts.get(status) ?? 0,\n }))\n }, [plan])\n\n if (!plan || !layout) return null\n\n const nodeCount = plan.nodes.length\n const landedCount = legend.find((entry) => entry.status === 'landed')?.count ?? 0\n // Thin summary for the collapsed bar (the legend dots are hidden on mobile),\n // e.g. \"5 nodes · 4 landed\".\n const summary = `${nodeCount} node${nodeCount === 1 ? '' : 's'}${landedCount > 0 ? ` · ${landedCount} landed` : ''}`\n\n return (\n <section className=\"shrink-0 border-b border-border bg-card/45 px-3 py-2\">\n <button\n type=\"button\"\n onClick={() => setPlanCollapsed(!collapsed)}\n aria-expanded={!collapsed}\n aria-label={collapsed ? 'Expand plan graph' : 'Collapse plan graph'}\n className={cn(\n 'flex w-full min-w-0 items-center gap-2 text-left',\n !collapsed && 'mb-2',\n )}\n >\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted/60\">\n <GitBranch className=\"h-3 w-3 text-muted-foreground\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-[13px] font-semibold leading-tight text-foreground\">{plan.title || 'Plan'}</div>\n <div className=\"truncate text-[11px] leading-tight text-muted-foreground\">\n {collapsed ? summary : (plan.objective || summary)}\n </div>\n </div>\n {legend.length > 0 && (\n <div className=\"hidden shrink-0 items-center gap-2.5 sm:flex\">\n {legend.map(({ status, count }) => (\n <span key={status} className=\"flex items-center gap-1 text-[10px] text-muted-foreground\">\n <span className={cn('h-1.5 w-1.5 rounded-full', STATUS_STYLE[status].dot)} />\n <span className={cn('font-medium tabular-nums', STATUS_STYLE[status].text)}>{count}</span>\n <span>{STATUS_STYLE[status].label}</span>\n </span>\n ))}\n </div>\n )}\n <span className=\"shrink-0 rounded bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-muted-foreground\">\n v{plan.version}\n </span>\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted/60 hover:text-foreground\">\n {collapsed ? <ChevronDown className=\"h-3.5 w-3.5\" /> : <ChevronUp className=\"h-3.5 w-3.5\" />}\n </span>\n </button>\n {collapsed ? null : plan.nodes.length === 0 ? (\n <div className=\"rounded-md border border-dashed border-border px-3 py-3 text-center text-xs text-muted-foreground\">\n No nodes yet — the plan graph fills in as the team is composed.\n </div>\n ) : (\n <div className=\"max-h-[min(46vh,360px)] overflow-auto\">\n <div className=\"relative\" style={{ width: layout.width, height: layout.height }}>\n <svg className=\"pointer-events-none absolute inset-0\" width={layout.width} height={layout.height} aria-hidden=\"true\">\n <defs>\n {STATUS_ORDER.map((status) => (\n <marker\n key={status}\n id={`plan-arrow-${status}`}\n viewBox=\"0 0 8 8\"\n refX=\"6.5\"\n refY=\"4\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n >\n <path d=\"M0 0 L8 4 L0 8 z\" fill={STATUS_STYLE[status].wire} />\n </marker>\n ))}\n </defs>\n {plan.edges.map((edge) => {\n const from = nodeById.get(edge.from)\n const to = nodeById.get(edge.to)\n if (!from || !to) return null\n const wire = STATUS_STYLE[from.node.status]\n return (\n <path\n key={`${edge.from}->${edge.to}`}\n d={edgePath(from, to)}\n stroke={wire.wire}\n strokeOpacity={0.7}\n strokeWidth={2}\n fill=\"none\"\n strokeLinecap=\"round\"\n markerEnd={`url(#plan-arrow-${from.node.status})`}\n />\n )\n })}\n </svg>\n {layout.nodes.map(({ node, x, y }) => {\n const status = STATUS_STYLE[node.status]\n const owner = ownerLabel(node.ownerRef)\n const issueRef = node.refs?.issueId ? issueRefsById[node.refs.issueId] : null\n const taskNumId = node.refs?.taskId ? parseInt(node.refs.taskId, 10) : null\n const taskTitle = taskNumId != null ? deliverableTasks.find((t) => t.id === taskNumId)?.title : undefined\n return (\n <article\n key={node.id}\n className={cn(\n 'group absolute flex flex-col justify-between rounded-md border border-l-[3px] bg-background/95 p-2',\n 'shadow-sm transition-all hover:-translate-y-px hover:border-border hover:shadow-md hover:bg-background',\n status.accent,\n )}\n style={{ left: x, top: y, width: NODE_W, height: NODE_H }}\n title={node.scopeBlurb || node.label}\n >\n <div className=\"truncate text-xs font-semibold leading-tight text-foreground\">{node.label}</div>\n {(node.scopeBlurb || node.id) && (\n <div className=\"truncate text-[10px] leading-tight text-muted-foreground\">{node.scopeBlurb || node.id}</div>\n )}\n {(issueRef || taskNumId != null) && (\n <div className=\"flex min-w-0 flex-wrap gap-1\">\n {issueRef && <IssueRefChip issueRef={issueRef} compact className=\"max-w-full\" />}\n {taskNumId != null && (\n <TaskChip id={taskNumId} title={taskTitle} onClick={() => openTaskDetailById(taskNumId)} className=\"max-w-full\" />\n )}\n </div>\n )}\n <div className=\"flex min-w-0 items-center justify-between gap-1.5\">\n <div className=\"flex shrink-0 items-center gap-1.5\">\n <span className={cn('h-1.5 w-1.5 shrink-0 rounded-full', status.dot)} />\n <span className={cn('text-[10px] font-medium', status.text)}>{status.label}</span>\n </div>\n <div className=\"flex min-w-0 items-center gap-1.5\">\n {owner && <span className=\"truncate text-[10px] text-muted-foreground\">{owner}</span>}\n {node.lane && (\n <span className=\"shrink-0 rounded-sm bg-muted/70 px-1 py-px text-[9px] font-medium uppercase tracking-wide text-muted-foreground\">\n {node.lane}\n </span>\n )}\n </div>\n </div>\n </article>\n )\n })}\n </div>\n </div>\n )}\n </section>\n )\n}\n\n// Tasks created/triggered from a session (#856). Client-side filter on createdBy until\n// the backend exposes a dedicated /api/tasks?createdBy= filter (backend follow-up needed).\nexport function SessionTasksPanel({ agentId }: { agentId: string }) {\n const deliverableTasks = useRelayStore((s) => s.deliverableTasks)\n const openTaskDetailById = useRelayStore((s) => s.openTaskDetailById)\n const sessionTasks = deliverableTasks.filter((t) => t.createdBy === agentId)\n if (sessionTasks.length === 0) return null\n return (\n <section className=\"shrink-0 border-b border-border bg-card/45 px-3 py-2\">\n <div className=\"mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground\">\n Tasks from this session ({sessionTasks.length})\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {sessionTasks.map((task) => (\n <TaskChip\n key={task.id}\n id={task.id}\n title={task.title}\n status={task.status}\n onClick={() => openTaskDetailById(task.id)}\n />\n ))}\n </div>\n </section>\n )\n}\n"],"mappings":"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"}