@flowdesk/opencode-plugin 0.1.12 → 0.1.14

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/README.md +1 -1
  2. package/dist/agent-task-output.d.ts +17 -0
  3. package/dist/agent-task-output.d.ts.map +1 -0
  4. package/dist/agent-task-output.js +119 -0
  5. package/dist/agent-task-output.js.map +1 -0
  6. package/dist/agent-task-runner.d.ts +24 -0
  7. package/dist/agent-task-runner.d.ts.map +1 -1
  8. package/dist/agent-task-runner.js +536 -61
  9. package/dist/agent-task-runner.js.map +1 -1
  10. package/dist/auto-continue-preview-tool.d.ts +36 -0
  11. package/dist/auto-continue-preview-tool.d.ts.map +1 -0
  12. package/dist/auto-continue-preview-tool.js +119 -0
  13. package/dist/auto-continue-preview-tool.js.map +1 -0
  14. package/dist/completion-ui-cache.d.ts +6 -0
  15. package/dist/completion-ui-cache.d.ts.map +1 -0
  16. package/dist/completion-ui-cache.js +260 -0
  17. package/dist/completion-ui-cache.js.map +1 -0
  18. package/dist/controlled-write-tool.d.ts +49 -0
  19. package/dist/controlled-write-tool.d.ts.map +1 -0
  20. package/dist/controlled-write-tool.js +296 -0
  21. package/dist/controlled-write-tool.js.map +1 -0
  22. package/dist/event-hook-observer.d.ts +14 -0
  23. package/dist/event-hook-observer.d.ts.map +1 -0
  24. package/dist/event-hook-observer.js +193 -0
  25. package/dist/event-hook-observer.js.map +1 -0
  26. package/dist/managed-dispatch-adapter.d.ts +1 -0
  27. package/dist/managed-dispatch-adapter.d.ts.map +1 -1
  28. package/dist/managed-dispatch-adapter.js +100 -29
  29. package/dist/managed-dispatch-adapter.js.map +1 -1
  30. package/dist/model-selection-engine.d.ts +47 -0
  31. package/dist/model-selection-engine.d.ts.map +1 -0
  32. package/dist/model-selection-engine.js +175 -0
  33. package/dist/model-selection-engine.js.map +1 -0
  34. package/dist/provider-usage-live-tool.d.ts +27 -0
  35. package/dist/provider-usage-live-tool.d.ts.map +1 -1
  36. package/dist/provider-usage-live-tool.js +443 -4
  37. package/dist/provider-usage-live-tool.js.map +1 -1
  38. package/dist/quick-reviewer-run.d.ts +3 -0
  39. package/dist/quick-reviewer-run.d.ts.map +1 -1
  40. package/dist/quick-reviewer-run.js +20 -8
  41. package/dist/quick-reviewer-run.js.map +1 -1
  42. package/dist/runtime-reviewer-execution-bridge.d.ts +21 -0
  43. package/dist/runtime-reviewer-execution-bridge.d.ts.map +1 -1
  44. package/dist/runtime-reviewer-execution-bridge.js +238 -0
  45. package/dist/runtime-reviewer-execution-bridge.js.map +1 -1
  46. package/dist/server.d.ts +60 -1
  47. package/dist/server.d.ts.map +1 -1
  48. package/dist/server.js +800 -41
  49. package/dist/server.js.map +1 -1
  50. package/dist/stall-recovery.d.ts +60 -0
  51. package/dist/stall-recovery.d.ts.map +1 -1
  52. package/dist/stall-recovery.js +763 -11
  53. package/dist/stall-recovery.js.map +1 -1
  54. package/dist/status-live-tool.d.ts +81 -0
  55. package/dist/status-live-tool.d.ts.map +1 -1
  56. package/dist/status-live-tool.js +620 -38
  57. package/dist/status-live-tool.js.map +1 -1
  58. package/dist/tui-subtask-activity.d.ts +69 -0
  59. package/dist/tui-subtask-activity.d.ts.map +1 -0
  60. package/dist/tui-subtask-activity.js +266 -0
  61. package/dist/tui-subtask-activity.js.map +1 -0
  62. package/dist/tui-usage-snapshot.d.ts +44 -0
  63. package/dist/tui-usage-snapshot.d.ts.map +1 -0
  64. package/dist/tui-usage-snapshot.js +397 -0
  65. package/dist/tui-usage-snapshot.js.map +1 -0
  66. package/dist/tui.d.ts +7 -0
  67. package/dist/tui.d.ts.map +1 -0
  68. package/dist/tui.js +134 -0
  69. package/dist/tui.js.map +1 -0
  70. package/dist/workflow-assign-tool.d.ts +23 -0
  71. package/dist/workflow-assign-tool.d.ts.map +1 -0
  72. package/dist/workflow-assign-tool.js +117 -0
  73. package/dist/workflow-assign-tool.js.map +1 -0
  74. package/dist/workflow-author-tool.d.ts +29 -0
  75. package/dist/workflow-author-tool.d.ts.map +1 -0
  76. package/dist/workflow-author-tool.js +227 -0
  77. package/dist/workflow-author-tool.js.map +1 -0
  78. package/dist/workflow-dispatch-plan-tool.d.ts +47 -0
  79. package/dist/workflow-dispatch-plan-tool.d.ts.map +1 -0
  80. package/dist/workflow-dispatch-plan-tool.js +251 -0
  81. package/dist/workflow-dispatch-plan-tool.js.map +1 -0
  82. package/dist/workflow-dispatch-tool.d.ts +56 -0
  83. package/dist/workflow-dispatch-tool.d.ts.map +1 -0
  84. package/dist/workflow-dispatch-tool.js +306 -0
  85. package/dist/workflow-dispatch-tool.js.map +1 -0
  86. package/dist/workflow-orchestrator.d.ts +31 -0
  87. package/dist/workflow-orchestrator.d.ts.map +1 -0
  88. package/dist/workflow-orchestrator.js +160 -0
  89. package/dist/workflow-orchestrator.js.map +1 -0
  90. package/dist/workflow-scheduler.d.ts +19 -0
  91. package/dist/workflow-scheduler.d.ts.map +1 -0
  92. package/dist/workflow-scheduler.js +45 -0
  93. package/dist/workflow-scheduler.js.map +1 -0
  94. package/dist/workflow-synthesis-tool.d.ts +31 -0
  95. package/dist/workflow-synthesis-tool.d.ts.map +1 -0
  96. package/dist/workflow-synthesis-tool.js +194 -0
  97. package/dist/workflow-synthesis-tool.js.map +1 -0
  98. package/package.json +10 -2
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { FLOWDESK_SESSION_EVIDENCE_CLASSES, projectFlowDeskLaneStallV1, reloadFlowDeskSessionEvidenceV1, } from "@flowdesk/core";
3
+ import { applyFlowDeskSessionEvidenceWriteIntentsV1, FLOWDESK_SESSION_EVIDENCE_CLASSES, prepareFlowDeskSessionEvidenceWriteIntentV1, projectFlowDeskLaneStallV1, reloadFlowDeskSessionEvidenceV1, } from "@flowdesk/core";
4
+ import { backfillTerminalAgentTaskFailedLanesV1 } from "./stall-recovery.js";
4
5
  const FLOWDESK_LANE_STALL_TERMINAL_STATES = new Set([
5
6
  "complete",
6
7
  "incomplete",
@@ -89,6 +90,27 @@ function pruneNonTerminalLifecycleSnapshotsNoLongerValid(reload) {
89
90
  return reload;
90
91
  return { ...reload, entries };
91
92
  }
93
+ function pruneInconsistencySnapshotsNoLongerValid(reload) {
94
+ const terminalLaneIds = new Set();
95
+ for (const entry of reload.entries) {
96
+ if (entry.evidenceClass === "task_result" || entry.evidenceClass === "task_failed") {
97
+ const laneId = getLaneLifecycleRecordField(entry.record, "lane_id");
98
+ if (laneId !== undefined)
99
+ terminalLaneIds.add(laneId);
100
+ }
101
+ }
102
+ if (terminalLaneIds.size === 0)
103
+ return reload;
104
+ const entries = reload.entries.filter((entry) => {
105
+ if (entry.evidenceClass !== "agent_task_inconsistency")
106
+ return true;
107
+ const laneId = getLaneLifecycleRecordField(entry.record, "lane_id");
108
+ return laneId === undefined || !terminalLaneIds.has(laneId);
109
+ });
110
+ if (entries.length === reload.entries.length)
111
+ return reload;
112
+ return { ...reload, entries };
113
+ }
92
114
  const FLOWDESK_SESSION_RECORD_ROOT = ".flowdesk/sessions";
93
115
  function blockedAuthority() {
94
116
  return {
@@ -105,6 +127,14 @@ function blockedAuthority() {
105
127
  function safeNextActions() {
106
128
  return ["/flowdesk-status", "/flowdesk-doctor", "/flowdesk-export-debug"];
107
129
  }
130
+ function authorityCapabilitySummary() {
131
+ return {
132
+ availableNow: ["display_only", "local_preview", "command_backed_guarded"],
133
+ explicitDevBeta: ["provider_task_lane", "controlled_workspace_write"],
134
+ laterGated: ["managed_dispatch", "managed_fallback", "tui_actions", "hard_chat_control"],
135
+ unsupportedByDefault: ["auto_provider_execution", "automatic_reselection"],
136
+ };
137
+ }
108
138
  function listSessionWorkflowIds(rootDir, max) {
109
139
  const sessionsDir = join(rootDir, FLOWDESK_SESSION_RECORD_ROOT);
110
140
  if (!existsSync(sessionsDir))
@@ -137,6 +167,165 @@ function getStringField(record, key) {
137
167
  const value = record[key];
138
168
  return typeof value === "string" ? value : undefined;
139
169
  }
170
+ function getPositiveIntegerField(record, key) {
171
+ const value = record[key];
172
+ return typeof value === "number" && Number.isInteger(value) && value > 0
173
+ ? value
174
+ : undefined;
175
+ }
176
+ const FORBIDDEN_SYNTHESIS_SUMMARY_MARKERS = /system prompt|provider payload|raw token|hidden injection|opencode\srun|dispatch|fallback|reselect/i;
177
+ function synthesisSummaryPreview(value) {
178
+ if (value === undefined)
179
+ return undefined;
180
+ const compact = value.replace(/\s+/g, " ").trim();
181
+ if (compact.length === 0 || FORBIDDEN_SYNTHESIS_SUMMARY_MARKERS.test(compact))
182
+ return undefined;
183
+ return compact.length > 160 ? `${compact.slice(0, 159)}…` : compact;
184
+ }
185
+ const DEFAULT_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS = 90_000;
186
+ const MIN_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS = 30_000;
187
+ const MAX_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS = 600_000;
188
+ function clampFinalizingInconsistencyGraceMs(value) {
189
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
190
+ return DEFAULT_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS;
191
+ return Math.min(MAX_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS, Math.max(MIN_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS, Math.floor(value)));
192
+ }
193
+ function finalizingInconsistencyEvidenceId(laneId) {
194
+ return `agent-task-inconsistency-${laneId}-finalizing-without-terminal`;
195
+ }
196
+ function materializeFinalizingWithoutTerminalInconsistencies(input) {
197
+ const observedAtMs = Date.parse(input.observedAt);
198
+ if (!Number.isFinite(observedAtMs))
199
+ return false;
200
+ const terminalLaneIds = new Set();
201
+ const existingInconsistencyIds = new Set();
202
+ const attemptIdByLane = new Map();
203
+ const taskIdByLane = new Map();
204
+ const latestActiveSignalByLane = new Map();
205
+ const latestFinalizingByLane = new Map();
206
+ for (const entry of input.reload.entries) {
207
+ const laneId = getStringField(entry.record, "lane_id");
208
+ if (laneId !== undefined) {
209
+ if (entry.evidenceClass === "lane_lifecycle" || entry.evidenceClass === "lane_heartbeat") {
210
+ const attemptId = getStringField(entry.record, "attempt_id");
211
+ if (attemptId !== undefined)
212
+ attemptIdByLane.set(laneId, attemptId);
213
+ const state = getStringField(entry.record, "state");
214
+ const observedAt = getStringField(entry.record, "updated_at") ??
215
+ getStringField(entry.record, "observed_at") ??
216
+ getStringField(entry.record, "created_at");
217
+ const observedAtMs = observedAt === undefined ? NaN : Date.parse(observedAt);
218
+ if ((state === "created" ||
219
+ state === "running" ||
220
+ state === "awaiting_dependency" ||
221
+ state === "cooldown") &&
222
+ Number.isFinite(observedAtMs)) {
223
+ const current = latestActiveSignalByLane.get(laneId);
224
+ if (current === undefined || observedAtMs > current)
225
+ latestActiveSignalByLane.set(laneId, observedAtMs);
226
+ }
227
+ }
228
+ if (entry.evidenceClass === "agent_task_context") {
229
+ const taskId = getStringField(entry.record, "task_id");
230
+ if (taskId !== undefined)
231
+ taskIdByLane.set(laneId, taskId);
232
+ }
233
+ }
234
+ if (entry.evidenceClass === "task_result" || entry.evidenceClass === "task_failed") {
235
+ if (laneId !== undefined)
236
+ terminalLaneIds.add(laneId);
237
+ continue;
238
+ }
239
+ if (entry.evidenceClass === "agent_task_inconsistency") {
240
+ existingInconsistencyIds.add(entry.evidenceId);
241
+ continue;
242
+ }
243
+ if (entry.evidenceClass !== "agent_task_progress")
244
+ continue;
245
+ if (laneId === undefined)
246
+ continue;
247
+ if (getStringField(entry.record, "phase") !== "finalizing")
248
+ continue;
249
+ const progressObservedAt = getStringField(entry.record, "observed_at");
250
+ if (progressObservedAt === undefined)
251
+ continue;
252
+ const progressObservedAtMs = Date.parse(progressObservedAt);
253
+ if (!Number.isFinite(progressObservedAtMs))
254
+ continue;
255
+ const current = latestFinalizingByLane.get(laneId);
256
+ if (current !== undefined && current.observedAtMs > progressObservedAtMs)
257
+ continue;
258
+ latestFinalizingByLane.set(laneId, {
259
+ laneId,
260
+ attemptId: attemptIdByLane.get(laneId),
261
+ taskId: getStringField(entry.record, "task_id"),
262
+ progressSeq: getPositiveIntegerField(entry.record, "progress_seq"),
263
+ observedAt: progressObservedAt,
264
+ observedAtMs: progressObservedAtMs,
265
+ });
266
+ }
267
+ const intents = [];
268
+ for (const progress of latestFinalizingByLane.values()) {
269
+ if (terminalLaneIds.has(progress.laneId))
270
+ continue;
271
+ const activeSignalMs = latestActiveSignalByLane.get(progress.laneId);
272
+ const finalizingAgeMs = observedAtMs - progress.observedAtMs;
273
+ const activeSignalAgeMs = activeSignalMs === undefined ? 0 : observedAtMs - activeSignalMs;
274
+ if (finalizingAgeMs < input.graceMs && activeSignalAgeMs < input.graceMs)
275
+ continue;
276
+ const attemptId = progress.attemptId ?? attemptIdByLane.get(progress.laneId);
277
+ const taskId = progress.taskId ?? taskIdByLane.get(progress.laneId);
278
+ const progressSeq = progress.progressSeq;
279
+ if (attemptId === undefined || taskId === undefined || progressSeq === undefined)
280
+ continue;
281
+ const evidenceId = finalizingInconsistencyEvidenceId(progress.laneId);
282
+ if (existingInconsistencyIds.has(evidenceId))
283
+ continue;
284
+ const record = {
285
+ schema_version: "flowdesk.agent_task_inconsistency.v1",
286
+ workflow_id: input.workflowId,
287
+ attempt_id: attemptId,
288
+ lane_id: progress.laneId,
289
+ task_id: taskId,
290
+ last_progress_seq: progressSeq,
291
+ last_progress_observed_at: progress.observedAt,
292
+ inconsistency_kind: "finalizing_without_terminal",
293
+ grace_window_ms: input.graceMs,
294
+ grace_source_label: input.graceMs === DEFAULT_AGENT_TASK_FINALIZING_INCONSISTENCY_GRACE_MS
295
+ ? "default_status_live_finalizing_inconsistency_grace"
296
+ : "configured_status_live_finalizing_inconsistency_grace",
297
+ observed_at: input.observedAt,
298
+ safe_next_actions: [
299
+ "/flowdesk-status",
300
+ "/flowdesk-abort",
301
+ "/flowdesk-retry",
302
+ "/flowdesk-doctor",
303
+ "/flowdesk-export-debug",
304
+ ],
305
+ redaction_version: "v1",
306
+ dispatch_authority_enabled: false,
307
+ };
308
+ const prepared = prepareFlowDeskSessionEvidenceWriteIntentV1({
309
+ workflowId: input.workflowId,
310
+ evidenceId,
311
+ record,
312
+ });
313
+ if (prepared.ok && prepared.writeIntent !== undefined)
314
+ intents.push(prepared.writeIntent);
315
+ }
316
+ if (intents.length === 0)
317
+ return false;
318
+ const applied = applyFlowDeskSessionEvidenceWriteIntentsV1(input.rootDir, intents);
319
+ return applied.ok && applied.writtenPaths.length > 0;
320
+ }
321
+ function compactPromptPreview(value, max = 96) {
322
+ if (value === undefined)
323
+ return undefined;
324
+ const compact = value.replace(/\s+/g, " ").trim();
325
+ if (compact.length === 0)
326
+ return undefined;
327
+ return compact.length > max ? `${compact.slice(0, Math.max(0, max - 1))}…` : compact;
328
+ }
140
329
  function summarizeWorkflow(workflowId, reload, maxRecent) {
141
330
  const counts = {};
142
331
  const recent = {};
@@ -144,6 +333,19 @@ function summarizeWorkflow(workflowId, reload, maxRecent) {
144
333
  const laneLifecycleStates = [];
145
334
  let regatePlanState;
146
335
  let providerAcquisitionStatus;
336
+ let workflowAuthoringStatus;
337
+ let workflowAuthoringGoalSummary;
338
+ let taskGraphTaskCount;
339
+ let taskGraphEdgeCount;
340
+ let taskAgentAssignmentCount;
341
+ let taskModelSelectionStatus;
342
+ let taskModelSelectionBlockedLabels;
343
+ let workflowSynthesisId;
344
+ let workflowSynthesisTasksSummarized;
345
+ let workflowSynthesisConflictDetected;
346
+ let workflowSynthesisSummaryPreview;
347
+ let workflowDispatchPlanRevisionId;
348
+ let workflowDispatchPlanTaskCount;
147
349
  for (const evidenceClass of FLOWDESK_SESSION_EVIDENCE_CLASSES) {
148
350
  const classEntries = reload.entries.filter((entry) => entry.evidenceClass === evidenceClass);
149
351
  if (classEntries.length === 0)
@@ -187,6 +389,57 @@ function summarizeWorkflow(workflowId, reload, maxRecent) {
187
389
  providerAcquisitionStatus = acquisitionStatus;
188
390
  }
189
391
  }
392
+ if (evidenceClass === "workflow_authoring_result") {
393
+ const last = classEntries[classEntries.length - 1];
394
+ if (last !== undefined) {
395
+ workflowAuthoringStatus = getStringField(last.record, "status");
396
+ workflowAuthoringGoalSummary = getStringField(last.record, "goal_summary");
397
+ }
398
+ }
399
+ if (evidenceClass === "task_graph") {
400
+ const last = classEntries[classEntries.length - 1];
401
+ if (last !== undefined) {
402
+ const nodes = last.record.nodes;
403
+ const edges = last.record.edges;
404
+ if (Array.isArray(nodes))
405
+ taskGraphTaskCount = nodes.length;
406
+ if (Array.isArray(edges))
407
+ taskGraphEdgeCount = edges.length;
408
+ }
409
+ }
410
+ if (evidenceClass === "task_agent_assignment") {
411
+ taskAgentAssignmentCount = classEntries.length;
412
+ }
413
+ if (evidenceClass === "task_model_selection") {
414
+ const last = classEntries[classEntries.length - 1];
415
+ if (last !== undefined) {
416
+ taskModelSelectionStatus = getStringField(last.record, "selection_status");
417
+ const blockedLabels = last.record.blocked_labels;
418
+ if (Array.isArray(blockedLabels)) {
419
+ taskModelSelectionBlockedLabels = blockedLabels.filter((label) => typeof label === "string");
420
+ }
421
+ }
422
+ }
423
+ if (evidenceClass === "workflow_synthesis_result") {
424
+ const last = classEntries[classEntries.length - 1];
425
+ if (last !== undefined) {
426
+ workflowSynthesisId = getStringField(last.record, "synthesis_id") ?? last.evidenceId;
427
+ const tasks = last.record.tasks_summarized;
428
+ if (typeof tasks === "number")
429
+ workflowSynthesisTasksSummarized = tasks;
430
+ workflowSynthesisConflictDetected = last.record.conflict_detected === true;
431
+ workflowSynthesisSummaryPreview = synthesisSummaryPreview(getStringField(last.record, "synthesis_summary"));
432
+ }
433
+ }
434
+ if (evidenceClass === "workflow_dispatch_plan") {
435
+ const last = classEntries[classEntries.length - 1];
436
+ if (last !== undefined) {
437
+ workflowDispatchPlanRevisionId = getStringField(last.record, "plan_revision_id");
438
+ const tasks = last.record.tasks;
439
+ if (Array.isArray(tasks))
440
+ workflowDispatchPlanTaskCount = tasks.length;
441
+ }
442
+ }
190
443
  }
191
444
  const providerUsageSummary = summarizeLatestProviderUsageSnapshot(reload.entries);
192
445
  return {
@@ -217,8 +470,248 @@ function summarizeWorkflow(workflowId, reload, maxRecent) {
217
470
  ...(providerUsageSummary.count > 0
218
471
  ? { providerUsageSnapshotCount: providerUsageSummary.count }
219
472
  : {}),
473
+ ...(workflowAuthoringStatus !== undefined
474
+ ? { latestWorkflowAuthoringStatus: workflowAuthoringStatus }
475
+ : {}),
476
+ ...(workflowAuthoringGoalSummary !== undefined
477
+ ? { latestWorkflowAuthoringGoalSummary: workflowAuthoringGoalSummary }
478
+ : {}),
479
+ ...(taskGraphTaskCount !== undefined
480
+ ? { latestTaskGraphTaskCount: taskGraphTaskCount }
481
+ : {}),
482
+ ...(taskGraphEdgeCount !== undefined
483
+ ? { latestTaskGraphEdgeCount: taskGraphEdgeCount }
484
+ : {}),
485
+ ...(taskAgentAssignmentCount !== undefined
486
+ ? { latestTaskAgentAssignmentCount: taskAgentAssignmentCount }
487
+ : {}),
488
+ ...(taskModelSelectionStatus !== undefined
489
+ ? { latestTaskModelSelectionStatus: taskModelSelectionStatus }
490
+ : {}),
491
+ ...(taskModelSelectionBlockedLabels !== undefined
492
+ ? { latestTaskModelSelectionBlockedLabels: taskModelSelectionBlockedLabels }
493
+ : {}),
494
+ ...(workflowSynthesisId !== undefined
495
+ ? { latestWorkflowSynthesisId: workflowSynthesisId }
496
+ : {}),
497
+ ...(workflowSynthesisTasksSummarized !== undefined
498
+ ? { latestWorkflowSynthesisTasksSummarized: workflowSynthesisTasksSummarized }
499
+ : {}),
500
+ ...(workflowSynthesisConflictDetected !== undefined
501
+ ? { latestWorkflowSynthesisConflictDetected: workflowSynthesisConflictDetected }
502
+ : {}),
503
+ ...(workflowSynthesisSummaryPreview !== undefined
504
+ ? { latestWorkflowSynthesisSummaryPreview: workflowSynthesisSummaryPreview }
505
+ : {}),
506
+ ...(workflowDispatchPlanRevisionId !== undefined
507
+ ? {
508
+ latestWorkflowDispatchPlanRevisionId: workflowDispatchPlanRevisionId,
509
+ }
510
+ : {}),
511
+ ...(workflowDispatchPlanTaskCount !== undefined
512
+ ? { latestWorkflowDispatchPlanTaskCount: workflowDispatchPlanTaskCount }
513
+ : {}),
220
514
  };
221
515
  }
516
+ function buildLaneProgressCards(workflowId, reload, projection) {
517
+ const lifecycleMeta = new Map();
518
+ const verdictLabels = new Map();
519
+ const taskResultLaneIds = new Set();
520
+ const taskResultByLane = new Map();
521
+ const agentTaskContextByLane = new Map();
522
+ const childSessionByLane = new Map();
523
+ const agentTaskProgressByLane = new Map();
524
+ for (const entry of reload.entries) {
525
+ if (entry.evidenceClass === "agent_task_progress") {
526
+ const laneId = getStringField(entry.record, "lane_id");
527
+ if (laneId !== undefined) {
528
+ const observedAt = getStringField(entry.record, "observed_at");
529
+ const observedAtMs = observedAt === undefined ? 0 : Date.parse(observedAt);
530
+ const current = agentTaskProgressByLane.get(laneId);
531
+ if (current === undefined || current.observedAtMs <= observedAtMs) {
532
+ agentTaskProgressByLane.set(laneId, {
533
+ observedAtMs: Number.isFinite(observedAtMs) ? observedAtMs : 0,
534
+ phase: getStringField(entry.record, "phase"),
535
+ label: getStringField(entry.record, "progress_label"),
536
+ observedAt,
537
+ });
538
+ }
539
+ }
540
+ continue;
541
+ }
542
+ if (entry.evidenceClass === "agent_task_context") {
543
+ const laneId = getStringField(entry.record, "lane_id");
544
+ if (laneId !== undefined) {
545
+ agentTaskContextByLane.set(laneId, {
546
+ taskId: getStringField(entry.record, "task_id"),
547
+ promptPreview: compactPromptPreview(getStringField(entry.record, "prompt_text")),
548
+ promptTextTruncated: entry.record.prompt_text_truncated === true,
549
+ });
550
+ }
551
+ continue;
552
+ }
553
+ if (entry.evidenceClass === "agent_task_child_session") {
554
+ const laneId = getStringField(entry.record, "lane_id");
555
+ const nudgeCount = entry.record.nudge_count;
556
+ if (laneId !== undefined) {
557
+ childSessionByLane.set(laneId, {
558
+ ...(typeof nudgeCount === "number" && Number.isFinite(nudgeCount)
559
+ ? { nudgeCount }
560
+ : {}),
561
+ });
562
+ }
563
+ continue;
564
+ }
565
+ if (entry.evidenceClass === "task_result") {
566
+ const laneId = getStringField(entry.record, "lane_id");
567
+ if (laneId !== undefined) {
568
+ taskResultLaneIds.add(laneId);
569
+ taskResultByLane.set(laneId, {
570
+ taskId: getStringField(entry.record, "task_id"),
571
+ completionStatus: getStringField(entry.record, "completion_status"),
572
+ outputKind: getStringField(entry.record, "output_kind"),
573
+ ...(typeof entry.record.usable_for_synthesis === "boolean"
574
+ ? { usableForSynthesis: entry.record.usable_for_synthesis }
575
+ : {}),
576
+ });
577
+ }
578
+ continue;
579
+ }
580
+ if (entry.evidenceClass === "reviewer_verdict") {
581
+ const label = getStringField(entry.record, "verdict_label");
582
+ if (label === "pass" ||
583
+ label === "changes_required" ||
584
+ label === "blocked" ||
585
+ label === "inconclusive")
586
+ verdictLabels.set(entry.evidenceId, label);
587
+ }
588
+ if (entry.evidenceClass !== "lane_lifecycle")
589
+ continue;
590
+ const laneId = getStringField(entry.record, "lane_id");
591
+ if (laneId === undefined)
592
+ continue;
593
+ const updatedAtRaw = getStringField(entry.record, "updated_at") ??
594
+ getStringField(entry.record, "created_at");
595
+ const updatedAtMs = updatedAtRaw === undefined ? 0 : Date.parse(updatedAtRaw);
596
+ const current = lifecycleMeta.get(laneId);
597
+ if (current !== undefined && current.updatedAtMs > updatedAtMs)
598
+ continue;
599
+ lifecycleMeta.set(laneId, {
600
+ updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0,
601
+ state: getStringField(entry.record, "state"),
602
+ agentRef: getStringField(entry.record, "agent_ref"),
603
+ providerQualifiedModelId: getStringField(entry.record, "provider_qualified_model_id"),
604
+ verdictRef: getStringField(entry.record, "verdict_ref"),
605
+ });
606
+ }
607
+ return projection.entries.slice(0, 6).map((entry) => {
608
+ const meta = lifecycleMeta.get(entry.laneId);
609
+ const context = agentTaskContextByLane.get(entry.laneId);
610
+ const childSession = childSessionByLane.get(entry.laneId);
611
+ const progress = agentTaskProgressByLane.get(entry.laneId);
612
+ const taskResult = taskResultByLane.get(entry.laneId);
613
+ const projectedState = meta?.state === "incomplete" && taskResultLaneIds.has(entry.laneId)
614
+ ? "task_result"
615
+ : (meta?.state ?? entry.lifecycleState);
616
+ const taskId = context?.taskId ?? taskResult?.taskId;
617
+ const verdictLabel = meta?.verdictRef === undefined
618
+ ? undefined
619
+ : verdictLabels.get(meta.verdictRef);
620
+ return {
621
+ workflowId,
622
+ laneId: entry.laneId,
623
+ ...(taskId === undefined ? {} : { taskId }),
624
+ attemptId: entry.attemptId,
625
+ state: projectedState,
626
+ classification: entry.classification,
627
+ ...(entry.secondsSinceLastSignal === undefined
628
+ ? {}
629
+ : { secondsSinceLastSignal: entry.secondsSinceLastSignal }),
630
+ ...(entry.lastSignalSource === undefined
631
+ ? {}
632
+ : { lastSignalSource: entry.lastSignalSource }),
633
+ ...(meta?.agentRef === undefined ? {} : { agentRef: meta.agentRef }),
634
+ ...(meta?.providerQualifiedModelId === undefined
635
+ ? {}
636
+ : { providerQualifiedModelId: meta.providerQualifiedModelId }),
637
+ ...(context?.promptPreview === undefined ? {} : { promptPreview: context.promptPreview }),
638
+ ...(context?.promptTextTruncated === undefined ? {} : { promptTextTruncated: context.promptTextTruncated }),
639
+ ...(childSession?.nudgeCount === undefined ? {} : { nudgeCount: childSession.nudgeCount }),
640
+ ...(progress?.phase === undefined ? {} : { progressPhase: progress.phase }),
641
+ ...(progress?.label === undefined ? {} : { progressLabel: progress.label }),
642
+ ...(progress?.observedAt === undefined ? {} : { progressObservedAt: progress.observedAt }),
643
+ ...(verdictLabel === undefined ? {} : { verdictLabel }),
644
+ ...(taskResult?.completionStatus === undefined
645
+ ? {}
646
+ : { completionStatus: taskResult.completionStatus }),
647
+ ...(taskResult?.outputKind === undefined
648
+ ? {}
649
+ : { outputKind: taskResult.outputKind }),
650
+ ...(taskResult?.usableForSynthesis === undefined
651
+ ? {}
652
+ : { usableForSynthesis: taskResult.usableForSynthesis }),
653
+ ...(entry.failureHint === undefined
654
+ ? {}
655
+ : { failureHint: entry.failureHint }),
656
+ statusCommandRef: "/flowdesk-status",
657
+ debugCommandRef: "/flowdesk-export-debug",
658
+ };
659
+ });
660
+ }
661
+ function buildLaneProgressAggregate(cards, synthesisAlreadyRecorded = false) {
662
+ const expected = cards.length;
663
+ const terminal = cards.filter(card => card.classification === "terminal").length;
664
+ const taskResult = cards.filter(card => card.state === "task_result").length;
665
+ const failed = cards.filter(card => card.failureHint !== undefined || card.state === "invocation_failed" || card.state === "task_failed").length;
666
+ const awaitingPermission = cards.filter(card => card.progressPhase === "awaiting_permission").length;
667
+ const normalCompleted = cards.filter(card => card.state === "task_result" &&
668
+ card.classification === "terminal" &&
669
+ card.completionStatus !== "partial" &&
670
+ card.usableForSynthesis !== false &&
671
+ card.failureHint === undefined).length;
672
+ const nextActionReady = expected > 0 && normalCompleted === expected && !synthesisAlreadyRecorded;
673
+ return {
674
+ expected,
675
+ terminal,
676
+ taskResult,
677
+ failed,
678
+ awaitingPermission,
679
+ normalCompleted,
680
+ autoNextStepEligible: nextActionReady,
681
+ nextActionAvailable: nextActionReady,
682
+ ...(nextActionReady ? { nextActionKind: "synthesis" } : {}),
683
+ nextActionRefs: ["/flowdesk-status", "/flowdesk-export-debug"],
684
+ };
685
+ }
686
+ function buildSubtaskActivityRows(cards) {
687
+ return cards.map((card) => {
688
+ const recoveryActionRefs = card.failureHint !== undefined ||
689
+ card.classification === "stalled" ||
690
+ card.classification === "progressing_late"
691
+ ? ["/flowdesk-status", "/flowdesk-retry", "/flowdesk-resume", "/flowdesk-abort", "/flowdesk-export-debug"]
692
+ : ["/flowdesk-status", "/flowdesk-export-debug"];
693
+ return {
694
+ workflowId: card.workflowId,
695
+ laneId: card.laneId,
696
+ ...(card.taskId === undefined ? {} : { taskId: card.taskId }),
697
+ ...(card.attemptId === undefined ? {} : { attemptId: card.attemptId }),
698
+ ...(card.state === undefined ? {} : { state: card.state }),
699
+ classification: card.classification,
700
+ ...(card.progressPhase === undefined ? {} : { progressPhase: card.progressPhase }),
701
+ ...(card.progressLabel === undefined ? {} : { progressLabel: card.progressLabel }),
702
+ ...(card.progressObservedAt === undefined ? {} : { lastObservedAt: card.progressObservedAt }),
703
+ ...(card.nudgeCount === undefined ? {} : { nudgeCount: card.nudgeCount }),
704
+ ...(card.completionStatus === undefined ? {} : { completionStatus: card.completionStatus }),
705
+ ...(card.outputKind === undefined ? {} : { outputKind: card.outputKind }),
706
+ ...(card.usableForSynthesis === undefined ? {} : { usableForSynthesis: card.usableForSynthesis }),
707
+ ...(card.verdictLabel === undefined ? {} : { verdictLabel: card.verdictLabel }),
708
+ ...(card.failureHint === undefined ? {} : { failureHint: card.failureHint }),
709
+ recoveryActionRefs,
710
+ statusCommandRef: "/flowdesk-status",
711
+ debugCommandRef: "/flowdesk-export-debug",
712
+ };
713
+ });
714
+ }
222
715
  function summarizeLatestProviderUsageSnapshot(entries) {
223
716
  const snapshots = entries.filter((entry) => entry.evidenceClass === "provider_usage_snapshot");
224
717
  if (snapshots.length === 0)
@@ -249,6 +742,7 @@ export async function executeFlowDeskStatusLiveV1(input) {
249
742
  workflows: [],
250
743
  redactedBlockReason: "status live tool requires a durable state root directory",
251
744
  safeNextActions: safeNextActions(),
745
+ authorityCapabilitySummary: authorityCapabilitySummary(),
252
746
  authority: blockedAuthority(),
253
747
  };
254
748
  }
@@ -267,51 +761,93 @@ export async function executeFlowDeskStatusLiveV1(input) {
267
761
  ? `no durable session evidence for workflow ${requestedWorkflowId}`
268
762
  : "no durable session workflows found under the configured durable state root",
269
763
  safeNextActions: safeNextActions(),
764
+ authorityCapabilitySummary: authorityCapabilitySummary(),
270
765
  authority: blockedAuthority(),
271
766
  };
272
767
  }
273
768
  const workflows = [];
274
769
  for (const workflowId of workflowIds) {
275
- const reload = reloadFlowDeskSessionEvidenceV1({
770
+ backfillTerminalAgentTaskFailedLanesV1({
771
+ workflowId,
772
+ rootDir,
773
+ now: new Date(observedAt),
774
+ });
775
+ let reload = reloadFlowDeskSessionEvidenceV1({
276
776
  workflowId,
277
777
  rootDir,
278
778
  });
279
- const summary = summarizeWorkflow(workflowId, reload, maxRecentEvidencePerClass);
280
- const projectionReload = pruneNonTerminalLifecycleSnapshotsNoLongerValid(reload);
281
- const projection = projectFlowDeskLaneStallV1({
779
+ let summary;
780
+ let projectionReload;
781
+ let projection;
782
+ const buildWorkflowSummary = () => {
783
+ summary = summarizeWorkflow(workflowId, reload, maxRecentEvidencePerClass);
784
+ projectionReload = pruneInconsistencySnapshotsNoLongerValid(pruneNonTerminalLifecycleSnapshotsNoLongerValid(reload));
785
+ projection = projectFlowDeskLaneStallV1({
786
+ workflowId,
787
+ reload: projectionReload,
788
+ observedAt,
789
+ ...(input.config.laneHeartbeatLateThresholdMs === undefined
790
+ ? {}
791
+ : { lateThresholdMs: input.config.laneHeartbeatLateThresholdMs }),
792
+ ...(input.config.laneHeartbeatStallThresholdMs === undefined
793
+ ? {}
794
+ : { stallThresholdMs: input.config.laneHeartbeatStallThresholdMs }),
795
+ });
796
+ summary.laneStallProjection = projection;
797
+ const projectedLifecycleStates = projection.entries
798
+ .map((entry) => entry.lifecycleState)
799
+ .filter((state) => typeof state === "string" && state.length > 0);
800
+ if (projectedLifecycleStates.length > 0) {
801
+ summary.latestLaneLifecycleStates = projectedLifecycleStates;
802
+ }
803
+ summary.worstLaneStallClassification = projection.worstClassification;
804
+ summary.stalledLaneCount = projection.totalStalledLanes;
805
+ summary.progressingLateLaneCount = projection.totalLateLanes;
806
+ summary.inconsistentFinalizingWithoutTerminalLaneCount =
807
+ projection.totalInconsistentLanes;
808
+ summary.laneProgressCards = buildLaneProgressCards(workflowId, projectionReload, projection);
809
+ summary.subtaskActivityRows = buildSubtaskActivityRows(summary.laneProgressCards);
810
+ summary.laneProgressAggregate = buildLaneProgressAggregate(summary.laneProgressCards, summary.latestWorkflowSynthesisTasksSummarized !== undefined);
811
+ };
812
+ const graceMs = clampFinalizingInconsistencyGraceMs(input.config.agentTaskFinalizingInconsistencyGraceMs);
813
+ const wroteInconsistency = materializeFinalizingWithoutTerminalInconsistencies({
282
814
  workflowId,
283
- reload: projectionReload,
815
+ rootDir,
816
+ reload,
284
817
  observedAt,
285
- ...(input.config.laneHeartbeatLateThresholdMs === undefined
286
- ? {}
287
- : { lateThresholdMs: input.config.laneHeartbeatLateThresholdMs }),
288
- ...(input.config.laneHeartbeatStallThresholdMs === undefined
289
- ? {}
290
- : { stallThresholdMs: input.config.laneHeartbeatStallThresholdMs }),
818
+ graceMs,
291
819
  });
292
- summary.laneStallProjection = projection;
293
- summary.worstLaneStallClassification = projection.worstClassification;
294
- summary.stalledLaneCount = projection.totalStalledLanes;
295
- summary.progressingLateLaneCount = projection.totalLateLanes;
820
+ if (wroteInconsistency) {
821
+ reload = reloadFlowDeskSessionEvidenceV1({
822
+ workflowId,
823
+ rootDir,
824
+ });
825
+ }
826
+ buildWorkflowSummary();
296
827
  workflows.push(summary);
297
828
  }
298
829
  const observed = workflows.some((summary) => Object.values(summary.evidenceCounts).some((count) => typeof count === "number" && count > 0));
299
830
  const totalStalled = workflows.reduce((sum, summary) => sum + (summary.stalledLaneCount ?? 0), 0);
300
831
  const totalLate = workflows.reduce((sum, summary) => sum + (summary.progressingLateLaneCount ?? 0), 0);
301
- const worstLaneStallClassification = workflows.some((summary) => summary.worstLaneStallClassification === "stalled")
302
- ? "stalled"
303
- : workflows.some((summary) => summary.worstLaneStallClassification === "progressing_late")
304
- ? "progressing_late"
305
- : workflows.some((summary) => summary.worstLaneStallClassification === "progressing_normal")
306
- ? "progressing_normal"
307
- : workflows.some((summary) => summary.worstLaneStallClassification === "terminal")
308
- ? "terminal"
309
- : "unknown";
832
+ const totalInconsistent = workflows.reduce((sum, summary) => sum + (summary.inconsistentFinalizingWithoutTerminalLaneCount ?? 0), 0);
833
+ const worstLaneStallClassification = workflows.some((summary) => summary.worstLaneStallClassification ===
834
+ "inconsistent_finalizing_without_terminal")
835
+ ? "inconsistent_finalizing_without_terminal"
836
+ : workflows.some((summary) => summary.worstLaneStallClassification === "stalled")
837
+ ? "stalled"
838
+ : workflows.some((summary) => summary.worstLaneStallClassification === "progressing_late")
839
+ ? "progressing_late"
840
+ : workflows.some((summary) => summary.worstLaneStallClassification === "progressing_normal")
841
+ ? "progressing_normal"
842
+ : workflows.some((summary) => summary.worstLaneStallClassification === "terminal")
843
+ ? "terminal"
844
+ : "unknown";
310
845
  const summaryForUser = buildStatusLiveSummaryForUser({
311
846
  workflows,
312
847
  worstLaneStallClassification,
313
848
  totalStalled,
314
849
  totalLate,
850
+ totalInconsistent,
315
851
  requestedWorkflowId,
316
852
  });
317
853
  return {
@@ -324,8 +860,10 @@ export async function executeFlowDeskStatusLiveV1(input) {
324
860
  worstLaneStallClassification,
325
861
  totalStalledLaneCount: totalStalled,
326
862
  totalProgressingLateLaneCount: totalLate,
863
+ totalInconsistentFinalizingWithoutTerminalLaneCount: totalInconsistent,
327
864
  summaryForUser,
328
865
  safeNextActions: safeNextActions(),
866
+ authorityCapabilitySummary: authorityCapabilitySummary(),
329
867
  authority: {
330
868
  realOpenCodeDispatch: false,
331
869
  providerCall: false,
@@ -345,26 +883,70 @@ function buildStatusLiveSummaryForUser(input) {
345
883
  ? `FlowDesk status: no durable evidence for workflow ${input.requestedWorkflowId}.`
346
884
  : "FlowDesk status: no durable workflows found.";
347
885
  }
348
- const headline = input.totalStalled > 0
349
- ? `FlowDesk status: ${workflowsCount} workflow(s); worst classification stalled (${input.totalStalled} stalled, ${input.totalLate} progressing-late).`
350
- : input.totalLate > 0
351
- ? `FlowDesk status: ${workflowsCount} workflow(s); ${input.totalLate} progressing-late, no stalled lanes.`
352
- : input.worstLaneStallClassification === "terminal"
353
- ? `FlowDesk status: ${workflowsCount} workflow(s); all observed lanes terminal/complete.`
354
- : input.worstLaneStallClassification === "progressing_normal"
355
- ? `FlowDesk status: ${workflowsCount} workflow(s); active lanes progressing_normal.`
356
- : `FlowDesk status: ${workflowsCount} workflow(s); worst classification ${input.worstLaneStallClassification}.`;
886
+ const headline = input.totalInconsistent > 0
887
+ ? `FlowDesk status: ${workflowsCount} workflow(s); ${input.totalInconsistent} finalizing-without-terminal inconsistent lane(s) require manual recovery.`
888
+ : input.totalStalled > 0
889
+ ? `FlowDesk status: ${workflowsCount} workflow(s); worst classification stalled (${input.totalStalled} stalled, ${input.totalLate} progressing-late).`
890
+ : input.totalLate > 0
891
+ ? `FlowDesk status: ${workflowsCount} workflow(s); ${input.totalLate} progressing-late, no stalled lanes.`
892
+ : input.worstLaneStallClassification === "terminal"
893
+ ? `FlowDesk status: ${workflowsCount} workflow(s); all observed lanes terminal/complete.`
894
+ : input.worstLaneStallClassification === "progressing_normal"
895
+ ? `FlowDesk status: ${workflowsCount} workflow(s); active lanes progressing_normal.`
896
+ : `FlowDesk status: ${workflowsCount} workflow(s); worst classification ${input.worstLaneStallClassification}.`;
357
897
  const perWorkflow = input.workflows.slice(0, 3).map((workflow) => {
358
898
  const verdictLabels = workflow.latestReviewerVerdictLabels;
359
899
  const verdictText = verdictLabels.length > 0
360
900
  ? `verdicts=${verdictLabels.join("/")}`
361
901
  : "verdicts=(none)";
362
- const lifecycleStates = workflow.latestLaneLifecycleStates;
902
+ const projectedLaneStates = (workflow.laneProgressCards ?? [])
903
+ .slice(0, 3)
904
+ .map((lane) => `${lane.state ?? "unknown"}/${lane.classification}`);
905
+ const lifecycleStates = projectedLaneStates.length > 0 ? projectedLaneStates : workflow.latestLaneLifecycleStates;
363
906
  const lifecycleText = lifecycleStates.length > 0
364
- ? `lifecycle=${lifecycleStates.slice(0, 3).join("/")}`
365
- : "lifecycle=(none)";
907
+ ? `${projectedLaneStates.length > 0 ? "lane_state" : "lifecycle"}=${lifecycleStates.slice(0, 3).join("/")}`
908
+ : "lane_state=(none)";
909
+ const synthesisPreview = workflow.latestWorkflowSynthesisSummaryPreview === undefined
910
+ ? ""
911
+ : ` preview="${workflow.latestWorkflowSynthesisSummaryPreview.replace(/"/g, "'").slice(0, 96)}"`;
912
+ const synthesisText = workflow.latestWorkflowSynthesisTasksSummarized !== undefined
913
+ ? ` synthesis=${workflow.latestWorkflowSynthesisTasksSummarized} tasks${workflow.latestWorkflowSynthesisConflictDetected ? " conflict=detected" : ""}${synthesisPreview}`
914
+ : "";
915
+ const autoNextText = workflow.laneProgressAggregate?.autoNextStepEligible === true ? " auto_next=ready" : "";
916
+ const nextActionText = workflow.laneProgressAggregate?.nextActionAvailable === true
917
+ ? ` next_action=${workflow.laneProgressAggregate.nextActionKind ?? "available"}_ready`
918
+ : "";
919
+ const planText = workflow.latestWorkflowDispatchPlanRevisionId !== undefined
920
+ ? `workflow_plan=${workflow.latestWorkflowDispatchPlanRevisionId} tasks=${workflow.latestWorkflowDispatchPlanTaskCount ?? "unknown"}${synthesisText}${autoNextText}${nextActionText}`
921
+ : workflow.latestTaskGraphTaskCount !== undefined
922
+ ? `planning_slice=${workflow.latestWorkflowAuthoringStatus ?? "unknown"} tasks=${workflow.latestTaskGraphTaskCount} assignments=${workflow.latestTaskAgentAssignmentCount ?? 0} model=${workflow.latestTaskModelSelectionStatus ?? "unknown"}${synthesisText}${autoNextText}${nextActionText}`
923
+ : synthesisText.length > 0
924
+ ? `${synthesisText.trim()}${autoNextText}${nextActionText}`
925
+ : `workflow_plan=(none)${autoNextText}${nextActionText}`;
366
926
  const classification = workflow.worstLaneStallClassification ?? "unknown";
367
- return `- ${workflow.workflowId}: ${classification}, ${verdictText}, ${lifecycleText}`;
927
+ const lanePreview = (workflow.laneProgressCards ?? [])
928
+ .slice(0, 2)
929
+ .map((lane) => {
930
+ const age = lane.secondsSinceLastSignal === undefined
931
+ ? "unknown"
932
+ : `~${Math.floor(lane.secondsSinceLastSignal / 60)}m`;
933
+ return `${lane.laneId}:${lane.state ?? "unknown"}/${lane.classification}/last=${age}`;
934
+ })
935
+ .join(", ");
936
+ const subtaskPreview = (workflow.subtaskActivityRows ?? [])
937
+ .slice(0, 3)
938
+ .map((row) => {
939
+ const actions = (row.recoveryActionRefs ?? [])
940
+ .slice(0, 5)
941
+ .map((action) => action.replace("/flowdesk-", ""))
942
+ .join("|");
943
+ const compact = `${row.laneId}:${row.state ?? "unknown"}/${row.classification}${actions.length > 0 ? `[${actions}]` : ""}`;
944
+ return compact.length > 96 ? `${compact.slice(0, 95)}…` : compact;
945
+ })
946
+ .join("; ");
947
+ const laneText = lanePreview.length > 0 && subtaskPreview.length === 0 ? `, lanes=${lanePreview}` : "";
948
+ const subtaskText = subtaskPreview.length > 0 ? `, subtasks=${subtaskPreview}` : "";
949
+ return `- ${workflow.workflowId}: ${classification}, ${verdictText}, ${lifecycleText}, ${planText}${laneText}${subtaskText}`;
368
950
  });
369
951
  return [headline, ...perWorkflow].join("\n");
370
952
  }