@flowdesk/opencode-plugin 0.1.13 → 0.1.15

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 (80) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-task-output.d.ts +29 -0
  3. package/dist/agent-task-output.d.ts.map +1 -0
  4. package/dist/agent-task-output.js +225 -0
  5. package/dist/agent-task-output.js.map +1 -0
  6. package/dist/agent-task-runner.d.ts +34 -0
  7. package/dist/agent-task-runner.d.ts.map +1 -1
  8. package/dist/agent-task-runner.js +634 -84
  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 +390 -0
  17. package/dist/completion-ui-cache.js.map +1 -0
  18. package/dist/event-hook-observer.d.ts +14 -0
  19. package/dist/event-hook-observer.d.ts.map +1 -0
  20. package/dist/event-hook-observer.js +257 -0
  21. package/dist/event-hook-observer.js.map +1 -0
  22. package/dist/managed-dispatch-adapter.d.ts +62 -0
  23. package/dist/managed-dispatch-adapter.d.ts.map +1 -1
  24. package/dist/managed-dispatch-adapter.js +472 -4
  25. package/dist/managed-dispatch-adapter.js.map +1 -1
  26. package/dist/model-selection-engine.d.ts +60 -0
  27. package/dist/model-selection-engine.d.ts.map +1 -0
  28. package/dist/model-selection-engine.js +242 -0
  29. package/dist/model-selection-engine.js.map +1 -0
  30. package/dist/provider-usage-live-tool.d.ts +10 -0
  31. package/dist/provider-usage-live-tool.d.ts.map +1 -1
  32. package/dist/provider-usage-live-tool.js +262 -33
  33. package/dist/provider-usage-live-tool.js.map +1 -1
  34. package/dist/server.d.ts +36 -1
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +497 -20
  37. package/dist/server.js.map +1 -1
  38. package/dist/stall-recovery.d.ts +34 -0
  39. package/dist/stall-recovery.d.ts.map +1 -1
  40. package/dist/stall-recovery.js +680 -3
  41. package/dist/stall-recovery.js.map +1 -1
  42. package/dist/status-live-tool.d.ts +54 -0
  43. package/dist/status-live-tool.d.ts.map +1 -1
  44. package/dist/status-live-tool.js +449 -44
  45. package/dist/status-live-tool.js.map +1 -1
  46. package/dist/tui-subtask-activity.d.ts +73 -0
  47. package/dist/tui-subtask-activity.d.ts.map +1 -0
  48. package/dist/tui-subtask-activity.js +271 -0
  49. package/dist/tui-subtask-activity.js.map +1 -0
  50. package/dist/tui-usage-snapshot.d.ts +14 -0
  51. package/dist/tui-usage-snapshot.d.ts.map +1 -1
  52. package/dist/tui-usage-snapshot.js +275 -8
  53. package/dist/tui-usage-snapshot.js.map +1 -1
  54. package/dist/tui.d.ts.map +1 -1
  55. package/dist/tui.js +102 -44
  56. package/dist/tui.js.map +1 -1
  57. package/dist/workflow-assign-tool.d.ts +23 -0
  58. package/dist/workflow-assign-tool.d.ts.map +1 -0
  59. package/dist/workflow-assign-tool.js +135 -0
  60. package/dist/workflow-assign-tool.js.map +1 -0
  61. package/dist/workflow-author-tool.d.ts +29 -0
  62. package/dist/workflow-author-tool.d.ts.map +1 -0
  63. package/dist/workflow-author-tool.js +227 -0
  64. package/dist/workflow-author-tool.js.map +1 -0
  65. package/dist/workflow-dispatch-tool.d.ts +12 -0
  66. package/dist/workflow-dispatch-tool.d.ts.map +1 -1
  67. package/dist/workflow-dispatch-tool.js +31 -3
  68. package/dist/workflow-dispatch-tool.js.map +1 -1
  69. package/dist/workflow-orchestrator.d.ts +31 -0
  70. package/dist/workflow-orchestrator.d.ts.map +1 -0
  71. package/dist/workflow-orchestrator.js +160 -0
  72. package/dist/workflow-orchestrator.js.map +1 -0
  73. package/dist/workflow-scheduler.d.ts.map +1 -1
  74. package/dist/workflow-scheduler.js +3 -1
  75. package/dist/workflow-scheduler.js.map +1 -1
  76. package/dist/workflow-synthesis-tool.d.ts +31 -0
  77. package/dist/workflow-synthesis-tool.d.ts.map +1 -0
  78. package/dist/workflow-synthesis-tool.js +194 -0
  79. package/dist/workflow-synthesis-tool.js.map +1 -0
  80. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -4,18 +4,23 @@ import { join, resolve, sep } from "node:path";
4
4
  import { applyFlowDeskSessionEvidenceWriteIntentsV1, createFlowDeskChatHookAuthorityProbeV1, evaluateFlowDeskChatIntakeV1, getFlowDeskPortableCommandToolName, getRelease1SchemaArtifact, materializeFlowDeskExactModelCacheEvidenceFromProviderAcquisitionEvidenceV1, materializeFlowDeskRuntimeLaneLaunchPlansFromReviewerFanoutEvidenceV1, planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, validateFlowDeskDefaultManagedDispatchAuthorizationV1, validateProjectConfigV1, validateRunRequestV1, } from "@flowdesk/core";
5
5
  import { tool, } from "@opencode-ai/plugin";
6
6
  import { flowdeskPluginId, flowdeskPluginScaffold, hasProductionOpenCodeRegistration, } from "./index.js";
7
+ import { observeFlowDeskOpenCodeEventV1 } from "./event-hook-observer.js";
7
8
  import { recordFlowDeskLaneHeartbeatV1, } from "./lane-heartbeat-writer.js";
8
9
  import { createFlowDeskLocalNonDispatchAdapterSession, flowdeskLocalNonDispatchAdapterProfile, hasFlowDeskLocalPlanningEvidenceV1, } from "./local-adapter.js";
9
- import { createFlowDeskManagedDispatchBetaDurableReservationStoreV1, createFlowDeskOpenCodeMetadataProviderAcquisitionClientV1, createFlowDeskOpenCodePromptBackedProviderAcquisitionClientV1, dispatchManagedDispatchBetaPromptV1, materializeFlowDeskManagedFallbackRegatePlanEvidenceV1, orchestrateFlowDeskManagedFallbackRegateV1, runFlowDeskExactModelProviderAcquisitionLiveTestV1, } from "./managed-dispatch-adapter.js";
10
+ import { createFlowDeskManagedDispatchBetaDurableReservationStoreV1, createFlowDeskOpenCodeMetadataProviderAcquisitionClientV1, createFlowDeskOpenCodePromptBackedProviderAcquisitionClientV1, dispatchManagedDispatchBetaPromptV1, observeAndFinalizeManagedDispatchLaneV1, materializeFlowDeskManagedFallbackRegatePlanEvidenceV1, orchestrateFlowDeskManagedFallbackRegateV1, runFlowDeskExactModelProviderAcquisitionLiveTestV1, } from "./managed-dispatch-adapter.js";
10
11
  import { executeFlowDeskProviderUsageLiveV1, } from "./provider-usage-live-tool.js";
12
+ import { formatFlowDeskTuiUsageSnapshotCompactText, loadFlowDeskTuiUsageSnapshotViewV1, } from "./tui-usage-snapshot.js";
11
13
  import { executeFlowDeskQuickFallbackRunV1, } from "./quick-fallback-run.js";
12
14
  import { executeFlowDeskQuickReviewerRunV1, } from "./quick-reviewer-run.js";
13
15
  import { executeFlowDeskRuntimeReviewerExecutionBridgeV1, redactedRuntimeReviewerExecutionBlocked, runtimeReviewerExecutionExpectationsFromValue, } from "./runtime-reviewer-execution-bridge.js";
14
16
  import { executeFlowDeskAgentTaskV1 } from "./agent-task-runner.js";
17
+ import { executeFlowDeskAutoContinuePreviewToolV1, } from "./auto-continue-preview-tool.js";
15
18
  import { executeFlowDeskControlledWriteApplyToolV1, } from "./controlled-write-tool.js";
16
19
  import { executeFlowDeskStatusLiveV1, } from "./status-live-tool.js";
17
20
  import { executeFlowDeskWorkflowDispatchPlanToolV1, } from "./workflow-dispatch-plan-tool.js";
18
21
  import { executeFlowDeskWorkflowDispatchToolV1, } from "./workflow-dispatch-tool.js";
22
+ import { executeFlowDeskWorkflowOrchestratorV1 } from "./workflow-orchestrator.js";
23
+ import { executeFlowDeskWorkflowSynthesisPreviewV1 } from "./workflow-synthesis-tool.js";
19
24
  import { evaluateGuardedAutoAbortHookV1, evaluateGuardedAutoRetryHookV1, reconcileStalePendingRetryPlansV1, checkSdkSessionApiHealthV1, runFlowDeskWatchdogCycleV1, } from "./stall-recovery.js";
20
25
  import { withTimeout, FlowDeskTimeoutError } from "./shared/with-timeout.js";
21
26
  import { FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS, getFlowDeskRelease1HandlerReadinessSummary, getFlowDeskRelease1ProductionReadinessSummary, hasPassingFds1SchemaConversionSpike, runFlowDeskPreSpikePluginToolStub, } from "./tool-stubs.js";
@@ -40,10 +45,12 @@ export const flowdeskLaneHeartbeatWriterOption = "laneHeartbeatWriter";
40
45
  export const flowdeskWorkflowDispatchPlanToolOption = "workflowDispatchPlanTool";
41
46
  export const flowdeskWorkflowDispatchOption = "workflowDispatch";
42
47
  export const flowdeskControlledWriteApplyOption = "controlledWriteApply";
48
+ export const flowdeskUiProbeOption = "uiProbe";
43
49
  export const flowdeskDefaultManagedDispatchAuthorizationOption = "defaultManagedDispatchAuthorization";
44
50
  export const flowdeskWatchdogOption = "watchdog";
45
51
  export const flowdeskWatchdogTriggerToolName = "flowdesk_watchdog_trigger";
46
52
  export const flowdeskManagedDispatchBetaToolName = "flowdesk_managed_dispatch_beta";
53
+ export const flowdeskManagedDispatchLaneFinalizeToolName = "flowdesk_managed_dispatch_lane_finalize";
47
54
  export const flowdeskExactModelProviderAcquisitionLiveTestToolName = "flowdesk_exact_model_provider_acquisition_live_test";
48
55
  export const flowdeskRuntimeReviewerExecutionToolName = "flowdesk_runtime_reviewer_execution";
49
56
  export const flowdeskManagedFallbackRegateToolName = "flowdesk_managed_fallback_regate";
@@ -57,6 +64,9 @@ export const flowdeskWorkflowDispatchToolName = "flowdesk_workflow_dispatch";
57
64
  export const flowdeskControlledWriteApplyToolName = "flowdesk_controlled_write_apply";
58
65
  export const flowdeskAgentTaskRunOption = "agentTaskRun";
59
66
  export const flowdeskAgentTaskRunToolName = "flowdesk_agent_task_run";
67
+ export const flowdeskWorkflowSynthesisPreviewToolName = "flowdesk_workflow_synthesis_preview";
68
+ export const flowdeskAutoContinuePreviewToolName = "flowdesk_auto_continue_preview";
69
+ export const flowdeskUiProbeToolName = "flowdesk_ui_probe";
60
70
  const flowdeskChatSuggestionDuplicateWindowMs = 10_000;
61
71
  const disabledAuthority = {
62
72
  productionRegistrationEligible: false,
@@ -537,6 +547,15 @@ function redactedManagedDispatchBetaToolResult(result) {
537
547
  sessionId: result.sessionId,
538
548
  agent: result.agent,
539
549
  model: result.model,
550
+ ...(!("laneId" in result) || result.laneId === undefined
551
+ ? {}
552
+ : { laneId: result.laneId }),
553
+ ...(!("childSessionRef" in result) || result.childSessionRef === undefined
554
+ ? {}
555
+ : { childSessionRef: result.childSessionRef }),
556
+ ...(!("messageRef" in result) || result.messageRef === undefined
557
+ ? {}
558
+ : { messageRef: result.messageRef }),
540
559
  ...("redactedErrorCategory" in result
541
560
  ? { redactedErrorCategory: result.redactedErrorCategory }
542
561
  : {}),
@@ -886,6 +905,9 @@ async function evaluateFlowDeskManagedDispatchRunRoute(request, options = {}) {
886
905
  ...(options.reservationStore === undefined
887
906
  ? {}
888
907
  : { reservationStore: options.reservationStore }),
908
+ ...(options.durableStateRootDir === undefined
909
+ ? {}
910
+ : { durableStateRootDir: options.durableStateRootDir }),
889
911
  });
890
912
  return {
891
913
  runRouteProfile: "flowdesk_run_default_managed_dispatch_route",
@@ -944,10 +966,46 @@ export function createFlowDeskManagedDispatchBetaOptInTools(client, reservationS
944
966
  dispatchManifest: record.dispatchManifest,
945
967
  ...(reloadedEvidence === undefined ? {} : { reloadedEvidence }),
946
968
  ...(reservationStore === undefined ? {} : { reservationStore }),
969
+ ...(durableStateRootDir === undefined ? {} : { durableStateRootDir }),
947
970
  });
948
971
  return JSON.stringify(redactedManagedDispatchBetaToolResult(result));
949
972
  },
950
973
  }),
974
+ [flowdeskManagedDispatchLaneFinalizeToolName]: tool({
975
+ description: "FlowDesk managed-dispatch lane finalize observer; reads a launched managed-dispatch lane's child session once and records terminal task_result + lane_lifecycle evidence. Observation-only: never nudges, aborts, or enables dispatch/provider/runtime/fallback authority.",
976
+ args: {
977
+ workflowId: tool.schema.string().describe("Workflow id that owns the managed-dispatch lane."),
978
+ laneId: tool.schema.string().describe("Lane id of the launched managed-dispatch lane."),
979
+ attemptId: tool.schema.string().describe("Attempt id bound to the lane."),
980
+ childSessionId: tool.schema.string().describe("Child session id to observe (raw OpenCode id)."),
981
+ agentRef: tool.schema.string().describe("Agent ref bound to the lane."),
982
+ providerQualifiedModelId: tool.schema.string().describe("Concrete provider-qualified model id for the lane."),
983
+ parentSessionRef: tool.schema.string().optional().describe("Optional parent ses- ref for lifecycle evidence."),
984
+ },
985
+ async execute(input) {
986
+ const record = isRecord(input) ? input : {};
987
+ if (durableStateRootDir === undefined) {
988
+ return JSON.stringify({
989
+ adapterProfile: "managed_dispatch_lane_finalize_observer",
990
+ status: "blocked_before_finalize",
991
+ redactedBlockReason: "managed-dispatch lane finalize requires a configured durable state root.",
992
+ authority: { ...disabledAuthority, toolAuthority: false },
993
+ });
994
+ }
995
+ const result = await observeAndFinalizeManagedDispatchLaneV1({
996
+ client,
997
+ rootDir: durableStateRootDir,
998
+ workflowId: String(record.workflowId ?? ""),
999
+ laneId: String(record.laneId ?? ""),
1000
+ attemptId: String(record.attemptId ?? ""),
1001
+ childSessionId: String(record.childSessionId ?? ""),
1002
+ agentRef: String(record.agentRef ?? "agent-managed-dispatch"),
1003
+ providerQualifiedModelId: String(record.providerQualifiedModelId ?? "unknown/unknown"),
1004
+ ...(typeof record.parentSessionRef === "string" ? { parentSessionRef: record.parentSessionRef } : {}),
1005
+ });
1006
+ return JSON.stringify(result);
1007
+ },
1008
+ }),
951
1009
  };
952
1010
  }
953
1011
  export function createFlowDeskExactModelProviderAcquisitionLiveTestOptInTools(client, rootDir, cacheMaterialization, runtimeReviewerExecutionClient) {
@@ -1175,9 +1233,11 @@ export function createFlowDeskExactModelProviderAcquisitionLiveTestOptInTools(cl
1175
1233
  }),
1176
1234
  };
1177
1235
  }
1178
- export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert, durableSuggestionRoot) {
1236
+ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert, durableSuggestionRoot, providerUsageLiveConfig) {
1179
1237
  const recentSuggestionCards = new Map();
1180
1238
  const recentStallAlerts = new Map();
1239
+ const usageAutoRefreshMaxAgeMs = 3 * 60_000;
1240
+ let lastUsageRefreshAttemptAtMs = 0;
1181
1241
  return async function message(input, output) {
1182
1242
  const inputRecord = isRecord(input) ? input : {};
1183
1243
  const partSessionID = typeof inputRecord.sessionID === "string" ? inputRecord.sessionID : "";
@@ -1192,6 +1252,40 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1192
1252
  const request = intakeRequestFromChatMessage({ ...inputRecord, ...output });
1193
1253
  const preview = previewNaturalLanguageRouting(request, session);
1194
1254
  const nowMs = clockMs(now);
1255
+ if (providerUsageLiveConfig?.durableStateRootDir && nowMs - lastUsageRefreshAttemptAtMs > 30_000) {
1256
+ lastUsageRefreshAttemptAtMs = nowMs;
1257
+ let isStale = false;
1258
+ try {
1259
+ const cachePath = join(providerUsageLiveConfig.durableStateRootDir, ".flowdesk", "ui", "provider-usage-sidebar.json");
1260
+ const cacheContent = JSON.parse(readFileSync(cachePath, "utf8"));
1261
+ if (typeof cacheContent.observed_at === "string" && nowMs - Date.parse(cacheContent.observed_at) > usageAutoRefreshMaxAgeMs)
1262
+ isStale = true;
1263
+ }
1264
+ catch {
1265
+ isStale = true;
1266
+ }
1267
+ if (isStale) {
1268
+ try {
1269
+ await executeFlowDeskProviderUsageLiveV1({ config: { ...providerUsageLiveConfig, persistSidebarCache: true }, request: { providerFamily: "all" } });
1270
+ }
1271
+ catch { }
1272
+ }
1273
+ }
1274
+ const usageTextToAppend = providerUsageLiveConfig?.durableStateRootDir &&
1275
+ providerUsageLiveConfig.appendToChat === true
1276
+ ? formatFlowDeskTuiUsageSnapshotCompactText(loadFlowDeskTuiUsageSnapshotViewV1({
1277
+ rootDir: providerUsageLiveConfig.durableStateRootDir,
1278
+ workflowId: providerUsageLiveConfig.persistWorkflowId,
1279
+ now: () => (typeof now === "function" ? now() : now),
1280
+ }))
1281
+ : undefined;
1282
+ const appendUsageCard = () => {
1283
+ if (usageTextToAppend === undefined)
1284
+ return;
1285
+ if (!Array.isArray(output.parts))
1286
+ output.parts = [];
1287
+ output.parts.push(buildTextPart(usageTextToAppend));
1288
+ };
1195
1289
  for (const [key, recordedAtMs] of recentSuggestionCards) {
1196
1290
  if (nowMs - recordedAtMs > flowdeskChatSuggestionDuplicateWindowMs ||
1197
1291
  nowMs < recordedAtMs)
@@ -1228,7 +1322,8 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1228
1322
  summary.totalLate > 0;
1229
1323
  const progressCardReady = stallAlert?.includeProgressCards === true &&
1230
1324
  summary.workflowSummaries.some((workflow) => (workflow.laneCards?.length ?? 0) > 0);
1231
- if (stalledAlertReady || lateAlertReady || progressCardReady) {
1325
+ const autoNextReady = summary.workflowSummaries.some((workflow) => workflow.autoNextReady === true);
1326
+ if (stalledAlertReady || lateAlertReady || progressCardReady || autoNextReady) {
1232
1327
  stallDedupKey = stallAlertDuplicateKey(request, summary);
1233
1328
  stallTextToAppend = stallAlertText(summary);
1234
1329
  }
@@ -1246,6 +1341,7 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1246
1341
  }
1247
1342
  };
1248
1343
  if (preview.evaluation.response.route_decision === "continue_chat") {
1344
+ appendUsageCard();
1249
1345
  appendStallCard();
1250
1346
  return;
1251
1347
  }
@@ -1256,13 +1352,16 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1256
1352
  recentSuggestionCards.set(duplicateKey, nowMs);
1257
1353
  writeDurableSuggestionPreference(durableSuggestionRoot, duplicateKey, request, preview.evaluation.response, nowMs);
1258
1354
  if (previousAtMs !== undefined &&
1259
- nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs)
1355
+ nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs) {
1356
+ appendUsageCard();
1260
1357
  return;
1358
+ }
1261
1359
  }
1262
1360
  const result = evaluateNaturalLanguageRouting(request, session);
1263
1361
  if (!Array.isArray(output.parts))
1264
1362
  output.parts = [];
1265
1363
  output.parts.push(buildTextPart(steeringText(result)));
1364
+ appendUsageCard();
1266
1365
  appendStallCard();
1267
1366
  };
1268
1367
  }
@@ -1361,12 +1460,14 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
1361
1460
  const stalledEntry = scopedEntries.find((entry) => entry.classification === "stalled");
1362
1461
  const lateEntry = scopedEntries.find((entry) => entry.classification === "progressing_late");
1363
1462
  const primary = stalledEntry ?? lateEntry;
1364
- const scopedLaneCards = (workflow.laneProgressCards ?? []).filter((lane) => lane.classification !== "terminal" &&
1463
+ const autoNextReady = workflow.laneProgressAggregate?.autoNextStepEligible === true;
1464
+ const scopedLaneCards = (workflow.laneProgressCards ?? []).filter((lane) => (autoNextReady || lane.classification !== "terminal") &&
1365
1465
  laneInCurrentSession(lane.laneId));
1366
1466
  const scopedStalledCount = scopedEntries.filter((entry) => entry.classification === "stalled").length;
1367
1467
  const scopedLateCount = scopedEntries.filter((entry) => entry.classification === "progressing_late").length;
1368
1468
  const shouldShowWorkflow = scopedStalledCount > 0 ||
1369
1469
  (stallAlert.includeProgressingLate === true && scopedLateCount > 0) ||
1470
+ (autoNextReady && scopedLaneCards.length > 0) ||
1370
1471
  (stallAlert.includeProgressCards === true && scopedLaneCards.length > 0);
1371
1472
  if (!shouldShowWorkflow)
1372
1473
  return undefined;
@@ -1443,18 +1544,36 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
1443
1544
  ...(primary?.failureHint === undefined
1444
1545
  ? {}
1445
1546
  : { failureHint: primary.failureHint }),
1547
+ ...(workflow.laneProgressAggregate === undefined
1548
+ ? {}
1549
+ : { laneProgressAggregate: workflow.laneProgressAggregate }),
1550
+ ...(autoNextReady ? { autoNextReady: true } : {}),
1551
+ ...(workflow.latestWorkflowSynthesisTasksSummarized === undefined
1552
+ ? {}
1553
+ : { synthesisTasksSummarized: workflow.latestWorkflowSynthesisTasksSummarized }),
1554
+ ...(workflow.latestWorkflowSynthesisConflictDetected === undefined
1555
+ ? {}
1556
+ : { synthesisConflictDetected: workflow.latestWorkflowSynthesisConflictDetected }),
1446
1557
  ...(stallAlert.includeProgressCards === true
1447
1558
  ? {
1448
1559
  laneCards: scopedLaneCards
1449
1560
  .slice(0, stallAlert.maxProgressCards ?? 3)
1450
1561
  .map((lane) => ({
1451
1562
  laneId: lane.laneId,
1563
+ taskId: lane.taskId,
1452
1564
  state: lane.state,
1453
1565
  classification: lane.classification,
1454
1566
  secondsSinceLastSignal: lane.secondsSinceLastSignal,
1455
1567
  agentRef: lane.agentRef,
1456
1568
  providerQualifiedModelId: lane.providerQualifiedModelId,
1569
+ promptPreview: lane.promptPreview,
1570
+ nudgeCount: lane.nudgeCount,
1571
+ progressPhase: lane.progressPhase,
1572
+ progressLabel: lane.progressLabel,
1457
1573
  verdictLabel: lane.verdictLabel,
1574
+ completionStatus: lane.completionStatus,
1575
+ outputKind: lane.outputKind,
1576
+ usableForSynthesis: lane.usableForSynthesis,
1458
1577
  failureHint: lane.failureHint,
1459
1578
  })),
1460
1579
  }
@@ -1524,6 +1643,9 @@ function stallAlertText(summary) {
1524
1643
  else if (summary.worstClassification === "progressing_late") {
1525
1644
  lines.push(`Late-progressing lanes detected: ${summary.totalLate} late, ${summary.totalStalled} stalled.`);
1526
1645
  }
1646
+ else if (summary.workflowSummaries.some((workflow) => workflow.autoNextReady === true)) {
1647
+ lines.push("All FlowDesk subtasks completed normally. Auto-next synthesis is ready.");
1648
+ }
1527
1649
  else if (progressCardCount > 0) {
1528
1650
  lines.push(`Lane progress: ${progressCardCount} lane(s) visible on the main screen.`);
1529
1651
  }
@@ -1534,19 +1656,56 @@ function stallAlertText(summary) {
1534
1656
  const secs = workflow.secondsSinceLastSignal ?? 0;
1535
1657
  const minutes = Math.floor(secs / 60);
1536
1658
  const hint = workflow.failureHint ?? "no recent heartbeat";
1537
- const counts = workflow.stalledLaneCount > 0
1538
- ? `${workflow.stalledLaneCount} stalled`
1539
- : `${workflow.lateLaneCount} progressing-late`;
1540
- lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
1659
+ if (workflow.autoNextReady === true) {
1660
+ const synthesis = workflow.synthesisTasksSummarized === undefined
1661
+ ? "synthesis not yet recorded"
1662
+ : `synthesis recorded for ${workflow.synthesisTasksSummarized} task(s)`;
1663
+ const conflict = workflow.synthesisConflictDetected === undefined
1664
+ ? " conflict=unknown"
1665
+ : workflow.synthesisConflictDetected === true ? " conflict=true" : " conflict=false";
1666
+ const nextAction = workflow.laneProgressAggregate?.nextActionAvailable === true
1667
+ ? ` next_action=${workflow.laneProgressAggregate.nextActionKind ?? "available"}_ready`
1668
+ : "";
1669
+ lines.push(`- workflow ${workflow.workflowId}: auto-next ready (${synthesis},${conflict}${nextAction}).`);
1670
+ }
1671
+ else {
1672
+ const counts = workflow.stalledLaneCount > 0
1673
+ ? `${workflow.stalledLaneCount} stalled`
1674
+ : `${workflow.lateLaneCount} progressing-late`;
1675
+ lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
1676
+ }
1677
+ if (workflow.laneProgressAggregate !== undefined) {
1678
+ const aggregate = workflow.laneProgressAggregate;
1679
+ const nextAction = aggregate.nextActionAvailable === true
1680
+ ? `, next_action=${aggregate.nextActionKind ?? "available"}_ready`
1681
+ : "";
1682
+ lines.push(` tasks: expected=${aggregate.expected}, terminal=${aggregate.terminal}, completed=${aggregate.normalCompleted}, failed=${aggregate.failed}, awaiting_permission=${aggregate.awaitingPermission}, auto_next=${aggregate.autoNextStepEligible}${nextAction}`);
1683
+ }
1541
1684
  for (const lane of workflow.laneCards?.slice(0, 3) ?? []) {
1542
1685
  const age = lane.secondsSinceLastSignal === undefined
1543
1686
  ? "unknown"
1544
1687
  : `~${Math.floor(lane.secondsSinceLastSignal / 60)}m ago`;
1545
1688
  const model = lane.providerQualifiedModelId ?? "(unknown)";
1546
1689
  const agent = lane.agentRef ?? "(unknown)";
1690
+ const task = lane.taskId ?? lane.laneId;
1691
+ const prompt = lane.promptPreview ?? "(hidden)";
1692
+ const nudge = lane.nudgeCount === undefined ? "?" : String(lane.nudgeCount);
1693
+ const progress = lane.progressLabel === undefined
1694
+ ? "(none)"
1695
+ : `${lane.progressPhase ?? "progress"}: ${lane.progressLabel}`;
1547
1696
  const verdict = lane.verdictLabel ?? "(none)";
1697
+ const resultQuality = lane.completionStatus === undefined && lane.outputKind === undefined && lane.usableForSynthesis === undefined
1698
+ ? "(none)"
1699
+ : `${lane.completionStatus ?? "unknown"}/${lane.outputKind ?? "unknown"}/synthesis=${lane.usableForSynthesis === undefined ? "unknown" : String(lane.usableForSynthesis)}`;
1548
1700
  const issue = lane.failureHint === undefined ? "" : ` issue=${lane.failureHint}`;
1549
- lines.push(` - lane ${lane.laneId}: ${lane.state ?? "unknown"}/${lane.classification}, last signal ${age}, agent=${agent}, model=${model}, verdict=${verdict}${issue}`);
1701
+ lines.push(` - lane ${lane.laneId}: ${lane.state ?? "unknown"}/${lane.classification}`);
1702
+ lines.push(` task: ${task}`);
1703
+ lines.push(` prompt: ${prompt}`);
1704
+ lines.push(` agent: ${agent}`);
1705
+ lines.push(` model: ${model}`);
1706
+ lines.push(` progress: ${progress}`);
1707
+ lines.push(` result: ${resultQuality}`);
1708
+ lines.push(` last signal: ${age}; nudges=${nudge}; verdict=${verdict}${issue}`);
1550
1709
  }
1551
1710
  }
1552
1711
  if (summary.autoAbortSummaries !== undefined && summary.autoAbortSummaries.length > 0) {
@@ -2074,6 +2233,32 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2074
2233
  return undefined;
2075
2234
  const client = input.client;
2076
2235
  const rootDir = input.durableStateRoot;
2236
+ const promptPreview = (text, max = 120) => {
2237
+ const compact = text.replace(/\s+/g, " ").trim();
2238
+ return compact.length > max ? `${compact.slice(0, max - 1)}…` : compact;
2239
+ };
2240
+ const taskSummaryForUser = (input) => {
2241
+ const lines = [
2242
+ input.status === "task_completed"
2243
+ ? "FlowDesk task completed."
2244
+ : input.status === "task_launched"
2245
+ ? "FlowDesk task launched."
2246
+ : "FlowDesk task failed.",
2247
+ `workflow: ${input.workflowId}`,
2248
+ `lane: ${input.laneId}`,
2249
+ `task: ${input.taskId}`,
2250
+ `prompt: ${promptPreview(input.promptText)}`,
2251
+ `agent: ${input.agentName}`,
2252
+ `model: ${input.providerQualifiedModelId}`,
2253
+ ];
2254
+ if (input.asyncMode === true)
2255
+ lines.push("progress: use /flowdesk-status or flowdesk_status_live to follow durable lane evidence");
2256
+ if (input.resultText !== undefined)
2257
+ lines.push(`result: ${promptPreview(input.resultText, 200)}`);
2258
+ if (input.failureCategory !== undefined)
2259
+ lines.push(`failure: ${input.failureCategory}${input.redactedReason === undefined ? "" : ` (${input.redactedReason})`}`);
2260
+ return lines.join("\n");
2261
+ };
2077
2262
  return {
2078
2263
  [flowdeskAgentTaskRunToolName]: tool({
2079
2264
  description: [
@@ -2081,7 +2266,7 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2081
2266
  "Use this to delegate a well-defined subtask to a specific model (e.g. Claude Opus for security analysis, GPT for architecture review).",
2082
2267
  "Requires developerModeAcknowledged=true and allowProviderCall=true per call.",
2083
2268
  "WHEN TO USE: user asks to delegate a specific task to a specific model/agent.",
2084
- "WHEN NOT TO USE: multi-step workflows (use flowdesk_workflow_dispatch), code review (use flowdesk_quick_reviewer_run).",
2269
+ "WHEN NOT TO USE: multi-step workflows (use flowdesk_workflow_dispatch).",
2085
2270
  "After calling, use flowdesk_status_live to check the lane status.",
2086
2271
  ].join(" "),
2087
2272
  args: {
@@ -2092,6 +2277,8 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2092
2277
  parentSessionId: tool.schema.string().optional().describe("Parent session id"),
2093
2278
  developerModeAcknowledged: tool.schema.boolean(),
2094
2279
  allowProviderCall: tool.schema.boolean(),
2280
+ nudgeQuietPeriodMs: tool.schema.number().optional().describe("Milliseconds of silence before sending a nudge prompt. Default 10000ms (10s). Recommended: always pass 10000. At 10s silence → nudge 1, 20s → nudge 2, 30s+ → lane fails and watchdog retries."),
2281
+ asyncMode: tool.schema.boolean().optional().describe("When true, return laneId immediately after launch. Watchdog polls child session, sends noReply nudges at 10s/20s, and aborts at 30s+. Coordinator uses flowdesk_status_live to detect completion. Recommended for all orchestration calls."),
2095
2282
  },
2096
2283
  async execute(args, ctx) {
2097
2284
  const record = isRecord(args) ? args : {};
@@ -2111,6 +2298,9 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2111
2298
  : typeof ctxRecord.sessionID === "string" && ctxRecord.sessionID.length > 0
2112
2299
  ? ctxRecord.sessionID
2113
2300
  : "";
2301
+ const nudgeQuietPeriodMs = typeof record.nudgeQuietPeriodMs === "number" && record.nudgeQuietPeriodMs > 0
2302
+ ? Math.floor(record.nudgeQuietPeriodMs) : undefined;
2303
+ const asyncMode = record.asyncMode === true;
2114
2304
  const taskId = `task-${Date.now().toString(36)}`;
2115
2305
  const laneId = `lane-task-${Date.now().toString(36)}`;
2116
2306
  const result = await executeFlowDeskAgentTaskV1({
@@ -2123,19 +2313,37 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2123
2313
  parentSessionId,
2124
2314
  rootDir,
2125
2315
  client,
2316
+ asyncMode,
2317
+ _nudgeQuietPeriodMs: nudgeQuietPeriodMs,
2126
2318
  });
2319
+ const failureCategory = result.status === "task_failed" ? result.failureCategory : undefined;
2320
+ const redactedReason = result.status === "task_failed" ? result.redactedReason : undefined;
2127
2321
  return JSON.stringify({
2128
2322
  workflowId,
2129
2323
  laneId,
2130
2324
  taskId,
2131
2325
  status: result.status,
2326
+ taskPreview: promptPreview(taskDescription),
2327
+ agentName,
2328
+ providerQualifiedModelId,
2329
+ ...(result.status === "task_launched" ? { childSessionId: result.childSessionId, asyncMode: true, safeNextActions: ["/flowdesk-status"] } : {}),
2132
2330
  resultText: result.status === "task_completed" ? result.resultText.slice(0, 4_096) : undefined,
2133
2331
  resultTruncated: result.status === "task_completed" && result.resultText.length > 4_096,
2134
- failureCategory: result.status === "task_failed" ? result.failureCategory : undefined,
2135
- redactedReason: result.status === "task_failed" ? result.redactedReason : undefined,
2136
- summaryForUser: result.status === "task_completed"
2137
- ? `Task completed on ${agentName} (${providerQualifiedModelId}). Result: ${result.resultText.slice(0, 200)}${result.resultText.length > 200 ? "..." : ""}`
2138
- : `Task failed on ${agentName}: ${result.failureCategory}`,
2332
+ failureCategory,
2333
+ redactedReason,
2334
+ summaryForUser: taskSummaryForUser({
2335
+ status: result.status,
2336
+ workflowId,
2337
+ laneId,
2338
+ taskId,
2339
+ agentName,
2340
+ providerQualifiedModelId,
2341
+ promptText: taskDescription,
2342
+ ...(result.status === "task_completed" ? { resultText: result.resultText } : {}),
2343
+ ...(result.status === "task_launched" ? { asyncMode: true } : {}),
2344
+ ...(failureCategory === undefined ? {} : { failureCategory }),
2345
+ ...(redactedReason === undefined ? {} : { redactedReason }),
2346
+ }),
2139
2347
  });
2140
2348
  },
2141
2349
  }),
@@ -2317,6 +2525,8 @@ function providerUsageLiveConfigFromOptions(options) {
2317
2525
  config.geminiProjectId = value.geminiProjectId;
2318
2526
  if (value.persistSnapshots === true)
2319
2527
  config.persistSnapshots = true;
2528
+ if (value.appendToChat === true)
2529
+ config.appendToChat = true;
2320
2530
  const explicitRoot = typeof value.durableStateRootDir === "string" &&
2321
2531
  value.durableStateRootDir.trim().length > 0
2322
2532
  ? value.durableStateRootDir
@@ -2474,6 +2684,67 @@ function controlledWriteApplyConfigFromOptions(input, options) {
2474
2684
  const workspaceRoot = optionWorkspaceRoot ?? inputWorkspaceRoot ?? process.cwd();
2475
2685
  return { durableStateRoot, workspaceRoot };
2476
2686
  }
2687
+ export const flowdeskOrchestrateToolName = "flowdesk_orchestrate";
2688
+ export const flowdeskOrchestrateOption = "workflowOrchestrate";
2689
+ function orchestrateToolConfigFromOptions(input, options) {
2690
+ const value = options?.[flowdeskOrchestrateOption];
2691
+ if (!isRecord(value) || value.enabled !== true || value.devBetaActualLaneLaunch !== true)
2692
+ return undefined;
2693
+ const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0 ? value.rootDir : undefined;
2694
+ const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
2695
+ if (rootDir === undefined)
2696
+ return undefined;
2697
+ const client = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
2698
+ return client === undefined ? undefined : { rootDir, client };
2699
+ }
2700
+ function createFlowDeskOrchestrateOptInTools(config) {
2701
+ return {
2702
+ [flowdeskOrchestrateToolName]: tool({
2703
+ description: [
2704
+ "Run a full FlowDesk multi-task orchestration: Author → Assign → Schedule → Synthesize.",
2705
+ "WHEN TO USE: the user gives a natural-language goal that requires decomposing into multiple tasks, assigning agents/models based on provider usage, executing them in dependency order, and aggregating results.",
2706
+ "INVOKE WITH: goalSummary (clear bounded goal text), parentSessionId, and optional workflowId.",
2707
+ "AFTER CALLING: surface summaryForUser and report synthesisId, taskCount, conflictDetected. All authority flags remain false.",
2708
+ "WHEN NOT TO USE: simple single-task requests, status checks, usage queries, or review fan-out.",
2709
+ ].join(" "),
2710
+ args: {
2711
+ goalSummary: tool.schema.string().describe("Bounded natural-language goal for the workflow. Max 500 chars."),
2712
+ parentSessionId: tool.schema.string().describe("Current OpenCode session id (ses-...)."),
2713
+ workflowId: tool.schema.string().optional().describe("Optional stable workflow id. Auto-generated when omitted."),
2714
+ providerQualifiedModelId: tool.schema.string().optional().describe("Concrete provider/model id for author and synthesis calls. Defaults to openai/gpt-5.5."),
2715
+ agentName: tool.schema.string().optional().describe("Agent name for author and synthesis calls. Defaults to reviewer-gpt-frontier."),
2716
+ developerModeAcknowledged: tool.schema.boolean().describe("Must be true to allow provider calls."),
2717
+ allowProviderCall: tool.schema.boolean().describe("Must be true to allow provider calls."),
2718
+ allowActualLaneLaunch: tool.schema.boolean().describe("Must be true to allow actual lane launch."),
2719
+ },
2720
+ async execute(input) {
2721
+ if (!isRecord(input))
2722
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "invalid input", safeNextActions: ["/flowdesk-doctor"] });
2723
+ if (input.developerModeAcknowledged !== true || input.allowProviderCall !== true || input.allowActualLaneLaunch !== true)
2724
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "developerModeAcknowledged, allowProviderCall, and allowActualLaneLaunch must all be true", safeNextActions: ["/flowdesk-doctor"] });
2725
+ const goalSummary = typeof input.goalSummary === "string" ? input.goalSummary.slice(0, 500) : "";
2726
+ if (!goalSummary.trim())
2727
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "goalSummary is required", safeNextActions: ["/flowdesk-doctor"] });
2728
+ const parentSessionId = typeof input.parentSessionId === "string" ? input.parentSessionId : "";
2729
+ if (!parentSessionId.trim())
2730
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "parentSessionId is required", safeNextActions: ["/flowdesk-doctor"] });
2731
+ const providerQualifiedModelId = typeof input.providerQualifiedModelId === "string" ? input.providerQualifiedModelId : "openai/gpt-5.5";
2732
+ const agentName = typeof input.agentName === "string" ? input.agentName : "reviewer-gpt-frontier";
2733
+ const workflowId = typeof input.workflowId === "string" ? input.workflowId : undefined;
2734
+ const result = await executeFlowDeskWorkflowOrchestratorV1({
2735
+ workflowId,
2736
+ goalSummary,
2737
+ parentSessionId,
2738
+ rootDir: config.rootDir,
2739
+ client: config.client,
2740
+ providerQualifiedModelId,
2741
+ agentName,
2742
+ });
2743
+ return JSON.stringify(result);
2744
+ },
2745
+ }),
2746
+ };
2747
+ }
2477
2748
  export function createFlowDeskWorkflowDispatchPlanOptInTools(config) {
2478
2749
  return {
2479
2750
  [flowdeskWorkflowDispatchPlanToolName]: tool({
@@ -2516,6 +2787,33 @@ export function createFlowDeskWorkflowDispatchPlanOptInTools(config) {
2516
2787
  }),
2517
2788
  };
2518
2789
  }
2790
+ export function createFlowDeskAutoContinuePreviewOptInTools(config) {
2791
+ return {
2792
+ [flowdeskAutoContinuePreviewToolName]: tool({
2793
+ description: [
2794
+ "Preview the next pending task from durable FlowDesk workflow_dispatch_plan evidence without executing it.",
2795
+ "WHEN TO USE: the user asks whether FlowDesk can continue remaining planned work or asks what would run next from a durable plan.",
2796
+ "WHEN NOT TO USE: requests to actually dispatch, call a provider, launch a lane, fallback/reselect, mutate TUI/chat, or write files. This tool is preview-only and never performs continuation.",
2797
+ "INVOKE WITH: workflowId and optional maxSteps. The configured durable state root is used. Todo source is durable workflow_dispatch_plan evidence only, not transient chat/todowrite state.",
2798
+ "AFTER CALLING: surface summaryForUser, nextTaskId/title/summary, pendingTaskCount, and authority. Never claim automatic execution, dispatch, provider call, fallback, hard chat, or TUI action authority.",
2799
+ ].join(" "),
2800
+ args: {
2801
+ workflowId: tool.schema.string().describe("Workflow id with durable workflow_dispatch_plan evidence."),
2802
+ maxSteps: tool.schema.number().optional().describe("Bounded preview cap. Clamped to 1..5; no steps are executed."),
2803
+ },
2804
+ async execute(input) {
2805
+ const result = executeFlowDeskAutoContinuePreviewToolV1({
2806
+ config,
2807
+ request: {
2808
+ workflowId: typeof input.workflowId === "string" ? input.workflowId : undefined,
2809
+ maxSteps: typeof input.maxSteps === "number" ? input.maxSteps : undefined,
2810
+ },
2811
+ });
2812
+ return JSON.stringify(result);
2813
+ },
2814
+ }),
2815
+ };
2816
+ }
2519
2817
  export function createFlowDeskWorkflowDispatchOptInTools(config) {
2520
2818
  return {
2521
2819
  [flowdeskWorkflowDispatchToolName]: tool({
@@ -2769,6 +3067,134 @@ export function createFlowDeskStatusLiveOptInTools(config) {
2769
3067
  }),
2770
3068
  };
2771
3069
  }
3070
+ export function createFlowDeskWorkflowSynthesisPreviewTools(config) {
3071
+ return {
3072
+ [flowdeskWorkflowSynthesisPreviewToolName]: tool({
3073
+ description: [
3074
+ "Build and persist a provider-free FlowDesk synthesis preview from existing task_result evidence. This local tool never calls providers, launches lanes, dispatches runtime work, performs fallback/reselection, writes workspace files, or grants execution authority.",
3075
+ "WHEN TO USE: a workflow reports next_action=synthesis_ready and the user asks to summarize, synthesize, continue with the next synthesis step, or see the completed task results without making a model/provider call.",
3076
+ "WHEN NOT TO USE: requests for model-authored synthesis, real dispatch, provider calls, write/apply, fallback/retry, or replacing raw task results.",
3077
+ "INVOKE WITH: workflowId for a workflow that already has task_result evidence under the configured durable root.",
3078
+ "AFTER CALLING: surface summaryForUser, synthesisId, tasksSummarized, conflictDetected, safeNextActions, and authority. State that this is a provider-free preview derived from durable task_result evidence.",
3079
+ ].join(" "),
3080
+ args: {
3081
+ workflowId: tool.schema.string().describe("Workflow id with existing task_result evidence to synthesize locally."),
3082
+ },
3083
+ async execute(input) {
3084
+ const workflowId = isRecord(input) && typeof input.workflowId === "string" ? input.workflowId : "";
3085
+ const result = executeFlowDeskWorkflowSynthesisPreviewV1({ workflowId, rootDir: config.rootDir });
3086
+ return JSON.stringify(result);
3087
+ },
3088
+ }),
3089
+ };
3090
+ }
3091
+ function isFlowDeskUiProbeEnabled(options) {
3092
+ const raw = options?.[flowdeskUiProbeOption];
3093
+ return process.env.FLOWDESK_UI_PROBE === "1" || raw === true || (isRecord(raw) && raw.enabled === true);
3094
+ }
3095
+ function boundedUiProbeString(value, maxLength = 160) {
3096
+ if (typeof value !== "string" || value.length === 0)
3097
+ return undefined;
3098
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
3099
+ }
3100
+ function summarizeFlowDeskUiProbeEventV1(event, observedAt) {
3101
+ const rec = isRecord(event) ? event : {};
3102
+ const properties = Object.keys(rec).sort().slice(0, 20);
3103
+ const nested = (name) => {
3104
+ const value = rec[name];
3105
+ return isRecord(value) ? value : undefined;
3106
+ };
3107
+ const session = nested("session") ?? nested("sessionID") ?? nested("sessionId");
3108
+ const message = nested("message");
3109
+ const part = nested("part");
3110
+ const sessionId = boundedUiProbeString(rec.sessionID) ??
3111
+ boundedUiProbeString(rec.sessionId) ??
3112
+ boundedUiProbeString(session?.id);
3113
+ return {
3114
+ observedAt,
3115
+ eventType: boundedUiProbeString(rec.type, 120) ?? boundedUiProbeString(rec.event, 120) ?? "unknown",
3116
+ ...(sessionId === undefined ? {} : { sessionId }),
3117
+ ...(boundedUiProbeString(message?.id) === undefined ? {} : { messageId: boundedUiProbeString(message?.id) }),
3118
+ ...(boundedUiProbeString(part?.id) === undefined ? {} : { partId: boundedUiProbeString(part?.id) }),
3119
+ keys: properties,
3120
+ };
3121
+ }
3122
+ function createFlowDeskUiProbeTools(observations) {
3123
+ return {
3124
+ [flowdeskUiProbeToolName]: tool({
3125
+ description: [
3126
+ "Developer-only FlowDesk UI renderer/event probe. Enabled only when FLOWDESK_UI_PROBE=1.",
3127
+ "It emits bounded markdown/details/link/code-fence/long-log samples and reports redacted OpenCode event observations captured by the plugin event hook.",
3128
+ "No dispatch, provider call, write authority, fallback authority, or hard chat control is granted.",
3129
+ ].join(" "),
3130
+ args: {
3131
+ mode: tool.schema
3132
+ .string()
3133
+ .optional()
3134
+ .describe("Probe mode: render, events, or all. Defaults to all."),
3135
+ limit: tool.schema
3136
+ .number()
3137
+ .optional()
3138
+ .describe("Maximum recent event observations to return; clamped to 1..50."),
3139
+ },
3140
+ async execute(input) {
3141
+ const mode = isRecord(input) && typeof input.mode === "string" ? input.mode : "all";
3142
+ const requestedLimit = isRecord(input) && typeof input.limit === "number" ? Math.floor(input.limit) : 20;
3143
+ const limit = Math.max(1, Math.min(50, requestedLimit));
3144
+ const includeRender = mode === "all" || mode === "render";
3145
+ const includeEvents = mode === "all" || mode === "events";
3146
+ const longLog = Array.from({ length: 40 }, (_, index) => {
3147
+ const n = String(index + 1).padStart(2, "0");
3148
+ return `${n}. ui-probe timeline event ${n}: message/status/log renderer sample`;
3149
+ });
3150
+ return JSON.stringify({
3151
+ schema_version: "flowdesk.ui_probe_result.v1",
3152
+ status: "ui_probe_collected",
3153
+ probeEnabled: true,
3154
+ mode,
3155
+ authority: {
3156
+ realOpenCodeDispatch: false,
3157
+ providerCall: false,
3158
+ runtimeExecution: false,
3159
+ actualLaneLaunch: false,
3160
+ fallbackAuthority: false,
3161
+ hardCancelOrNoReplyAuthority: false,
3162
+ },
3163
+ ...(includeRender
3164
+ ? {
3165
+ renderSamples: {
3166
+ markdownDetails: [
3167
+ "<details>",
3168
+ "<summary>FlowDesk UI probe: details summary</summary>",
3169
+ "",
3170
+ "[FlowDesk markdown link probe](https://example.com/flowdesk-ui-probe)",
3171
+ "",
3172
+ "```text",
3173
+ ...longLog.slice(0, 8),
3174
+ "```",
3175
+ "</details>",
3176
+ ].join("\n"),
3177
+ longLogText: ["FlowDesk UI probe long log", ...longLog].join("\n"),
3178
+ observationInstructions: [
3179
+ "1. Check whether markdownDetails renders as collapsible UI or plain text.",
3180
+ "2. Click the markdown link and re-run mode=events to see whether any click/navigation event reached Hooks.event.",
3181
+ "3. Check whether longLogText is folded, scrollable, or fully expanded by OpenCode's tool-output renderer.",
3182
+ ].join("\n"),
3183
+ },
3184
+ }
3185
+ : {}),
3186
+ ...(includeEvents
3187
+ ? {
3188
+ recentEventObservations: observations.slice(-limit),
3189
+ recentEventObservationCount: Math.min(observations.length, limit),
3190
+ totalBufferedEventObservationCount: observations.length,
3191
+ }
3192
+ : {}),
3193
+ });
3194
+ },
3195
+ }),
3196
+ };
3197
+ }
2772
3198
  export function createFlowDeskProviderUsageLiveOptInTools(config) {
2773
3199
  return {
2774
3200
  [flowdeskProviderUsageLiveToolName]: tool({
@@ -2778,7 +3204,7 @@ export function createFlowDeskProviderUsageLiveOptInTools(config) {
2778
3204
  "ALSO PROACTIVELY USE: before starting a large multi-step task that depends on a specific provider (e.g. extensive refactor, long agentic loop, multi-perspective review), call this tool first to check whether the chosen provider has enough headroom; if worstAlertLevel is critical or exhausted, warn the user and suggest switching providers or waiting for reset.",
2779
3205
  "WHEN NOT TO USE: general chat, status of an in-progress workflow (use status instead), or any non-usage question.",
2780
3206
  "INVOKE WITH: optional providerFamily ('claude', 'openai', 'gemini', or 'all'; default 'all'). The plugin user has already opted in to provider-native usage collection at configuration time, so this tool can be called automatically without per-call confirmation.",
2781
- "AFTER CALLING: summarize per-provider availability for the user in plain language. For each provider include the bucket label (claude-5h, claude-weekly, openai-gpt-5h, gemini-pro-5h, gemini-pro-weekly, gemini-flash-daily, gemini-flash-lite-daily), remainingPercent, reset time, alertLevel, and recommendation. If any provider returned non_dispatchable, exhausted, critical, stale, or unknown, note it explicitly and surface the overallRecommendation. Never echo raw tokens or raw payloads.",
3207
+ "AFTER CALLING: summarize per-provider availability for the user in plain language. For each provider include the bucket label (claude-5h, claude-weekly, openai-gpt-5h, gemini-pro-daily, gemini-pro-weekly, gemini-flash-daily, gemini-flash-lite-daily), remainingPercent, reset time, alertLevel, and recommendation. If any provider returned non_dispatchable, exhausted, critical, stale, or unknown, note it explicitly and surface the overallRecommendation. Never echo raw tokens or raw payloads.",
2782
3208
  ].join(" "),
2783
3209
  args: {
2784
3210
  providerFamily: tool.schema
@@ -3076,8 +3502,10 @@ const flowdeskServerPlugin = async (input, options) => {
3076
3502
  const statusLiveConfig = isStatusLiveEnabled(options)
3077
3503
  ? statusLiveConfigFromOptions(options)
3078
3504
  : undefined;
3079
- if (statusLiveConfig !== undefined)
3505
+ if (statusLiveConfig !== undefined) {
3080
3506
  Object.assign(tools, createFlowDeskStatusLiveOptInTools(statusLiveConfig));
3507
+ Object.assign(tools, createFlowDeskWorkflowSynthesisPreviewTools({ rootDir: statusLiveConfig.rootDir }));
3508
+ }
3081
3509
  const quickFallbackRunConfig = isQuickFallbackRunEnabled(options)
3082
3510
  ? quickFallbackRunConfigFromOptions(options)
3083
3511
  : undefined;
@@ -3093,6 +3521,8 @@ const flowdeskServerPlugin = async (input, options) => {
3093
3521
  : undefined;
3094
3522
  if (workflowDispatchPlanConfig !== undefined)
3095
3523
  Object.assign(tools, createFlowDeskWorkflowDispatchPlanOptInTools(workflowDispatchPlanConfig));
3524
+ if (workflowDispatchPlanConfig !== undefined)
3525
+ Object.assign(tools, createFlowDeskAutoContinuePreviewOptInTools(workflowDispatchPlanConfig));
3096
3526
  const workflowDispatchConfig = workflowDispatchToolConfigFromOptions(input, options);
3097
3527
  if (workflowDispatchConfig !== undefined)
3098
3528
  Object.assign(tools, createFlowDeskWorkflowDispatchOptInTools(workflowDispatchConfig));
@@ -3110,6 +3540,13 @@ const flowdeskServerPlugin = async (input, options) => {
3110
3540
  if (agentTaskRunTools !== undefined)
3111
3541
  Object.assign(tools, agentTaskRunTools);
3112
3542
  }
3543
+ const orchestrateConfig = orchestrateToolConfigFromOptions(input, options);
3544
+ if (orchestrateConfig !== undefined)
3545
+ Object.assign(tools, createFlowDeskOrchestrateOptInTools(orchestrateConfig));
3546
+ const uiProbeEnabled = isFlowDeskUiProbeEnabled(options);
3547
+ const uiProbeEventObservations = [];
3548
+ if (uiProbeEnabled)
3549
+ Object.assign(tools, createFlowDeskUiProbeTools(uiProbeEventObservations));
3113
3550
  // P8 Background Watchdog
3114
3551
  const watchdogConfig = watchdogConfigFromOptions(options);
3115
3552
  const chatStallAlertRaw = options?.[flowdeskChatMessageStallAlertOption];
@@ -3210,14 +3647,54 @@ const flowdeskServerPlugin = async (input, options) => {
3210
3647
  },
3211
3648
  });
3212
3649
  }
3650
+ const eventRootDir = durableStateRootFromOptions(options);
3651
+ const eventHook = eventRootDir === undefined && !uiProbeEnabled
3652
+ ? undefined
3653
+ : async (input) => {
3654
+ if (uiProbeEnabled) {
3655
+ uiProbeEventObservations.push(summarizeFlowDeskUiProbeEventV1(input.event, new Date().toISOString()));
3656
+ if (uiProbeEventObservations.length > 200)
3657
+ uiProbeEventObservations.splice(0, uiProbeEventObservations.length - 200);
3658
+ }
3659
+ if (eventRootDir !== undefined) {
3660
+ await observeFlowDeskOpenCodeEventV1({ rootDir: eventRootDir, event: input.event });
3661
+ }
3662
+ };
3213
3663
  if (!naturalLanguageRoutingEnabled)
3214
- return { tool: tools };
3664
+ return eventHook === undefined ? { tool: tools } : { tool: tools, event: eventHook };
3215
3665
  const stallAlertOption = chatMessageStallAlertOptionsFrom(options, statusLiveConfig, isRecord(input) && isManagedDispatchBetaClient(input.client)
3216
3666
  ? input.client
3217
3667
  : undefined);
3668
+ // Background sidebar cache refresh interval — runs every 3 minutes regardless of chat activity
3669
+ if (providerUsageLiveConfig?.durableStateRootDir) {
3670
+ const capturedUsageConfig = providerUsageLiveConfig;
3671
+ const sidebarRefreshIntervalMs = 3 * 60_000;
3672
+ const sidebarRefreshInterval = setInterval(() => {
3673
+ const cachePath = join(capturedUsageConfig.durableStateRootDir, ".flowdesk", "ui", "provider-usage-sidebar.json");
3674
+ let isStale = false;
3675
+ try {
3676
+ const cache = JSON.parse(readFileSync(cachePath, "utf8"));
3677
+ if (typeof cache.observed_at === "string" && Date.now() - Date.parse(cache.observed_at) > sidebarRefreshIntervalMs)
3678
+ isStale = true;
3679
+ }
3680
+ catch {
3681
+ isStale = true;
3682
+ }
3683
+ if (isStale) {
3684
+ executeFlowDeskProviderUsageLiveV1({
3685
+ config: { ...capturedUsageConfig, persistSidebarCache: true },
3686
+ request: { providerFamily: "all" },
3687
+ }).catch(() => { });
3688
+ }
3689
+ }, sidebarRefreshIntervalMs);
3690
+ sidebarRefreshInterval.unref();
3691
+ process.once("exit", () => clearInterval(sidebarRefreshInterval));
3692
+ process.once("SIGTERM", () => clearInterval(sidebarRefreshInterval));
3693
+ }
3218
3694
  return {
3219
3695
  tool: tools,
3220
- "chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption, durableStateRootFromOptions(options)),
3696
+ ...(eventHook === undefined ? {} : { event: eventHook }),
3697
+ "chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption, durableStateRootFromOptions(options), providerUsageLiveConfig),
3221
3698
  };
3222
3699
  };
3223
3700
  export const flowdeskChatMessageStallAlertOption = "chatMessageStallAlert";