@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
package/dist/server.js CHANGED
@@ -4,15 +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
10
  import { createFlowDeskManagedDispatchBetaDurableReservationStoreV1, createFlowDeskOpenCodeMetadataProviderAcquisitionClientV1, createFlowDeskOpenCodePromptBackedProviderAcquisitionClientV1, dispatchManagedDispatchBetaPromptV1, 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";
18
+ import { executeFlowDeskControlledWriteApplyToolV1, } from "./controlled-write-tool.js";
15
19
  import { executeFlowDeskStatusLiveV1, } from "./status-live-tool.js";
20
+ import { executeFlowDeskWorkflowDispatchPlanToolV1, } from "./workflow-dispatch-plan-tool.js";
21
+ import { executeFlowDeskWorkflowDispatchToolV1, } from "./workflow-dispatch-tool.js";
22
+ import { executeFlowDeskWorkflowOrchestratorV1 } from "./workflow-orchestrator.js";
23
+ import { executeFlowDeskWorkflowSynthesisPreviewV1 } from "./workflow-synthesis-tool.js";
16
24
  import { evaluateGuardedAutoAbortHookV1, evaluateGuardedAutoRetryHookV1, reconcileStalePendingRetryPlansV1, checkSdkSessionApiHealthV1, runFlowDeskWatchdogCycleV1, } from "./stall-recovery.js";
17
25
  import { withTimeout, FlowDeskTimeoutError } from "./shared/with-timeout.js";
18
26
  import { FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS, getFlowDeskRelease1HandlerReadinessSummary, getFlowDeskRelease1ProductionReadinessSummary, hasPassingFds1SchemaConversionSpike, runFlowDeskPreSpikePluginToolStub, } from "./tool-stubs.js";
@@ -34,6 +42,10 @@ export const flowdeskProviderUsageLiveOption = "providerUsageLive";
34
42
  export const flowdeskStatusLiveOption = "statusLive";
35
43
  export const flowdeskQuickFallbackRunOption = "quickFallbackRun";
36
44
  export const flowdeskLaneHeartbeatWriterOption = "laneHeartbeatWriter";
45
+ export const flowdeskWorkflowDispatchPlanToolOption = "workflowDispatchPlanTool";
46
+ export const flowdeskWorkflowDispatchOption = "workflowDispatch";
47
+ export const flowdeskControlledWriteApplyOption = "controlledWriteApply";
48
+ export const flowdeskUiProbeOption = "uiProbe";
37
49
  export const flowdeskDefaultManagedDispatchAuthorizationOption = "defaultManagedDispatchAuthorization";
38
50
  export const flowdeskWatchdogOption = "watchdog";
39
51
  export const flowdeskWatchdogTriggerToolName = "flowdesk_watchdog_trigger";
@@ -46,8 +58,14 @@ export const flowdeskProviderUsageLiveToolName = "flowdesk_provider_usage_live";
46
58
  export const flowdeskStatusLiveToolName = "flowdesk_status_live";
47
59
  export const flowdeskQuickFallbackRunToolName = "flowdesk_quick_fallback_run";
48
60
  export const flowdeskLaneHeartbeatWriterToolName = "flowdesk_lane_heartbeat_record";
61
+ export const flowdeskWorkflowDispatchPlanToolName = "flowdesk_workflow_dispatch_plan";
62
+ export const flowdeskWorkflowDispatchToolName = "flowdesk_workflow_dispatch";
63
+ export const flowdeskControlledWriteApplyToolName = "flowdesk_controlled_write_apply";
49
64
  export const flowdeskAgentTaskRunOption = "agentTaskRun";
50
65
  export const flowdeskAgentTaskRunToolName = "flowdesk_agent_task_run";
66
+ export const flowdeskWorkflowSynthesisPreviewToolName = "flowdesk_workflow_synthesis_preview";
67
+ export const flowdeskAutoContinuePreviewToolName = "flowdesk_auto_continue_preview";
68
+ export const flowdeskUiProbeToolName = "flowdesk_ui_probe";
51
69
  const flowdeskChatSuggestionDuplicateWindowMs = 10_000;
52
70
  const disabledAuthority = {
53
71
  productionRegistrationEligible: false,
@@ -1166,9 +1184,11 @@ export function createFlowDeskExactModelProviderAcquisitionLiveTestOptInTools(cl
1166
1184
  }),
1167
1185
  };
1168
1186
  }
1169
- export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert, durableSuggestionRoot) {
1187
+ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Date(), session = createFlowDeskLocalNonDispatchAdapterSession(now), stallAlert, durableSuggestionRoot, providerUsageLiveConfig) {
1170
1188
  const recentSuggestionCards = new Map();
1171
1189
  const recentStallAlerts = new Map();
1190
+ const usageAutoRefreshMaxAgeMs = 3 * 60_000;
1191
+ let lastUsageRefreshAttemptAtMs = 0;
1172
1192
  return async function message(input, output) {
1173
1193
  const inputRecord = isRecord(input) ? input : {};
1174
1194
  const partSessionID = typeof inputRecord.sessionID === "string" ? inputRecord.sessionID : "";
@@ -1183,6 +1203,40 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1183
1203
  const request = intakeRequestFromChatMessage({ ...inputRecord, ...output });
1184
1204
  const preview = previewNaturalLanguageRouting(request, session);
1185
1205
  const nowMs = clockMs(now);
1206
+ if (providerUsageLiveConfig?.durableStateRootDir && nowMs - lastUsageRefreshAttemptAtMs > 30_000) {
1207
+ lastUsageRefreshAttemptAtMs = nowMs;
1208
+ let isStale = false;
1209
+ try {
1210
+ const cachePath = join(providerUsageLiveConfig.durableStateRootDir, ".flowdesk", "ui", "provider-usage-sidebar.json");
1211
+ const cacheContent = JSON.parse(readFileSync(cachePath, "utf8"));
1212
+ if (typeof cacheContent.observed_at === "string" && nowMs - Date.parse(cacheContent.observed_at) > usageAutoRefreshMaxAgeMs)
1213
+ isStale = true;
1214
+ }
1215
+ catch {
1216
+ isStale = true;
1217
+ }
1218
+ if (isStale) {
1219
+ try {
1220
+ await executeFlowDeskProviderUsageLiveV1({ config: { ...providerUsageLiveConfig, persistSidebarCache: true }, request: { providerFamily: "all" } });
1221
+ }
1222
+ catch { }
1223
+ }
1224
+ }
1225
+ const usageTextToAppend = providerUsageLiveConfig?.durableStateRootDir &&
1226
+ providerUsageLiveConfig.appendToChat === true
1227
+ ? formatFlowDeskTuiUsageSnapshotCompactText(loadFlowDeskTuiUsageSnapshotViewV1({
1228
+ rootDir: providerUsageLiveConfig.durableStateRootDir,
1229
+ workflowId: providerUsageLiveConfig.persistWorkflowId,
1230
+ now: () => (typeof now === "function" ? now() : now),
1231
+ }))
1232
+ : undefined;
1233
+ const appendUsageCard = () => {
1234
+ if (usageTextToAppend === undefined)
1235
+ return;
1236
+ if (!Array.isArray(output.parts))
1237
+ output.parts = [];
1238
+ output.parts.push(buildTextPart(usageTextToAppend));
1239
+ };
1186
1240
  for (const [key, recordedAtMs] of recentSuggestionCards) {
1187
1241
  if (nowMs - recordedAtMs > flowdeskChatSuggestionDuplicateWindowMs ||
1188
1242
  nowMs < recordedAtMs)
@@ -1194,7 +1248,9 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1194
1248
  recentStallAlerts.delete(key);
1195
1249
  }
1196
1250
  const stallResult = stallAlert
1197
- ? await collectStallAlertResult(stallAlert, now)
1251
+ ? await collectStallAlertResult(stallAlert, now, {
1252
+ currentSessionRef: partSessionID || request.session_ref,
1253
+ })
1198
1254
  : { status: "none" };
1199
1255
  let stallTextToAppend = undefined;
1200
1256
  let stallDedupKey = undefined;
@@ -1215,7 +1271,10 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1215
1271
  const lateAlertReady = stallAlert?.includeProgressingLate === true &&
1216
1272
  summary.worstClassification === "progressing_late" &&
1217
1273
  summary.totalLate > 0;
1218
- if (stalledAlertReady || lateAlertReady) {
1274
+ const progressCardReady = stallAlert?.includeProgressCards === true &&
1275
+ summary.workflowSummaries.some((workflow) => (workflow.laneCards?.length ?? 0) > 0);
1276
+ const autoNextReady = summary.workflowSummaries.some((workflow) => workflow.autoNextReady === true);
1277
+ if (stalledAlertReady || lateAlertReady || progressCardReady || autoNextReady) {
1219
1278
  stallDedupKey = stallAlertDuplicateKey(request, summary);
1220
1279
  stallTextToAppend = stallAlertText(summary);
1221
1280
  }
@@ -1233,6 +1292,7 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1233
1292
  }
1234
1293
  };
1235
1294
  if (preview.evaluation.response.route_decision === "continue_chat") {
1295
+ appendUsageCard();
1236
1296
  appendStallCard();
1237
1297
  return;
1238
1298
  }
@@ -1243,13 +1303,16 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
1243
1303
  recentSuggestionCards.set(duplicateKey, nowMs);
1244
1304
  writeDurableSuggestionPreference(durableSuggestionRoot, duplicateKey, request, preview.evaluation.response, nowMs);
1245
1305
  if (previousAtMs !== undefined &&
1246
- nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs)
1306
+ nowMs - previousAtMs <= flowdeskChatSuggestionDuplicateWindowMs) {
1307
+ appendUsageCard();
1247
1308
  return;
1309
+ }
1248
1310
  }
1249
1311
  const result = evaluateNaturalLanguageRouting(request, session);
1250
1312
  if (!Array.isArray(output.parts))
1251
1313
  output.parts = [];
1252
1314
  output.parts.push(buildTextPart(steeringText(result)));
1315
+ appendUsageCard();
1253
1316
  appendStallCard();
1254
1317
  };
1255
1318
  }
@@ -1257,6 +1320,28 @@ const ALLOWED_ERROR_NAMES = new Set(["FlowDeskDiskError", "FlowDeskStateError"])
1257
1320
  export function assertNever(x) {
1258
1321
  throw new Error("Unexpected object: " + x);
1259
1322
  }
1323
+ function sessionRefVariants(value) {
1324
+ const variants = new Set();
1325
+ if (value === undefined || value.trim().length === 0)
1326
+ return variants;
1327
+ const token = safeToken(value, "session");
1328
+ variants.add(token);
1329
+ if (token.startsWith("ses_"))
1330
+ variants.add(`ses-${token.slice(4)}`);
1331
+ if (token.startsWith("ses-"))
1332
+ variants.add(`ses_${token.slice(4)}`);
1333
+ return variants;
1334
+ }
1335
+ function sessionRefsMatch(left, right) {
1336
+ const leftVariants = sessionRefVariants(left);
1337
+ if (leftVariants.size === 0)
1338
+ return false;
1339
+ for (const candidate of sessionRefVariants(right)) {
1340
+ if (leftVariants.has(candidate))
1341
+ return true;
1342
+ }
1343
+ return false;
1344
+ }
1260
1345
  function latestParentSessionRefForLane(rootDir, workflowId, laneId) {
1261
1346
  const reload = reloadFlowDeskSessionEvidenceV1({ rootDir, workflowId });
1262
1347
  if (!reload.ok)
@@ -1310,18 +1395,36 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
1310
1395
  if (result.status !== "status_live_collected")
1311
1396
  return { status: "none" };
1312
1397
  const autoAbortSummaries = [];
1313
- const workflowSummaries = await Promise.all(result.workflows
1314
- .filter((workflow) => (workflow.stalledLaneCount ?? 0) > 0 ||
1315
- (stallAlert.includeProgressingLate === true &&
1316
- (workflow.progressingLateLaneCount ?? 0) > 0))
1317
- .slice(0, 3)
1398
+ const currentSessionRef = deps.currentSessionRef;
1399
+ const workflowSummariesWithEmpty = await Promise.all(result.workflows
1318
1400
  .map(async (workflow) => {
1319
- const stalledEntry = workflow.laneStallProjection?.entries.find((entry) => entry.classification === "stalled");
1320
- const lateEntry = workflow.laneStallProjection?.entries.find((entry) => entry.classification === "progressing_late");
1401
+ const parentRefCache = new Map();
1402
+ const latestParentRef = (laneId) => {
1403
+ if (!parentRefCache.has(laneId)) {
1404
+ parentRefCache.set(laneId, latestParentSessionRefForLane(stallAlert.rootDir, workflow.workflowId, laneId));
1405
+ }
1406
+ return parentRefCache.get(laneId);
1407
+ };
1408
+ const laneInCurrentSession = (laneId) => currentSessionRef === undefined ||
1409
+ sessionRefsMatch(latestParentRef(laneId), currentSessionRef);
1410
+ const scopedEntries = (workflow.laneStallProjection?.entries ?? []).filter((entry) => laneInCurrentSession(entry.laneId));
1411
+ const stalledEntry = scopedEntries.find((entry) => entry.classification === "stalled");
1412
+ const lateEntry = scopedEntries.find((entry) => entry.classification === "progressing_late");
1321
1413
  const primary = stalledEntry ?? lateEntry;
1414
+ const autoNextReady = workflow.laneProgressAggregate?.autoNextStepEligible === true;
1415
+ const scopedLaneCards = (workflow.laneProgressCards ?? []).filter((lane) => (autoNextReady || lane.classification !== "terminal") &&
1416
+ laneInCurrentSession(lane.laneId));
1417
+ const scopedStalledCount = scopedEntries.filter((entry) => entry.classification === "stalled").length;
1418
+ const scopedLateCount = scopedEntries.filter((entry) => entry.classification === "progressing_late").length;
1419
+ const shouldShowWorkflow = scopedStalledCount > 0 ||
1420
+ (stallAlert.includeProgressingLate === true && scopedLateCount > 0) ||
1421
+ (autoNextReady && scopedLaneCards.length > 0) ||
1422
+ (stallAlert.includeProgressCards === true && scopedLaneCards.length > 0);
1423
+ if (!shouldShowWorkflow)
1424
+ return undefined;
1322
1425
  if (stallAlert.guardedAutoAbort !== undefined &&
1323
1426
  stalledEntry !== undefined &&
1324
- (workflow.stalledLaneCount ?? 0) > 0) {
1427
+ scopedStalledCount > 0) {
1325
1428
  // Reconcile stale pending retry plans on each stall check
1326
1429
  try {
1327
1430
  reconcileStalePendingRetryPlansV1({
@@ -1383,8 +1486,8 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
1383
1486
  }
1384
1487
  return {
1385
1488
  workflowId: workflow.workflowId,
1386
- stalledLaneCount: workflow.stalledLaneCount ?? 0,
1387
- lateLaneCount: workflow.progressingLateLaneCount ?? 0,
1489
+ stalledLaneCount: scopedStalledCount,
1490
+ lateLaneCount: scopedLateCount,
1388
1491
  ...(primary?.secondsSinceLastSignal === undefined
1389
1492
  ? {}
1390
1493
  : { secondsSinceLastSignal: primary.secondsSinceLastSignal }),
@@ -1392,17 +1495,61 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
1392
1495
  ...(primary?.failureHint === undefined
1393
1496
  ? {}
1394
1497
  : { failureHint: primary.failureHint }),
1498
+ ...(workflow.laneProgressAggregate === undefined
1499
+ ? {}
1500
+ : { laneProgressAggregate: workflow.laneProgressAggregate }),
1501
+ ...(autoNextReady ? { autoNextReady: true } : {}),
1502
+ ...(workflow.latestWorkflowSynthesisTasksSummarized === undefined
1503
+ ? {}
1504
+ : { synthesisTasksSummarized: workflow.latestWorkflowSynthesisTasksSummarized }),
1505
+ ...(workflow.latestWorkflowSynthesisConflictDetected === undefined
1506
+ ? {}
1507
+ : { synthesisConflictDetected: workflow.latestWorkflowSynthesisConflictDetected }),
1508
+ ...(stallAlert.includeProgressCards === true
1509
+ ? {
1510
+ laneCards: scopedLaneCards
1511
+ .slice(0, stallAlert.maxProgressCards ?? 3)
1512
+ .map((lane) => ({
1513
+ laneId: lane.laneId,
1514
+ taskId: lane.taskId,
1515
+ state: lane.state,
1516
+ classification: lane.classification,
1517
+ secondsSinceLastSignal: lane.secondsSinceLastSignal,
1518
+ agentRef: lane.agentRef,
1519
+ providerQualifiedModelId: lane.providerQualifiedModelId,
1520
+ promptPreview: lane.promptPreview,
1521
+ nudgeCount: lane.nudgeCount,
1522
+ progressPhase: lane.progressPhase,
1523
+ progressLabel: lane.progressLabel,
1524
+ verdictLabel: lane.verdictLabel,
1525
+ completionStatus: lane.completionStatus,
1526
+ outputKind: lane.outputKind,
1527
+ usableForSynthesis: lane.usableForSynthesis,
1528
+ failureHint: lane.failureHint,
1529
+ })),
1530
+ }
1531
+ : {}),
1395
1532
  };
1396
1533
  }));
1534
+ const workflowSummaries = workflowSummariesWithEmpty
1535
+ .filter((summary) => summary !== undefined)
1536
+ .slice(0, 3);
1397
1537
  if (workflowSummaries.length === 0) {
1398
1538
  return { status: "none" };
1399
1539
  }
1540
+ const scopedTotalStalled = workflowSummaries.reduce((sum, workflow) => sum + workflow.stalledLaneCount, 0);
1541
+ const scopedTotalLate = workflowSummaries.reduce((sum, workflow) => sum + workflow.lateLaneCount, 0);
1542
+ const scopedWorstClassification = scopedTotalStalled > 0
1543
+ ? "stalled"
1544
+ : scopedTotalLate > 0
1545
+ ? "progressing_late"
1546
+ : (result.worstLaneStallClassification ?? "unknown");
1400
1547
  return {
1401
1548
  status: "ok",
1402
1549
  data: {
1403
- worstClassification: result.worstLaneStallClassification ?? "unknown",
1404
- totalStalled: result.totalStalledLaneCount ?? 0,
1405
- totalLate: result.totalProgressingLateLaneCount ?? 0,
1550
+ worstClassification: scopedWorstClassification,
1551
+ totalStalled: scopedTotalStalled,
1552
+ totalLate: scopedTotalLate,
1406
1553
  workflowSummaries,
1407
1554
  ...(autoAbortSummaries.length === 0 ? {} : { autoAbortSummaries }),
1408
1555
  }
@@ -1424,7 +1571,15 @@ function stallAlertDuplicateKey(request, summary) {
1424
1571
  const ageMinutes = typeof entry.secondsSinceLastSignal === "number"
1425
1572
  ? Math.floor(entry.secondsSinceLastSignal / 60)
1426
1573
  : -1;
1427
- return `${entry.workflowId}:${entry.stalledLaneCount}:${ageMinutes}`;
1574
+ const lanes = (entry.laneCards ?? [])
1575
+ .map((lane) => {
1576
+ const laneAge = typeof lane.secondsSinceLastSignal === "number"
1577
+ ? Math.floor(lane.secondsSinceLastSignal / 60)
1578
+ : -1;
1579
+ return `${lane.laneId}:${lane.state ?? "unknown"}:${lane.classification}:${laneAge}`;
1580
+ })
1581
+ .join(",");
1582
+ return `${entry.workflowId}:${entry.stalledLaneCount}:${ageMinutes}:${lanes}`;
1428
1583
  })
1429
1584
  .join("|");
1430
1585
  return `${safeToken(request.session_ref, "session")}|stall|${wf}|worst:${summary.worstClassification}`;
@@ -1432,12 +1587,19 @@ function stallAlertDuplicateKey(request, summary) {
1432
1587
  function stallAlertText(summary) {
1433
1588
  const lines = [];
1434
1589
  lines.push("FlowDesk");
1590
+ const progressCardCount = summary.workflowSummaries.reduce((sum, workflow) => sum + (workflow.laneCards?.length ?? 0), 0);
1435
1591
  if (summary.worstClassification === "stalled") {
1436
1592
  lines.push(`Stalled lanes detected: ${summary.totalStalled} stalled, ${summary.totalLate} progressing-late.`);
1437
1593
  }
1438
1594
  else if (summary.worstClassification === "progressing_late") {
1439
1595
  lines.push(`Late-progressing lanes detected: ${summary.totalLate} late, ${summary.totalStalled} stalled.`);
1440
1596
  }
1597
+ else if (summary.workflowSummaries.some((workflow) => workflow.autoNextReady === true)) {
1598
+ lines.push("All FlowDesk subtasks completed normally. Auto-next synthesis is ready.");
1599
+ }
1600
+ else if (progressCardCount > 0) {
1601
+ lines.push(`Lane progress: ${progressCardCount} lane(s) visible on the main screen.`);
1602
+ }
1441
1603
  else {
1442
1604
  lines.push(`Lane progress check: ${summary.totalStalled} stalled, ${summary.totalLate} progressing-late.`);
1443
1605
  }
@@ -1445,12 +1607,58 @@ function stallAlertText(summary) {
1445
1607
  const secs = workflow.secondsSinceLastSignal ?? 0;
1446
1608
  const minutes = Math.floor(secs / 60);
1447
1609
  const hint = workflow.failureHint ?? "no recent heartbeat";
1448
- const counts = workflow.stalledLaneCount > 0
1449
- ? `${workflow.stalledLaneCount} stalled`
1450
- : `${workflow.lateLaneCount} progressing-late`;
1451
- lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
1610
+ if (workflow.autoNextReady === true) {
1611
+ const synthesis = workflow.synthesisTasksSummarized === undefined
1612
+ ? "synthesis not yet recorded"
1613
+ : `synthesis recorded for ${workflow.synthesisTasksSummarized} task(s)`;
1614
+ const conflict = workflow.synthesisConflictDetected === undefined
1615
+ ? " conflict=unknown"
1616
+ : workflow.synthesisConflictDetected === true ? " conflict=true" : " conflict=false";
1617
+ const nextAction = workflow.laneProgressAggregate?.nextActionAvailable === true
1618
+ ? ` next_action=${workflow.laneProgressAggregate.nextActionKind ?? "available"}_ready`
1619
+ : "";
1620
+ lines.push(`- workflow ${workflow.workflowId}: auto-next ready (${synthesis},${conflict}${nextAction}).`);
1621
+ }
1622
+ else {
1623
+ const counts = workflow.stalledLaneCount > 0
1624
+ ? `${workflow.stalledLaneCount} stalled`
1625
+ : `${workflow.lateLaneCount} progressing-late`;
1626
+ lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
1627
+ }
1628
+ if (workflow.laneProgressAggregate !== undefined) {
1629
+ const aggregate = workflow.laneProgressAggregate;
1630
+ const nextAction = aggregate.nextActionAvailable === true
1631
+ ? `, next_action=${aggregate.nextActionKind ?? "available"}_ready`
1632
+ : "";
1633
+ lines.push(` tasks: expected=${aggregate.expected}, terminal=${aggregate.terminal}, completed=${aggregate.normalCompleted}, failed=${aggregate.failed}, awaiting_permission=${aggregate.awaitingPermission}, auto_next=${aggregate.autoNextStepEligible}${nextAction}`);
1634
+ }
1635
+ for (const lane of workflow.laneCards?.slice(0, 3) ?? []) {
1636
+ const age = lane.secondsSinceLastSignal === undefined
1637
+ ? "unknown"
1638
+ : `~${Math.floor(lane.secondsSinceLastSignal / 60)}m ago`;
1639
+ const model = lane.providerQualifiedModelId ?? "(unknown)";
1640
+ const agent = lane.agentRef ?? "(unknown)";
1641
+ const task = lane.taskId ?? lane.laneId;
1642
+ const prompt = lane.promptPreview ?? "(hidden)";
1643
+ const nudge = lane.nudgeCount === undefined ? "?" : String(lane.nudgeCount);
1644
+ const progress = lane.progressLabel === undefined
1645
+ ? "(none)"
1646
+ : `${lane.progressPhase ?? "progress"}: ${lane.progressLabel}`;
1647
+ const verdict = lane.verdictLabel ?? "(none)";
1648
+ const resultQuality = lane.completionStatus === undefined && lane.outputKind === undefined && lane.usableForSynthesis === undefined
1649
+ ? "(none)"
1650
+ : `${lane.completionStatus ?? "unknown"}/${lane.outputKind ?? "unknown"}/synthesis=${lane.usableForSynthesis === undefined ? "unknown" : String(lane.usableForSynthesis)}`;
1651
+ const issue = lane.failureHint === undefined ? "" : ` issue=${lane.failureHint}`;
1652
+ lines.push(` - lane ${lane.laneId}: ${lane.state ?? "unknown"}/${lane.classification}`);
1653
+ lines.push(` task: ${task}`);
1654
+ lines.push(` prompt: ${prompt}`);
1655
+ lines.push(` agent: ${agent}`);
1656
+ lines.push(` model: ${model}`);
1657
+ lines.push(` progress: ${progress}`);
1658
+ lines.push(` result: ${resultQuality}`);
1659
+ lines.push(` last signal: ${age}; nudges=${nudge}; verdict=${verdict}${issue}`);
1660
+ }
1452
1661
  }
1453
- lines.push("FlowDesk does not auto-retry, auto-abort, or auto-fallback on stall.");
1454
1662
  if (summary.autoAbortSummaries !== undefined && summary.autoAbortSummaries.length > 0) {
1455
1663
  lines.push("Guarded auto-abort diagnostics (evidence-only, opt-in):");
1456
1664
  for (const line of summary.autoAbortSummaries.slice(0, 3))
@@ -1466,6 +1674,9 @@ function stallAlertText(summary) {
1466
1674
  "/flowdesk-export-debug",
1467
1675
  ])
1468
1676
  lines.push(`- ${action}`);
1677
+ if (progressCardCount > 0) {
1678
+ lines.push("Lane log refs are command-based in this MVP; native clickable task UI is not claimed.");
1679
+ }
1469
1680
  return lines.join("\n");
1470
1681
  }
1471
1682
  function isFds1SchemaConversionProbeEnabled(options) {
@@ -1973,6 +2184,32 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
1973
2184
  return undefined;
1974
2185
  const client = input.client;
1975
2186
  const rootDir = input.durableStateRoot;
2187
+ const promptPreview = (text, max = 120) => {
2188
+ const compact = text.replace(/\s+/g, " ").trim();
2189
+ return compact.length > max ? `${compact.slice(0, max - 1)}…` : compact;
2190
+ };
2191
+ const taskSummaryForUser = (input) => {
2192
+ const lines = [
2193
+ input.status === "task_completed"
2194
+ ? "FlowDesk task completed."
2195
+ : input.status === "task_launched"
2196
+ ? "FlowDesk task launched."
2197
+ : "FlowDesk task failed.",
2198
+ `workflow: ${input.workflowId}`,
2199
+ `lane: ${input.laneId}`,
2200
+ `task: ${input.taskId}`,
2201
+ `prompt: ${promptPreview(input.promptText)}`,
2202
+ `agent: ${input.agentName}`,
2203
+ `model: ${input.providerQualifiedModelId}`,
2204
+ ];
2205
+ if (input.asyncMode === true)
2206
+ lines.push("progress: use /flowdesk-status or flowdesk_status_live to follow durable lane evidence");
2207
+ if (input.resultText !== undefined)
2208
+ lines.push(`result: ${promptPreview(input.resultText, 200)}`);
2209
+ if (input.failureCategory !== undefined)
2210
+ lines.push(`failure: ${input.failureCategory}${input.redactedReason === undefined ? "" : ` (${input.redactedReason})`}`);
2211
+ return lines.join("\n");
2212
+ };
1976
2213
  return {
1977
2214
  [flowdeskAgentTaskRunToolName]: tool({
1978
2215
  description: [
@@ -1980,7 +2217,7 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
1980
2217
  "Use this to delegate a well-defined subtask to a specific model (e.g. Claude Opus for security analysis, GPT for architecture review).",
1981
2218
  "Requires developerModeAcknowledged=true and allowProviderCall=true per call.",
1982
2219
  "WHEN TO USE: user asks to delegate a specific task to a specific model/agent.",
1983
- "WHEN NOT TO USE: multi-step workflows (use flowdesk_workflow_dispatch), code review (use flowdesk_quick_reviewer_run).",
2220
+ "WHEN NOT TO USE: multi-step workflows (use flowdesk_workflow_dispatch).",
1984
2221
  "After calling, use flowdesk_status_live to check the lane status.",
1985
2222
  ].join(" "),
1986
2223
  args: {
@@ -1991,6 +2228,8 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
1991
2228
  parentSessionId: tool.schema.string().optional().describe("Parent session id"),
1992
2229
  developerModeAcknowledged: tool.schema.boolean(),
1993
2230
  allowProviderCall: tool.schema.boolean(),
2231
+ nudgeQuietPeriodMs: tool.schema.number().optional().describe("Milliseconds of silence before sending a nudge prompt. Default 20000ms (20s). Recommended: always pass 20000. At 20s silence → nudge 1, 40s → nudge 2, 60s+ → lane fails and watchdog retries."),
2232
+ asyncMode: tool.schema.boolean().optional().describe("When true, return laneId immediately after launch. Watchdog polls child session, sends noReply nudges at 20s/40s, and aborts at 60s+. Coordinator uses flowdesk_status_live to detect completion. Recommended for all orchestration calls."),
1994
2233
  },
1995
2234
  async execute(args, ctx) {
1996
2235
  const record = isRecord(args) ? args : {};
@@ -2010,6 +2249,9 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2010
2249
  : typeof ctxRecord.sessionID === "string" && ctxRecord.sessionID.length > 0
2011
2250
  ? ctxRecord.sessionID
2012
2251
  : "";
2252
+ const nudgeQuietPeriodMs = typeof record.nudgeQuietPeriodMs === "number" && record.nudgeQuietPeriodMs > 0
2253
+ ? Math.floor(record.nudgeQuietPeriodMs) : undefined;
2254
+ const asyncMode = record.asyncMode === true;
2013
2255
  const taskId = `task-${Date.now().toString(36)}`;
2014
2256
  const laneId = `lane-task-${Date.now().toString(36)}`;
2015
2257
  const result = await executeFlowDeskAgentTaskV1({
@@ -2022,19 +2264,37 @@ export function createFlowDeskAgentTaskRunOptInTools(input) {
2022
2264
  parentSessionId,
2023
2265
  rootDir,
2024
2266
  client,
2267
+ asyncMode,
2268
+ _nudgeQuietPeriodMs: nudgeQuietPeriodMs,
2025
2269
  });
2270
+ const failureCategory = result.status === "task_failed" ? result.failureCategory : undefined;
2271
+ const redactedReason = result.status === "task_failed" ? result.redactedReason : undefined;
2026
2272
  return JSON.stringify({
2027
2273
  workflowId,
2028
2274
  laneId,
2029
2275
  taskId,
2030
2276
  status: result.status,
2277
+ taskPreview: promptPreview(taskDescription),
2278
+ agentName,
2279
+ providerQualifiedModelId,
2280
+ ...(result.status === "task_launched" ? { childSessionId: result.childSessionId, asyncMode: true, safeNextActions: ["/flowdesk-status"] } : {}),
2031
2281
  resultText: result.status === "task_completed" ? result.resultText.slice(0, 4_096) : undefined,
2032
2282
  resultTruncated: result.status === "task_completed" && result.resultText.length > 4_096,
2033
- failureCategory: result.status === "task_failed" ? result.failureCategory : undefined,
2034
- redactedReason: result.status === "task_failed" ? result.redactedReason : undefined,
2035
- summaryForUser: result.status === "task_completed"
2036
- ? `Task completed on ${agentName} (${providerQualifiedModelId}). Result: ${result.resultText.slice(0, 200)}${result.resultText.length > 200 ? "..." : ""}`
2037
- : `Task failed on ${agentName}: ${result.failureCategory}`,
2283
+ failureCategory,
2284
+ redactedReason,
2285
+ summaryForUser: taskSummaryForUser({
2286
+ status: result.status,
2287
+ workflowId,
2288
+ laneId,
2289
+ taskId,
2290
+ agentName,
2291
+ providerQualifiedModelId,
2292
+ promptText: taskDescription,
2293
+ ...(result.status === "task_completed" ? { resultText: result.resultText } : {}),
2294
+ ...(result.status === "task_launched" ? { asyncMode: true } : {}),
2295
+ ...(failureCategory === undefined ? {} : { failureCategory }),
2296
+ ...(redactedReason === undefined ? {} : { redactedReason }),
2297
+ }),
2038
2298
  });
2039
2299
  },
2040
2300
  }),
@@ -2157,6 +2417,37 @@ function isProviderUsageLiveEnabled(options) {
2157
2417
  const value = options?.[flowdeskProviderUsageLiveOption];
2158
2418
  return value === true || (isRecord(value) && value.enabled === true);
2159
2419
  }
2420
+ /**
2421
+ * Returns true when OpenCode auth store has a google/gemini OAuth record that
2422
+ * FlowDesk's Gemini usage collector can auto-detect (via opencode-gemini-auth login),
2423
+ * without requiring explicit geminiOAuthClientId/Secret config.
2424
+ */
2425
+ function geminiOAuthAutoDetectAvailable(homeDir) {
2426
+ try {
2427
+ const home = homeDir ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
2428
+ if (!home)
2429
+ return false;
2430
+ const xdgData = process.env.XDG_DATA_HOME
2431
+ ? join(process.env.XDG_DATA_HOME, "opencode")
2432
+ : join(home, ".local", "share", "opencode");
2433
+ const authPath = join(xdgData, "auth.json");
2434
+ const raw = readFileSync(authPath, "utf8");
2435
+ const parsed = JSON.parse(raw);
2436
+ if (typeof parsed !== "object" || parsed === null)
2437
+ return false;
2438
+ const db = parsed;
2439
+ const entry = db.google ?? db.gemini;
2440
+ if (typeof entry !== "object" || entry === null)
2441
+ return false;
2442
+ const rec = entry;
2443
+ return rec.type === "oauth" &&
2444
+ (typeof rec.access === "string" && rec.access.length > 0 ||
2445
+ typeof rec.refresh === "string" && rec.refresh.length > 0);
2446
+ }
2447
+ catch {
2448
+ return false;
2449
+ }
2450
+ }
2160
2451
  function providerUsageLiveConfigFromOptions(options) {
2161
2452
  const value = options?.[flowdeskProviderUsageLiveOption];
2162
2453
  if (!isRecord(value) || value.enabled !== true)
@@ -2185,6 +2476,8 @@ function providerUsageLiveConfigFromOptions(options) {
2185
2476
  config.geminiProjectId = value.geminiProjectId;
2186
2477
  if (value.persistSnapshots === true)
2187
2478
  config.persistSnapshots = true;
2479
+ if (value.appendToChat === true)
2480
+ config.appendToChat = true;
2188
2481
  const explicitRoot = typeof value.durableStateRootDir === "string" &&
2189
2482
  value.durableStateRootDir.trim().length > 0
2190
2483
  ? value.durableStateRootDir
@@ -2292,6 +2585,254 @@ function isLaneHeartbeatWriterEnabled(options) {
2292
2585
  const value = options?.[flowdeskLaneHeartbeatWriterOption];
2293
2586
  return value === true || (isRecord(value) && value.enabled === true);
2294
2587
  }
2588
+ function isWorkflowDispatchPlanToolEnabled(options) {
2589
+ const value = options?.[flowdeskWorkflowDispatchPlanToolOption];
2590
+ return value === true || (isRecord(value) && value.enabled === true);
2591
+ }
2592
+ function workflowDispatchPlanToolConfigFromOptions(options) {
2593
+ const value = options?.[flowdeskWorkflowDispatchPlanToolOption];
2594
+ const enabledFromBool = value === true;
2595
+ const enabledFromRecord = isRecord(value) && value.enabled === true;
2596
+ if (!enabledFromBool && !enabledFromRecord)
2597
+ return undefined;
2598
+ const explicitRoot = isRecord(value) &&
2599
+ typeof value.rootDir === "string" &&
2600
+ value.rootDir.trim().length > 0
2601
+ ? value.rootDir
2602
+ : undefined;
2603
+ const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
2604
+ return rootDir === undefined ? undefined : { rootDir };
2605
+ }
2606
+ function workflowDispatchToolConfigFromOptions(input, options) {
2607
+ const value = options?.[flowdeskWorkflowDispatchOption];
2608
+ if (!isRecord(value) || value.enabled !== true || value.devBetaActualLaneLaunch !== true)
2609
+ return undefined;
2610
+ const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0 ? value.rootDir : undefined;
2611
+ const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
2612
+ if (rootDir === undefined)
2613
+ return undefined;
2614
+ const client = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
2615
+ return client === undefined ? undefined : { rootDir, client };
2616
+ }
2617
+ function controlledWriteApplyConfigFromOptions(input, options) {
2618
+ const value = options?.[flowdeskControlledWriteApplyOption];
2619
+ if (!isRecord(value) || value.enabled !== true || value.devBetaControlledWriteApply !== true)
2620
+ return undefined;
2621
+ const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0
2622
+ ? value.rootDir
2623
+ : undefined;
2624
+ const durableStateRoot = explicitRoot ?? durableStateRootFromOptions(options);
2625
+ if (durableStateRoot === undefined)
2626
+ return undefined;
2627
+ const optionWorkspaceRoot = typeof value.workspaceRoot === "string" && value.workspaceRoot.trim().length > 0
2628
+ ? value.workspaceRoot
2629
+ : undefined;
2630
+ const inputWorkspaceRoot = isRecord(input) && typeof input.workspace === "string" && input.workspace.trim().length > 0
2631
+ ? input.workspace
2632
+ : isRecord(input) && typeof input.directory === "string" && input.directory.trim().length > 0
2633
+ ? input.directory
2634
+ : undefined;
2635
+ const workspaceRoot = optionWorkspaceRoot ?? inputWorkspaceRoot ?? process.cwd();
2636
+ return { durableStateRoot, workspaceRoot };
2637
+ }
2638
+ export const flowdeskOrchestrateToolName = "flowdesk_orchestrate";
2639
+ export const flowdeskOrchestrateOption = "workflowOrchestrate";
2640
+ function orchestrateToolConfigFromOptions(input, options) {
2641
+ const value = options?.[flowdeskOrchestrateOption];
2642
+ if (!isRecord(value) || value.enabled !== true || value.devBetaActualLaneLaunch !== true)
2643
+ return undefined;
2644
+ const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0 ? value.rootDir : undefined;
2645
+ const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
2646
+ if (rootDir === undefined)
2647
+ return undefined;
2648
+ const client = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
2649
+ return client === undefined ? undefined : { rootDir, client };
2650
+ }
2651
+ function createFlowDeskOrchestrateOptInTools(config) {
2652
+ return {
2653
+ [flowdeskOrchestrateToolName]: tool({
2654
+ description: [
2655
+ "Run a full FlowDesk multi-task orchestration: Author → Assign → Schedule → Synthesize.",
2656
+ "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.",
2657
+ "INVOKE WITH: goalSummary (clear bounded goal text), parentSessionId, and optional workflowId.",
2658
+ "AFTER CALLING: surface summaryForUser and report synthesisId, taskCount, conflictDetected. All authority flags remain false.",
2659
+ "WHEN NOT TO USE: simple single-task requests, status checks, usage queries, or review fan-out.",
2660
+ ].join(" "),
2661
+ args: {
2662
+ goalSummary: tool.schema.string().describe("Bounded natural-language goal for the workflow. Max 500 chars."),
2663
+ parentSessionId: tool.schema.string().describe("Current OpenCode session id (ses-...)."),
2664
+ workflowId: tool.schema.string().optional().describe("Optional stable workflow id. Auto-generated when omitted."),
2665
+ providerQualifiedModelId: tool.schema.string().optional().describe("Concrete provider/model id for author and synthesis calls. Defaults to openai/gpt-5.5."),
2666
+ agentName: tool.schema.string().optional().describe("Agent name for author and synthesis calls. Defaults to reviewer-gpt-frontier."),
2667
+ developerModeAcknowledged: tool.schema.boolean().describe("Must be true to allow provider calls."),
2668
+ allowProviderCall: tool.schema.boolean().describe("Must be true to allow provider calls."),
2669
+ allowActualLaneLaunch: tool.schema.boolean().describe("Must be true to allow actual lane launch."),
2670
+ },
2671
+ async execute(input) {
2672
+ if (!isRecord(input))
2673
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "invalid input", safeNextActions: ["/flowdesk-doctor"] });
2674
+ if (input.developerModeAcknowledged !== true || input.allowProviderCall !== true || input.allowActualLaneLaunch !== true)
2675
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "developerModeAcknowledged, allowProviderCall, and allowActualLaneLaunch must all be true", safeNextActions: ["/flowdesk-doctor"] });
2676
+ const goalSummary = typeof input.goalSummary === "string" ? input.goalSummary.slice(0, 500) : "";
2677
+ if (!goalSummary.trim())
2678
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "goalSummary is required", safeNextActions: ["/flowdesk-doctor"] });
2679
+ const parentSessionId = typeof input.parentSessionId === "string" ? input.parentSessionId : "";
2680
+ if (!parentSessionId.trim())
2681
+ return JSON.stringify({ status: "blocked_before_orchestration", summaryForUser: "parentSessionId is required", safeNextActions: ["/flowdesk-doctor"] });
2682
+ const providerQualifiedModelId = typeof input.providerQualifiedModelId === "string" ? input.providerQualifiedModelId : "openai/gpt-5.5";
2683
+ const agentName = typeof input.agentName === "string" ? input.agentName : "reviewer-gpt-frontier";
2684
+ const workflowId = typeof input.workflowId === "string" ? input.workflowId : undefined;
2685
+ const result = await executeFlowDeskWorkflowOrchestratorV1({
2686
+ workflowId,
2687
+ goalSummary,
2688
+ parentSessionId,
2689
+ rootDir: config.rootDir,
2690
+ client: config.client,
2691
+ providerQualifiedModelId,
2692
+ agentName,
2693
+ });
2694
+ return JSON.stringify(result);
2695
+ },
2696
+ }),
2697
+ };
2698
+ }
2699
+ export function createFlowDeskWorkflowDispatchPlanOptInTools(config) {
2700
+ return {
2701
+ [flowdeskWorkflowDispatchPlanToolName]: tool({
2702
+ description: [
2703
+ "Build and persist a FlowDesk planning-only workflow dispatch plan using flowdesk.workflow_dispatch_plan.v1 evidence. This optional tool never opens dispatch authority, never calls providers, never executes runtime work, never launches lanes, and never performs fallback or reselection.",
2704
+ "WHEN TO USE: the user explicitly asks to plan a multi-role or multi-task FlowDesk workflow and durable planning evidence is useful before any later guarded command-backed step.",
2705
+ "WHEN NOT TO USE: ordinary chat, provider usage questions, status checks, code review fan-out, fallback/retry requests, or any request to actually dispatch, run, launch, execute, switch providers, or call a model.",
2706
+ "INVOKE WITH: optional workflowId, goalSummary, optional selectedAgentRoles, and tasks[] with agentRole plus summary/title labels. The configured server durable state root is used; do not pass user filesystem paths.",
2707
+ "AFTER CALLING: surface summaryForUser and safeNextActions. On blocked_before_workflow_dispatch_plan, report redactedBlockReason. Never claim dispatch/provider/runtime/lane/fallback authority.",
2708
+ ].join(" "),
2709
+ args: {
2710
+ workflowId: tool.schema
2711
+ .string()
2712
+ .optional()
2713
+ .describe("Optional workflow id to bind the durable planning evidence to. Auto-generated when omitted."),
2714
+ goalSummary: tool.schema
2715
+ .string()
2716
+ .describe("Bounded redacted summary of the workflow planning goal."),
2717
+ selectedAgentRoles: tool.schema
2718
+ .array(tool.schema.string())
2719
+ .optional()
2720
+ .describe("Optional FlowDesk role categories to include in the planning evidence."),
2721
+ tasks: tool.schema
2722
+ .array(tool.schema.object({
2723
+ agentRole: tool.schema.string(),
2724
+ title: tool.schema.string().optional(),
2725
+ summary: tool.schema.string(),
2726
+ agentRoleRef: tool.schema.string().optional(),
2727
+ dependsOnTaskIds: tool.schema.array(tool.schema.string()).optional(),
2728
+ }))
2729
+ .describe("One or more planning-only task labels with agentRole and summary. No raw prompts or provider payloads."),
2730
+ },
2731
+ async execute(input) {
2732
+ const result = executeFlowDeskWorkflowDispatchPlanToolV1({
2733
+ config,
2734
+ rawInput: input,
2735
+ });
2736
+ return JSON.stringify(result);
2737
+ },
2738
+ }),
2739
+ };
2740
+ }
2741
+ export function createFlowDeskAutoContinuePreviewOptInTools(config) {
2742
+ return {
2743
+ [flowdeskAutoContinuePreviewToolName]: tool({
2744
+ description: [
2745
+ "Preview the next pending task from durable FlowDesk workflow_dispatch_plan evidence without executing it.",
2746
+ "WHEN TO USE: the user asks whether FlowDesk can continue remaining planned work or asks what would run next from a durable plan.",
2747
+ "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.",
2748
+ "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.",
2749
+ "AFTER CALLING: surface summaryForUser, nextTaskId/title/summary, pendingTaskCount, and authority. Never claim automatic execution, dispatch, provider call, fallback, hard chat, or TUI action authority.",
2750
+ ].join(" "),
2751
+ args: {
2752
+ workflowId: tool.schema.string().describe("Workflow id with durable workflow_dispatch_plan evidence."),
2753
+ maxSteps: tool.schema.number().optional().describe("Bounded preview cap. Clamped to 1..5; no steps are executed."),
2754
+ },
2755
+ async execute(input) {
2756
+ const result = executeFlowDeskAutoContinuePreviewToolV1({
2757
+ config,
2758
+ request: {
2759
+ workflowId: typeof input.workflowId === "string" ? input.workflowId : undefined,
2760
+ maxSteps: typeof input.maxSteps === "number" ? input.maxSteps : undefined,
2761
+ },
2762
+ });
2763
+ return JSON.stringify(result);
2764
+ },
2765
+ }),
2766
+ };
2767
+ }
2768
+ export function createFlowDeskWorkflowDispatchOptInTools(config) {
2769
+ return {
2770
+ [flowdeskWorkflowDispatchToolName]: tool({
2771
+ description: [
2772
+ "Run one explicit dev-mode FlowDesk workflow task through the injected OpenCode SDK client. This optional beta tool is disabled by default and requires workflowDispatch.enabled=true plus workflowDispatch.devBetaActualLaneLaunch=true, durableStateRoot, developerModeAcknowledged=true, allowProviderCall=true, and allowActualLaneLaunch=true. It persists non-authorizing workflow_dispatch_plan evidence, launches exactly one lane through executeFlowDeskAgentTaskV1, and verifies terminal task evidence.",
2773
+ "WHEN TO USE: only when the user explicitly asks for dev-mode actual one-task workflow dispatch and understands this makes a provider/runtime call.",
2774
+ "WHEN NOT TO USE: default Release 1 workflows, planning-only requests, fallback/reselection, provider switching, controlled write/apply, ordinary chat, status, usage, or review fan-out.",
2775
+ "INVOKE WITH: optional workflowId, goalSummary, parentSessionId, one task with agentRole, summary, promptText, agentName, providerQualifiedModelId, optional outputContractRef=contract-task-result-v1, and the three explicit allow flags. Do not pass raw transcripts, provider payloads, write/apply instructions, fallback wording, or filesystem paths.",
2776
+ "AFTER CALLING: surface summaryForUser, ids, safeNextActions, and authority. Never claim default dispatch authority, write authority, fallback authority, hard chat cancellation, or default Release 1 dispatch enablement.",
2777
+ ].join(" "),
2778
+ args: {
2779
+ workflowId: tool.schema.string().optional().describe("Optional workflow id. Auto-generated when omitted."),
2780
+ goalSummary: tool.schema.string().describe("Bounded redacted summary of the one-task dev-mode workflow goal."),
2781
+ parentSessionId: tool.schema.string().describe("Existing OpenCode parent session id. Required; no unrelated silent parent session is created."),
2782
+ task: tool.schema.object({
2783
+ agentRole: tool.schema.string().describe("FlowDesk role category for the single task."),
2784
+ summary: tool.schema.string().describe("Bounded task summary label for evidence."),
2785
+ promptText: tool.schema.string().describe("Bounded prompt text for the one launched lane."),
2786
+ agentName: tool.schema.string().describe("OpenCode agent name or agent-* ref for the lane."),
2787
+ providerQualifiedModelId: tool.schema.string().describe("Concrete provider/model id such as openai/gpt-5.5."),
2788
+ outputContractRef: tool.schema.string().optional().describe("Optional; only contract-task-result-v1 is supported in this pass."),
2789
+ }),
2790
+ developerModeAcknowledged: tool.schema.boolean().describe("Must be true to acknowledge dev-mode beta lane launch."),
2791
+ allowProviderCall: tool.schema.boolean().describe("Must be true to allow the provider call for this one lane."),
2792
+ allowActualLaneLaunch: tool.schema.boolean().describe("Must be true to allow actual one-lane runtime launch."),
2793
+ },
2794
+ async execute(input) {
2795
+ const result = await executeFlowDeskWorkflowDispatchToolV1({
2796
+ config,
2797
+ rawInput: input,
2798
+ });
2799
+ return JSON.stringify(result);
2800
+ },
2801
+ }),
2802
+ };
2803
+ }
2804
+ export function createFlowDeskControlledWriteApplyOptInTools(config) {
2805
+ return {
2806
+ [flowdeskControlledWriteApplyToolName]: tool({
2807
+ description: [
2808
+ "Apply one complete-file replacement through the FlowDesk dev/beta controlled write path. This optional tool is disabled by default and requires controlledWriteApply.enabled=true plus controlledWriteApply.devBetaControlledWriteApply=true, durableStateRoot, developerModeAcknowledged=true, userApprovalRef, allowControlledWrite=true, and a workspace-relative target path.",
2809
+ "WHEN TO USE: only when the user explicitly approves a bounded local workspace file replacement in dev mode and the current file hash is known or the caller explicitly sets allowMissingExpectedHashForDevMode=true.",
2810
+ "WHEN NOT TO USE: default Release 1 workflows, model-generated automatic apply, provider/runtime dispatch, fallback/reselection, remote writes, shell execution, absolute paths, path traversal, symlink targets, binary content, or hidden injection.",
2811
+ "INVOKE WITH: workflowId, targetFilePath relative to the workspace root, expectedSha256 or expectedContentSha256 when available, replacementText, reasonSummary, developerModeAcknowledged=true, bounded userApprovalRef, and allowControlledWrite=true. Never pass raw transcripts, prompts, provider payloads, secrets, absolute paths, or shell output.",
2812
+ "AFTER CALLING: surface summaryForUser, targetFilePath, ledgerEntryId, hashes, safeNextActions, and authority. Never claim default Release 1 write authority, dispatch authority, provider calls, runtime execution, fallback authority, or hard chat cancellation.",
2813
+ ].join(" "),
2814
+ args: {
2815
+ workflowId: tool.schema.string().describe("Stable FlowDesk workflow id for durable ledger evidence."),
2816
+ targetFilePath: tool.schema.string().describe("Workspace-relative target file path only. Absolute paths and traversal are rejected."),
2817
+ expectedSha256: tool.schema.string().optional().describe("Optional sha256-<hex> hash of the current target file content."),
2818
+ expectedContentSha256: tool.schema.string().optional().describe("Optional sha256-<hex> hash alias for the current target file content."),
2819
+ allowMissingExpectedHashForDevMode: tool.schema.boolean().optional().describe("Must be true to proceed without an expected current-content hash."),
2820
+ replacementText: tool.schema.string().describe("Complete replacement file text, bounded and non-binary."),
2821
+ reasonSummary: tool.schema.string().describe("Bounded redacted reason for the controlled write."),
2822
+ developerModeAcknowledged: tool.schema.boolean().describe("Must be true to acknowledge dev/beta controlled write authority."),
2823
+ userApprovalRef: tool.schema.string().describe("Bounded opaque user approval reference for this write."),
2824
+ allowControlledWrite: tool.schema.boolean().describe("Must be true to permit this one controlled local write."),
2825
+ },
2826
+ async execute(input) {
2827
+ const result = executeFlowDeskControlledWriteApplyToolV1({
2828
+ config,
2829
+ rawInput: input,
2830
+ });
2831
+ return JSON.stringify(result);
2832
+ },
2833
+ }),
2834
+ };
2835
+ }
2295
2836
  function isAgentTaskRunEnabled(options) {
2296
2837
  const value = options?.[flowdeskAgentTaskRunOption];
2297
2838
  return value === true || (isRecord(value) && value.enabled === true);
@@ -2477,6 +3018,134 @@ export function createFlowDeskStatusLiveOptInTools(config) {
2477
3018
  }),
2478
3019
  };
2479
3020
  }
3021
+ export function createFlowDeskWorkflowSynthesisPreviewTools(config) {
3022
+ return {
3023
+ [flowdeskWorkflowSynthesisPreviewToolName]: tool({
3024
+ description: [
3025
+ "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.",
3026
+ "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.",
3027
+ "WHEN NOT TO USE: requests for model-authored synthesis, real dispatch, provider calls, write/apply, fallback/retry, or replacing raw task results.",
3028
+ "INVOKE WITH: workflowId for a workflow that already has task_result evidence under the configured durable root.",
3029
+ "AFTER CALLING: surface summaryForUser, synthesisId, tasksSummarized, conflictDetected, safeNextActions, and authority. State that this is a provider-free preview derived from durable task_result evidence.",
3030
+ ].join(" "),
3031
+ args: {
3032
+ workflowId: tool.schema.string().describe("Workflow id with existing task_result evidence to synthesize locally."),
3033
+ },
3034
+ async execute(input) {
3035
+ const workflowId = isRecord(input) && typeof input.workflowId === "string" ? input.workflowId : "";
3036
+ const result = executeFlowDeskWorkflowSynthesisPreviewV1({ workflowId, rootDir: config.rootDir });
3037
+ return JSON.stringify(result);
3038
+ },
3039
+ }),
3040
+ };
3041
+ }
3042
+ function isFlowDeskUiProbeEnabled(options) {
3043
+ const raw = options?.[flowdeskUiProbeOption];
3044
+ return process.env.FLOWDESK_UI_PROBE === "1" || raw === true || (isRecord(raw) && raw.enabled === true);
3045
+ }
3046
+ function boundedUiProbeString(value, maxLength = 160) {
3047
+ if (typeof value !== "string" || value.length === 0)
3048
+ return undefined;
3049
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
3050
+ }
3051
+ function summarizeFlowDeskUiProbeEventV1(event, observedAt) {
3052
+ const rec = isRecord(event) ? event : {};
3053
+ const properties = Object.keys(rec).sort().slice(0, 20);
3054
+ const nested = (name) => {
3055
+ const value = rec[name];
3056
+ return isRecord(value) ? value : undefined;
3057
+ };
3058
+ const session = nested("session") ?? nested("sessionID") ?? nested("sessionId");
3059
+ const message = nested("message");
3060
+ const part = nested("part");
3061
+ const sessionId = boundedUiProbeString(rec.sessionID) ??
3062
+ boundedUiProbeString(rec.sessionId) ??
3063
+ boundedUiProbeString(session?.id);
3064
+ return {
3065
+ observedAt,
3066
+ eventType: boundedUiProbeString(rec.type, 120) ?? boundedUiProbeString(rec.event, 120) ?? "unknown",
3067
+ ...(sessionId === undefined ? {} : { sessionId }),
3068
+ ...(boundedUiProbeString(message?.id) === undefined ? {} : { messageId: boundedUiProbeString(message?.id) }),
3069
+ ...(boundedUiProbeString(part?.id) === undefined ? {} : { partId: boundedUiProbeString(part?.id) }),
3070
+ keys: properties,
3071
+ };
3072
+ }
3073
+ function createFlowDeskUiProbeTools(observations) {
3074
+ return {
3075
+ [flowdeskUiProbeToolName]: tool({
3076
+ description: [
3077
+ "Developer-only FlowDesk UI renderer/event probe. Enabled only when FLOWDESK_UI_PROBE=1.",
3078
+ "It emits bounded markdown/details/link/code-fence/long-log samples and reports redacted OpenCode event observations captured by the plugin event hook.",
3079
+ "No dispatch, provider call, write authority, fallback authority, or hard chat control is granted.",
3080
+ ].join(" "),
3081
+ args: {
3082
+ mode: tool.schema
3083
+ .string()
3084
+ .optional()
3085
+ .describe("Probe mode: render, events, or all. Defaults to all."),
3086
+ limit: tool.schema
3087
+ .number()
3088
+ .optional()
3089
+ .describe("Maximum recent event observations to return; clamped to 1..50."),
3090
+ },
3091
+ async execute(input) {
3092
+ const mode = isRecord(input) && typeof input.mode === "string" ? input.mode : "all";
3093
+ const requestedLimit = isRecord(input) && typeof input.limit === "number" ? Math.floor(input.limit) : 20;
3094
+ const limit = Math.max(1, Math.min(50, requestedLimit));
3095
+ const includeRender = mode === "all" || mode === "render";
3096
+ const includeEvents = mode === "all" || mode === "events";
3097
+ const longLog = Array.from({ length: 40 }, (_, index) => {
3098
+ const n = String(index + 1).padStart(2, "0");
3099
+ return `${n}. ui-probe timeline event ${n}: message/status/log renderer sample`;
3100
+ });
3101
+ return JSON.stringify({
3102
+ schema_version: "flowdesk.ui_probe_result.v1",
3103
+ status: "ui_probe_collected",
3104
+ probeEnabled: true,
3105
+ mode,
3106
+ authority: {
3107
+ realOpenCodeDispatch: false,
3108
+ providerCall: false,
3109
+ runtimeExecution: false,
3110
+ actualLaneLaunch: false,
3111
+ fallbackAuthority: false,
3112
+ hardCancelOrNoReplyAuthority: false,
3113
+ },
3114
+ ...(includeRender
3115
+ ? {
3116
+ renderSamples: {
3117
+ markdownDetails: [
3118
+ "<details>",
3119
+ "<summary>FlowDesk UI probe: details summary</summary>",
3120
+ "",
3121
+ "[FlowDesk markdown link probe](https://example.com/flowdesk-ui-probe)",
3122
+ "",
3123
+ "```text",
3124
+ ...longLog.slice(0, 8),
3125
+ "```",
3126
+ "</details>",
3127
+ ].join("\n"),
3128
+ longLogText: ["FlowDesk UI probe long log", ...longLog].join("\n"),
3129
+ observationInstructions: [
3130
+ "1. Check whether markdownDetails renders as collapsible UI or plain text.",
3131
+ "2. Click the markdown link and re-run mode=events to see whether any click/navigation event reached Hooks.event.",
3132
+ "3. Check whether longLogText is folded, scrollable, or fully expanded by OpenCode's tool-output renderer.",
3133
+ ].join("\n"),
3134
+ },
3135
+ }
3136
+ : {}),
3137
+ ...(includeEvents
3138
+ ? {
3139
+ recentEventObservations: observations.slice(-limit),
3140
+ recentEventObservationCount: Math.min(observations.length, limit),
3141
+ totalBufferedEventObservationCount: observations.length,
3142
+ }
3143
+ : {}),
3144
+ });
3145
+ },
3146
+ }),
3147
+ };
3148
+ }
2480
3149
  export function createFlowDeskProviderUsageLiveOptInTools(config) {
2481
3150
  return {
2482
3151
  [flowdeskProviderUsageLiveToolName]: tool({
@@ -2486,7 +3155,7 @@ export function createFlowDeskProviderUsageLiveOptInTools(config) {
2486
3155
  "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.",
2487
3156
  "WHEN NOT TO USE: general chat, status of an in-progress workflow (use status instead), or any non-usage question.",
2488
3157
  "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.",
2489
- "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.",
3158
+ "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.",
2490
3159
  ].join(" "),
2491
3160
  args: {
2492
3161
  providerFamily: tool.schema
@@ -2603,6 +3272,9 @@ const flowdeskServerPlugin = async (input, options) => {
2603
3272
  const laneHeartbeatWriterConfigForDoctor = isLaneHeartbeatWriterEnabled(options)
2604
3273
  ? laneHeartbeatWriterConfigFromOptions(options)
2605
3274
  : undefined;
3275
+ const workflowDispatchPlanConfigForDoctor = isWorkflowDispatchPlanToolEnabled(options)
3276
+ ? workflowDispatchPlanToolConfigFromOptions(options)
3277
+ : undefined;
2606
3278
  const quickReviewerRunRegistered = isQuickReviewerRunEnabled(options) &&
2607
3279
  quickReviewerRunClientFrom(input, options) !== undefined;
2608
3280
  const naturalLanguageTools = {
@@ -2622,14 +3294,15 @@ const flowdeskServerPlugin = async (input, options) => {
2622
3294
  "string",
2623
3295
  persistWorkflowId: providerUsageLiveConfigForDoctor?.persistWorkflowId,
2624
3296
  geminiOAuthConfigured: providerUsageLiveConfigForDoctor !== undefined &&
2625
- ((providerUsageLiveConfigForDoctor.geminiOAuthClientId !==
2626
- undefined &&
2627
- providerUsageLiveConfigForDoctor.geminiOAuthClientSecret !==
2628
- undefined) ||
2629
- typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_ID ===
2630
- "string" ||
2631
- typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_SECRET ===
2632
- "string"),
3297
+ (
3298
+ // Explicit inline config
3299
+ (providerUsageLiveConfigForDoctor.geminiOAuthClientId !== undefined &&
3300
+ providerUsageLiveConfigForDoctor.geminiOAuthClientSecret !== undefined) ||
3301
+ // Explicit env vars
3302
+ typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_ID === "string" ||
3303
+ typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_SECRET === "string" ||
3304
+ // Auto-detect: OpenCode auth store (opencode-gemini-auth login)
3305
+ geminiOAuthAutoDetectAvailable(providerUsageLiveConfigForDoctor.homeDir)),
2633
3306
  hint: isProviderUsageLiveEnabled(options) &&
2634
3307
  providerUsageLiveConfigForDoctor === undefined
2635
3308
  ? "providerUsageLive.enabled=true but no provider family configured; set providers=['claude','openai','gemini']"
@@ -2665,6 +3338,23 @@ const flowdeskServerPlugin = async (input, options) => {
2665
3338
  ? "laneHeartbeatWriter.enabled=true but no durable state root resolved; set laneHeartbeatWriter.rootDir or top-level durableStateRoot"
2666
3339
  : undefined,
2667
3340
  },
3341
+ workflowDispatchPlanTool: {
3342
+ enabled: isWorkflowDispatchPlanToolEnabled(options),
3343
+ registered: workflowDispatchPlanConfigForDoctor !== undefined,
3344
+ rootDir: workflowDispatchPlanConfigForDoctor?.rootDir,
3345
+ persistsWorkflowDispatchPlanEvidence: workflowDispatchPlanConfigForDoctor !== undefined,
3346
+ authority: {
3347
+ realOpenCodeDispatch: false,
3348
+ providerCall: false,
3349
+ runtimeExecution: false,
3350
+ actualLaneLaunch: false,
3351
+ fallbackAuthority: false,
3352
+ },
3353
+ hint: isWorkflowDispatchPlanToolEnabled(options) &&
3354
+ workflowDispatchPlanConfigForDoctor === undefined
3355
+ ? "workflowDispatchPlanTool.enabled=true but no durable state root resolved; set workflowDispatchPlanTool.rootDir or top-level durableStateRoot"
3356
+ : undefined,
3357
+ },
2668
3358
  chatMessageStallAlert: {
2669
3359
  enabled: options?.[flowdeskChatMessageStallAlertOption] === true ||
2670
3360
  (isRecord(options?.[flowdeskChatMessageStallAlertOption]) &&
@@ -2763,8 +3453,10 @@ const flowdeskServerPlugin = async (input, options) => {
2763
3453
  const statusLiveConfig = isStatusLiveEnabled(options)
2764
3454
  ? statusLiveConfigFromOptions(options)
2765
3455
  : undefined;
2766
- if (statusLiveConfig !== undefined)
3456
+ if (statusLiveConfig !== undefined) {
2767
3457
  Object.assign(tools, createFlowDeskStatusLiveOptInTools(statusLiveConfig));
3458
+ Object.assign(tools, createFlowDeskWorkflowSynthesisPreviewTools({ rootDir: statusLiveConfig.rootDir }));
3459
+ }
2768
3460
  const quickFallbackRunConfig = isQuickFallbackRunEnabled(options)
2769
3461
  ? quickFallbackRunConfigFromOptions(options)
2770
3462
  : undefined;
@@ -2775,6 +3467,19 @@ const flowdeskServerPlugin = async (input, options) => {
2775
3467
  : undefined;
2776
3468
  if (laneHeartbeatWriterConfig !== undefined)
2777
3469
  Object.assign(tools, createFlowDeskLaneHeartbeatWriterOptInTools(laneHeartbeatWriterConfig));
3470
+ const workflowDispatchPlanConfig = isWorkflowDispatchPlanToolEnabled(options)
3471
+ ? workflowDispatchPlanToolConfigFromOptions(options)
3472
+ : undefined;
3473
+ if (workflowDispatchPlanConfig !== undefined)
3474
+ Object.assign(tools, createFlowDeskWorkflowDispatchPlanOptInTools(workflowDispatchPlanConfig));
3475
+ if (workflowDispatchPlanConfig !== undefined)
3476
+ Object.assign(tools, createFlowDeskAutoContinuePreviewOptInTools(workflowDispatchPlanConfig));
3477
+ const workflowDispatchConfig = workflowDispatchToolConfigFromOptions(input, options);
3478
+ if (workflowDispatchConfig !== undefined)
3479
+ Object.assign(tools, createFlowDeskWorkflowDispatchOptInTools(workflowDispatchConfig));
3480
+ const controlledWriteApplyConfig = controlledWriteApplyConfigFromOptions(input, options);
3481
+ if (controlledWriteApplyConfig !== undefined)
3482
+ Object.assign(tools, createFlowDeskControlledWriteApplyOptInTools(controlledWriteApplyConfig));
2778
3483
  const agentTaskRunEnabled = isAgentTaskRunEnabled(options);
2779
3484
  if (agentTaskRunEnabled) {
2780
3485
  const agentTaskRunClient = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
@@ -2786,6 +3491,13 @@ const flowdeskServerPlugin = async (input, options) => {
2786
3491
  if (agentTaskRunTools !== undefined)
2787
3492
  Object.assign(tools, agentTaskRunTools);
2788
3493
  }
3494
+ const orchestrateConfig = orchestrateToolConfigFromOptions(input, options);
3495
+ if (orchestrateConfig !== undefined)
3496
+ Object.assign(tools, createFlowDeskOrchestrateOptInTools(orchestrateConfig));
3497
+ const uiProbeEnabled = isFlowDeskUiProbeEnabled(options);
3498
+ const uiProbeEventObservations = [];
3499
+ if (uiProbeEnabled)
3500
+ Object.assign(tools, createFlowDeskUiProbeTools(uiProbeEventObservations));
2789
3501
  // P8 Background Watchdog
2790
3502
  const watchdogConfig = watchdogConfigFromOptions(options);
2791
3503
  const chatStallAlertRaw = options?.[flowdeskChatMessageStallAlertOption];
@@ -2886,14 +3598,54 @@ const flowdeskServerPlugin = async (input, options) => {
2886
3598
  },
2887
3599
  });
2888
3600
  }
3601
+ const eventRootDir = durableStateRootFromOptions(options);
3602
+ const eventHook = eventRootDir === undefined && !uiProbeEnabled
3603
+ ? undefined
3604
+ : async (input) => {
3605
+ if (uiProbeEnabled) {
3606
+ uiProbeEventObservations.push(summarizeFlowDeskUiProbeEventV1(input.event, new Date().toISOString()));
3607
+ if (uiProbeEventObservations.length > 200)
3608
+ uiProbeEventObservations.splice(0, uiProbeEventObservations.length - 200);
3609
+ }
3610
+ if (eventRootDir !== undefined) {
3611
+ await observeFlowDeskOpenCodeEventV1({ rootDir: eventRootDir, event: input.event });
3612
+ }
3613
+ };
2889
3614
  if (!naturalLanguageRoutingEnabled)
2890
- return { tool: tools };
3615
+ return eventHook === undefined ? { tool: tools } : { tool: tools, event: eventHook };
2891
3616
  const stallAlertOption = chatMessageStallAlertOptionsFrom(options, statusLiveConfig, isRecord(input) && isManagedDispatchBetaClient(input.client)
2892
3617
  ? input.client
2893
3618
  : undefined);
3619
+ // Background sidebar cache refresh interval — runs every 3 minutes regardless of chat activity
3620
+ if (providerUsageLiveConfig?.durableStateRootDir) {
3621
+ const capturedUsageConfig = providerUsageLiveConfig;
3622
+ const sidebarRefreshIntervalMs = 3 * 60_000;
3623
+ const sidebarRefreshInterval = setInterval(() => {
3624
+ const cachePath = join(capturedUsageConfig.durableStateRootDir, ".flowdesk", "ui", "provider-usage-sidebar.json");
3625
+ let isStale = false;
3626
+ try {
3627
+ const cache = JSON.parse(readFileSync(cachePath, "utf8"));
3628
+ if (typeof cache.observed_at === "string" && Date.now() - Date.parse(cache.observed_at) > sidebarRefreshIntervalMs)
3629
+ isStale = true;
3630
+ }
3631
+ catch {
3632
+ isStale = true;
3633
+ }
3634
+ if (isStale) {
3635
+ executeFlowDeskProviderUsageLiveV1({
3636
+ config: { ...capturedUsageConfig, persistSidebarCache: true },
3637
+ request: { providerFamily: "all" },
3638
+ }).catch(() => { });
3639
+ }
3640
+ }, sidebarRefreshIntervalMs);
3641
+ sidebarRefreshInterval.unref();
3642
+ process.once("exit", () => clearInterval(sidebarRefreshInterval));
3643
+ process.once("SIGTERM", () => clearInterval(sidebarRefreshInterval));
3644
+ }
2894
3645
  return {
2895
3646
  tool: tools,
2896
- "chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption, durableStateRootFromOptions(options)),
3647
+ ...(eventHook === undefined ? {} : { event: eventHook }),
3648
+ "chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption, durableStateRootFromOptions(options), providerUsageLiveConfig),
2897
3649
  };
2898
3650
  };
2899
3651
  export const flowdeskChatMessageStallAlertOption = "chatMessageStallAlert";
@@ -2933,6 +3685,13 @@ function chatMessageStallAlertOptionsFrom(options, statusLiveConfig, sdkClient)
2933
3685
  if (recordRaw !== undefined &&
2934
3686
  typeof recordRaw.includeProgressingLate === "boolean")
2935
3687
  config.includeProgressingLate = recordRaw.includeProgressingLate;
3688
+ if (recordRaw !== undefined &&
3689
+ typeof recordRaw.includeProgressCards === "boolean")
3690
+ config.includeProgressCards = recordRaw.includeProgressCards;
3691
+ if (recordRaw !== undefined &&
3692
+ typeof recordRaw.maxProgressCards === "number" &&
3693
+ recordRaw.maxProgressCards > 0)
3694
+ config.maxProgressCards = Math.min(6, Math.max(1, Math.floor(recordRaw.maxProgressCards)));
2936
3695
  if (recordRaw !== undefined && isRecord(recordRaw.guardedAutoAbort)) {
2937
3696
  const rawGuard = recordRaw.guardedAutoAbort;
2938
3697
  const guardedAutoAbort = {