@flowdesk/opencode-plugin 0.1.11 → 0.1.13
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.
- package/dist/agent-task-runner.d.ts +27 -0
- package/dist/agent-task-runner.d.ts.map +1 -0
- package/dist/agent-task-runner.js +390 -0
- package/dist/agent-task-runner.js.map +1 -0
- package/dist/bootstrap-installer.d.ts +3 -0
- package/dist/bootstrap-installer.d.ts.map +1 -1
- package/dist/bootstrap-installer.js +153 -7
- package/dist/bootstrap-installer.js.map +1 -1
- package/dist/command-handlers.d.ts +3 -0
- package/dist/command-handlers.d.ts.map +1 -1
- package/dist/command-handlers.js +38 -4
- package/dist/command-handlers.js.map +1 -1
- package/dist/controlled-write-tool.d.ts +49 -0
- package/dist/controlled-write-tool.d.ts.map +1 -0
- package/dist/controlled-write-tool.js +296 -0
- package/dist/controlled-write-tool.js.map +1 -0
- package/dist/local-adapter.d.ts.map +1 -1
- package/dist/local-adapter.js +19 -0
- package/dist/local-adapter.js.map +1 -1
- package/dist/managed-dispatch-adapter.d.ts +3 -0
- package/dist/managed-dispatch-adapter.d.ts.map +1 -1
- package/dist/managed-dispatch-adapter.js +179 -27
- package/dist/managed-dispatch-adapter.js.map +1 -1
- package/dist/provider-usage-live-tool.d.ts +17 -0
- package/dist/provider-usage-live-tool.d.ts.map +1 -1
- package/dist/provider-usage-live-tool.js +317 -5
- package/dist/provider-usage-live-tool.js.map +1 -1
- package/dist/quick-reviewer-run.d.ts +16 -2
- package/dist/quick-reviewer-run.d.ts.map +1 -1
- package/dist/quick-reviewer-run.js +228 -72
- package/dist/quick-reviewer-run.js.map +1 -1
- package/dist/runtime-reviewer-execution-bridge.d.ts +21 -0
- package/dist/runtime-reviewer-execution-bridge.d.ts.map +1 -1
- package/dist/runtime-reviewer-execution-bridge.js +284 -1
- package/dist/runtime-reviewer-execution-bridge.js.map +1 -1
- package/dist/server.d.ts +72 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +816 -77
- package/dist/server.js.map +1 -1
- package/dist/shared/with-timeout.d.ts +12 -0
- package/dist/shared/with-timeout.d.ts.map +1 -0
- package/dist/shared/with-timeout.js +31 -0
- package/dist/shared/with-timeout.js.map +1 -0
- package/dist/stall-recovery.d.ts +214 -0
- package/dist/stall-recovery.d.ts.map +1 -0
- package/dist/stall-recovery.js +1257 -0
- package/dist/stall-recovery.js.map +1 -0
- package/dist/status-live-tool.d.ts +28 -0
- package/dist/status-live-tool.d.ts.map +1 -1
- package/dist/status-live-tool.js +306 -1
- package/dist/status-live-tool.js.map +1 -1
- package/dist/tui-usage-snapshot.d.ts +30 -0
- package/dist/tui-usage-snapshot.d.ts.map +1 -0
- package/dist/tui-usage-snapshot.js +216 -0
- package/dist/tui-usage-snapshot.js.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +103 -0
- package/dist/tui.js.map +1 -0
- package/dist/workflow-dispatch-plan-tool.d.ts +47 -0
- package/dist/workflow-dispatch-plan-tool.d.ts.map +1 -0
- package/dist/workflow-dispatch-plan-tool.js +251 -0
- package/dist/workflow-dispatch-plan-tool.js.map +1 -0
- package/dist/workflow-dispatch-tool.d.ts +56 -0
- package/dist/workflow-dispatch-tool.d.ts.map +1 -0
- package/dist/workflow-dispatch-tool.js +276 -0
- package/dist/workflow-dispatch-tool.js.map +1 -0
- package/dist/workflow-scheduler.d.ts +19 -0
- package/dist/workflow-scheduler.d.ts.map +1 -0
- package/dist/workflow-scheduler.js +43 -0
- package/dist/workflow-scheduler.js.map +1 -0
- package/package.json +10 -2
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { join, resolve, sep } from "node:path";
|
|
3
4
|
import { applyFlowDeskSessionEvidenceWriteIntentsV1, createFlowDeskChatHookAuthorityProbeV1, evaluateFlowDeskChatIntakeV1, getFlowDeskPortableCommandToolName, getRelease1SchemaArtifact, materializeFlowDeskExactModelCacheEvidenceFromProviderAcquisitionEvidenceV1, materializeFlowDeskRuntimeLaneLaunchPlansFromReviewerFanoutEvidenceV1, planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1, prepareFlowDeskSessionEvidenceWriteIntentV1, reloadFlowDeskSessionEvidenceV1, validateFlowDeskDefaultManagedDispatchAuthorizationV1, validateProjectConfigV1, validateRunRequestV1, } from "@flowdesk/core";
|
|
@@ -10,7 +11,13 @@ import { executeFlowDeskProviderUsageLiveV1, } from "./provider-usage-live-tool.
|
|
|
10
11
|
import { executeFlowDeskQuickFallbackRunV1, } from "./quick-fallback-run.js";
|
|
11
12
|
import { executeFlowDeskQuickReviewerRunV1, } from "./quick-reviewer-run.js";
|
|
12
13
|
import { executeFlowDeskRuntimeReviewerExecutionBridgeV1, redactedRuntimeReviewerExecutionBlocked, runtimeReviewerExecutionExpectationsFromValue, } from "./runtime-reviewer-execution-bridge.js";
|
|
14
|
+
import { executeFlowDeskAgentTaskV1 } from "./agent-task-runner.js";
|
|
15
|
+
import { executeFlowDeskControlledWriteApplyToolV1, } from "./controlled-write-tool.js";
|
|
13
16
|
import { executeFlowDeskStatusLiveV1, } from "./status-live-tool.js";
|
|
17
|
+
import { executeFlowDeskWorkflowDispatchPlanToolV1, } from "./workflow-dispatch-plan-tool.js";
|
|
18
|
+
import { executeFlowDeskWorkflowDispatchToolV1, } from "./workflow-dispatch-tool.js";
|
|
19
|
+
import { evaluateGuardedAutoAbortHookV1, evaluateGuardedAutoRetryHookV1, reconcileStalePendingRetryPlansV1, checkSdkSessionApiHealthV1, runFlowDeskWatchdogCycleV1, } from "./stall-recovery.js";
|
|
20
|
+
import { withTimeout, FlowDeskTimeoutError } from "./shared/with-timeout.js";
|
|
14
21
|
import { FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS, getFlowDeskRelease1HandlerReadinessSummary, getFlowDeskRelease1ProductionReadinessSummary, hasPassingFds1SchemaConversionSpike, runFlowDeskPreSpikePluginToolStub, } from "./tool-stubs.js";
|
|
15
22
|
export const flowdeskPreSpikeDoctorToolName = "flowdesk_pre_spike_doctor";
|
|
16
23
|
export const flowdeskChatIntakeToolName = "flowdesk_chat_intake";
|
|
@@ -30,7 +37,12 @@ export const flowdeskProviderUsageLiveOption = "providerUsageLive";
|
|
|
30
37
|
export const flowdeskStatusLiveOption = "statusLive";
|
|
31
38
|
export const flowdeskQuickFallbackRunOption = "quickFallbackRun";
|
|
32
39
|
export const flowdeskLaneHeartbeatWriterOption = "laneHeartbeatWriter";
|
|
40
|
+
export const flowdeskWorkflowDispatchPlanToolOption = "workflowDispatchPlanTool";
|
|
41
|
+
export const flowdeskWorkflowDispatchOption = "workflowDispatch";
|
|
42
|
+
export const flowdeskControlledWriteApplyOption = "controlledWriteApply";
|
|
33
43
|
export const flowdeskDefaultManagedDispatchAuthorizationOption = "defaultManagedDispatchAuthorization";
|
|
44
|
+
export const flowdeskWatchdogOption = "watchdog";
|
|
45
|
+
export const flowdeskWatchdogTriggerToolName = "flowdesk_watchdog_trigger";
|
|
34
46
|
export const flowdeskManagedDispatchBetaToolName = "flowdesk_managed_dispatch_beta";
|
|
35
47
|
export const flowdeskExactModelProviderAcquisitionLiveTestToolName = "flowdesk_exact_model_provider_acquisition_live_test";
|
|
36
48
|
export const flowdeskRuntimeReviewerExecutionToolName = "flowdesk_runtime_reviewer_execution";
|
|
@@ -40,6 +52,11 @@ export const flowdeskProviderUsageLiveToolName = "flowdesk_provider_usage_live";
|
|
|
40
52
|
export const flowdeskStatusLiveToolName = "flowdesk_status_live";
|
|
41
53
|
export const flowdeskQuickFallbackRunToolName = "flowdesk_quick_fallback_run";
|
|
42
54
|
export const flowdeskLaneHeartbeatWriterToolName = "flowdesk_lane_heartbeat_record";
|
|
55
|
+
export const flowdeskWorkflowDispatchPlanToolName = "flowdesk_workflow_dispatch_plan";
|
|
56
|
+
export const flowdeskWorkflowDispatchToolName = "flowdesk_workflow_dispatch";
|
|
57
|
+
export const flowdeskControlledWriteApplyToolName = "flowdesk_controlled_write_apply";
|
|
58
|
+
export const flowdeskAgentTaskRunOption = "agentTaskRun";
|
|
59
|
+
export const flowdeskAgentTaskRunToolName = "flowdesk_agent_task_run";
|
|
43
60
|
const flowdeskChatSuggestionDuplicateWindowMs = 10_000;
|
|
44
61
|
const disabledAuthority = {
|
|
45
62
|
productionRegistrationEligible: false,
|
|
@@ -1163,6 +1180,15 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1163
1180
|
const recentStallAlerts = new Map();
|
|
1164
1181
|
return async function message(input, output) {
|
|
1165
1182
|
const inputRecord = isRecord(input) ? input : {};
|
|
1183
|
+
const partSessionID = typeof inputRecord.sessionID === "string" ? inputRecord.sessionID : "";
|
|
1184
|
+
const partMessageID = typeof inputRecord.messageID === "string" ? inputRecord.messageID : "";
|
|
1185
|
+
const buildTextPart = (text) => ({
|
|
1186
|
+
id: `prt_${randomUUID().replaceAll("-", "")}`,
|
|
1187
|
+
sessionID: partSessionID,
|
|
1188
|
+
messageID: partMessageID,
|
|
1189
|
+
type: "text",
|
|
1190
|
+
text,
|
|
1191
|
+
});
|
|
1166
1192
|
const request = intakeRequestFromChatMessage({ ...inputRecord, ...output });
|
|
1167
1193
|
const preview = previewNaturalLanguageRouting(request, session);
|
|
1168
1194
|
const nowMs = clockMs(now);
|
|
@@ -1176,32 +1202,51 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1176
1202
|
nowMs < recordedAtMs)
|
|
1177
1203
|
recentStallAlerts.delete(key);
|
|
1178
1204
|
}
|
|
1179
|
-
const
|
|
1180
|
-
? await
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1205
|
+
const stallResult = stallAlert
|
|
1206
|
+
? await collectStallAlertResult(stallAlert, now, {
|
|
1207
|
+
currentSessionRef: partSessionID || request.session_ref,
|
|
1208
|
+
})
|
|
1209
|
+
: { status: "none" };
|
|
1210
|
+
let stallTextToAppend = undefined;
|
|
1211
|
+
let stallDedupKey = undefined;
|
|
1212
|
+
if (stallResult.status === "unavailable") {
|
|
1213
|
+
stallDedupKey = `${safeToken(request.session_ref, "session")}|stall-unavailable`;
|
|
1214
|
+
stallTextToAppend =
|
|
1215
|
+
"FlowDesk\nStall detection temporarily unavailable (status check timed out).\nSafe next actions:\n- /flowdesk-status\n- /flowdesk-doctor";
|
|
1216
|
+
}
|
|
1217
|
+
else if (stallResult.status === "error") {
|
|
1218
|
+
stallDedupKey = `${safeToken(request.session_ref, "session")}|stall-error`;
|
|
1219
|
+
stallTextToAppend =
|
|
1220
|
+
"FlowDesk\nStall detection encountered an error.\nRun /flowdesk-doctor to diagnose.";
|
|
1221
|
+
}
|
|
1222
|
+
else if (stallResult.status === "ok") {
|
|
1223
|
+
const summary = stallResult.data;
|
|
1224
|
+
const stalledAlertReady = summary.worstClassification === "stalled" &&
|
|
1225
|
+
summary.totalStalled > 0;
|
|
1226
|
+
const lateAlertReady = stallAlert?.includeProgressingLate === true &&
|
|
1227
|
+
summary.worstClassification === "progressing_late" &&
|
|
1228
|
+
summary.totalLate > 0;
|
|
1229
|
+
const progressCardReady = stallAlert?.includeProgressCards === true &&
|
|
1230
|
+
summary.workflowSummaries.some((workflow) => (workflow.laneCards?.length ?? 0) > 0);
|
|
1231
|
+
if (stalledAlertReady || lateAlertReady || progressCardReady) {
|
|
1232
|
+
stallDedupKey = stallAlertDuplicateKey(request, summary);
|
|
1233
|
+
stallTextToAppend = stallAlertText(summary);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
const appendStallCard = () => {
|
|
1237
|
+
if (stallDedupKey && stallTextToAppend) {
|
|
1238
|
+
const previous = recentStallAlerts.get(stallDedupKey);
|
|
1239
|
+
recentStallAlerts.set(stallDedupKey, nowMs);
|
|
1195
1240
|
if (previous === undefined ||
|
|
1196
1241
|
nowMs - previous > flowdeskChatSuggestionDuplicateWindowMs) {
|
|
1197
1242
|
if (!Array.isArray(output.parts))
|
|
1198
1243
|
output.parts = [];
|
|
1199
|
-
output.parts.push(
|
|
1200
|
-
type: "text",
|
|
1201
|
-
text: stallAlertText(stallSummary),
|
|
1202
|
-
});
|
|
1244
|
+
output.parts.push(buildTextPart(stallTextToAppend));
|
|
1203
1245
|
}
|
|
1204
1246
|
}
|
|
1247
|
+
};
|
|
1248
|
+
if (preview.evaluation.response.route_decision === "continue_chat") {
|
|
1249
|
+
appendStallCard();
|
|
1205
1250
|
return;
|
|
1206
1251
|
}
|
|
1207
1252
|
if (!mayCreatePendingConfirmation(preview)) {
|
|
@@ -1217,21 +1262,64 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1217
1262
|
const result = evaluateNaturalLanguageRouting(request, session);
|
|
1218
1263
|
if (!Array.isArray(output.parts))
|
|
1219
1264
|
output.parts = [];
|
|
1220
|
-
output.parts.push(
|
|
1221
|
-
|
|
1222
|
-
const key = stallAlertDuplicateKey(request, stallSummary);
|
|
1223
|
-
const previous = recentStallAlerts.get(key);
|
|
1224
|
-
recentStallAlerts.set(key, nowMs);
|
|
1225
|
-
if (previous === undefined ||
|
|
1226
|
-
nowMs - previous > flowdeskChatSuggestionDuplicateWindowMs)
|
|
1227
|
-
output.parts.push({
|
|
1228
|
-
type: "text",
|
|
1229
|
-
text: stallAlertText(stallSummary),
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1265
|
+
output.parts.push(buildTextPart(steeringText(result)));
|
|
1266
|
+
appendStallCard();
|
|
1232
1267
|
};
|
|
1233
1268
|
}
|
|
1234
|
-
|
|
1269
|
+
const ALLOWED_ERROR_NAMES = new Set(["FlowDeskDiskError", "FlowDeskStateError"]);
|
|
1270
|
+
export function assertNever(x) {
|
|
1271
|
+
throw new Error("Unexpected object: " + x);
|
|
1272
|
+
}
|
|
1273
|
+
function sessionRefVariants(value) {
|
|
1274
|
+
const variants = new Set();
|
|
1275
|
+
if (value === undefined || value.trim().length === 0)
|
|
1276
|
+
return variants;
|
|
1277
|
+
const token = safeToken(value, "session");
|
|
1278
|
+
variants.add(token);
|
|
1279
|
+
if (token.startsWith("ses_"))
|
|
1280
|
+
variants.add(`ses-${token.slice(4)}`);
|
|
1281
|
+
if (token.startsWith("ses-"))
|
|
1282
|
+
variants.add(`ses_${token.slice(4)}`);
|
|
1283
|
+
return variants;
|
|
1284
|
+
}
|
|
1285
|
+
function sessionRefsMatch(left, right) {
|
|
1286
|
+
const leftVariants = sessionRefVariants(left);
|
|
1287
|
+
if (leftVariants.size === 0)
|
|
1288
|
+
return false;
|
|
1289
|
+
for (const candidate of sessionRefVariants(right)) {
|
|
1290
|
+
if (leftVariants.has(candidate))
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
function latestParentSessionRefForLane(rootDir, workflowId, laneId) {
|
|
1296
|
+
const reload = reloadFlowDeskSessionEvidenceV1({ rootDir, workflowId });
|
|
1297
|
+
if (!reload.ok)
|
|
1298
|
+
return undefined;
|
|
1299
|
+
let latest;
|
|
1300
|
+
for (const entry of reload.entries) {
|
|
1301
|
+
if (entry.evidenceClass !== "lane_lifecycle")
|
|
1302
|
+
continue;
|
|
1303
|
+
const record = entry.record;
|
|
1304
|
+
if (!isRecord(record))
|
|
1305
|
+
continue;
|
|
1306
|
+
if (record.lane_id !== laneId)
|
|
1307
|
+
continue;
|
|
1308
|
+
if (typeof record.parent_session_ref !== "string")
|
|
1309
|
+
continue;
|
|
1310
|
+
const updatedAt = typeof record.updated_at === "string"
|
|
1311
|
+
? Date.parse(record.updated_at)
|
|
1312
|
+
: Number.NaN;
|
|
1313
|
+
if (!Number.isFinite(updatedAt))
|
|
1314
|
+
continue;
|
|
1315
|
+
if (latest === undefined || updatedAt > latest.updatedAtMs) {
|
|
1316
|
+
latest = { parentSessionRef: record.parent_session_ref, updatedAtMs: updatedAt };
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return latest?.parentSessionRef;
|
|
1320
|
+
}
|
|
1321
|
+
export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
|
|
1322
|
+
const statusLiveImpl = deps.statusLiveImpl ?? executeFlowDeskStatusLiveV1;
|
|
1235
1323
|
try {
|
|
1236
1324
|
const observedAt = (typeof clock === "function" ? clock() : clock).toISOString();
|
|
1237
1325
|
const config = {
|
|
@@ -1250,25 +1338,104 @@ async function collectStallAlertSummary(stallAlert, clock) {
|
|
|
1250
1338
|
laneHeartbeatStallThresholdMs: stallAlert.laneHeartbeatStallThresholdMs,
|
|
1251
1339
|
}),
|
|
1252
1340
|
};
|
|
1253
|
-
const result = await
|
|
1341
|
+
const result = await withTimeout(statusLiveImpl({
|
|
1254
1342
|
config,
|
|
1255
1343
|
now: () => new Date(observedAt),
|
|
1256
|
-
});
|
|
1344
|
+
}), stallAlert.statusLiveTimeoutMs ?? 8_000, "executeFlowDeskStatusLiveV1");
|
|
1257
1345
|
if (result.status !== "status_live_collected")
|
|
1258
|
-
return
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1346
|
+
return { status: "none" };
|
|
1347
|
+
const autoAbortSummaries = [];
|
|
1348
|
+
const currentSessionRef = deps.currentSessionRef;
|
|
1349
|
+
const workflowSummariesWithEmpty = await Promise.all(result.workflows
|
|
1350
|
+
.map(async (workflow) => {
|
|
1351
|
+
const parentRefCache = new Map();
|
|
1352
|
+
const latestParentRef = (laneId) => {
|
|
1353
|
+
if (!parentRefCache.has(laneId)) {
|
|
1354
|
+
parentRefCache.set(laneId, latestParentSessionRefForLane(stallAlert.rootDir, workflow.workflowId, laneId));
|
|
1355
|
+
}
|
|
1356
|
+
return parentRefCache.get(laneId);
|
|
1357
|
+
};
|
|
1358
|
+
const laneInCurrentSession = (laneId) => currentSessionRef === undefined ||
|
|
1359
|
+
sessionRefsMatch(latestParentRef(laneId), currentSessionRef);
|
|
1360
|
+
const scopedEntries = (workflow.laneStallProjection?.entries ?? []).filter((entry) => laneInCurrentSession(entry.laneId));
|
|
1361
|
+
const stalledEntry = scopedEntries.find((entry) => entry.classification === "stalled");
|
|
1362
|
+
const lateEntry = scopedEntries.find((entry) => entry.classification === "progressing_late");
|
|
1267
1363
|
const primary = stalledEntry ?? lateEntry;
|
|
1364
|
+
const scopedLaneCards = (workflow.laneProgressCards ?? []).filter((lane) => lane.classification !== "terminal" &&
|
|
1365
|
+
laneInCurrentSession(lane.laneId));
|
|
1366
|
+
const scopedStalledCount = scopedEntries.filter((entry) => entry.classification === "stalled").length;
|
|
1367
|
+
const scopedLateCount = scopedEntries.filter((entry) => entry.classification === "progressing_late").length;
|
|
1368
|
+
const shouldShowWorkflow = scopedStalledCount > 0 ||
|
|
1369
|
+
(stallAlert.includeProgressingLate === true && scopedLateCount > 0) ||
|
|
1370
|
+
(stallAlert.includeProgressCards === true && scopedLaneCards.length > 0);
|
|
1371
|
+
if (!shouldShowWorkflow)
|
|
1372
|
+
return undefined;
|
|
1373
|
+
if (stallAlert.guardedAutoAbort !== undefined &&
|
|
1374
|
+
stalledEntry !== undefined &&
|
|
1375
|
+
scopedStalledCount > 0) {
|
|
1376
|
+
// Reconcile stale pending retry plans on each stall check
|
|
1377
|
+
try {
|
|
1378
|
+
reconcileStalePendingRetryPlansV1({
|
|
1379
|
+
rootDir: stallAlert.rootDir,
|
|
1380
|
+
workflowId: workflow.workflowId,
|
|
1381
|
+
now: new Date(observedAt),
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
catch {
|
|
1385
|
+
// Reconciliation is best-effort
|
|
1386
|
+
}
|
|
1387
|
+
let sdkSessionHealth = stallAlert.guardedAutoAbort.sdkSessionHealth;
|
|
1388
|
+
if (sdkSessionHealth === undefined &&
|
|
1389
|
+
stallAlert.guardedAutoAbort.useLiveSdkSessionHealth === true &&
|
|
1390
|
+
stallAlert.guardedAutoAbort.sdkClient !== undefined) {
|
|
1391
|
+
const parentSessionRef = latestParentSessionRefForLane(stallAlert.rootDir, workflow.workflowId, stalledEntry.laneId);
|
|
1392
|
+
sdkSessionHealth = parentSessionRef === undefined
|
|
1393
|
+
? { status: "unknown", reason: "parent_session_ref_missing" }
|
|
1394
|
+
: await checkSdkSessionApiHealthV1(stallAlert.guardedAutoAbort.sdkClient, parentSessionRef);
|
|
1395
|
+
}
|
|
1396
|
+
const autoAbort = evaluateGuardedAutoAbortHookV1({
|
|
1397
|
+
rootDir: stallAlert.rootDir,
|
|
1398
|
+
workflow_id: workflow.workflowId,
|
|
1399
|
+
lane_id: stalledEntry.laneId,
|
|
1400
|
+
config: stallAlert.guardedAutoAbort,
|
|
1401
|
+
stallConfirmed: true,
|
|
1402
|
+
sdkSessionHealth: sdkSessionHealth ?? {
|
|
1403
|
+
status: "unknown",
|
|
1404
|
+
reason: "sdk_session_health_not_supplied_to_chat_hook",
|
|
1405
|
+
},
|
|
1406
|
+
now: () => new Date(observedAt),
|
|
1407
|
+
});
|
|
1408
|
+
autoAbortSummaries.push(`workflow ${workflow.workflowId} lane ${stalledEntry.laneId}: guarded auto-abort ${autoAbort.status}`);
|
|
1409
|
+
// After auto-abort, evaluate guarded auto-retry if configured
|
|
1410
|
+
if (autoAbort.status === "auto_abort_executed" &&
|
|
1411
|
+
stallAlert.guardedAutoAbort.autoRetryAfterAbort === true &&
|
|
1412
|
+
stallAlert.guardedAutoAbort.sdkClient !== undefined) {
|
|
1413
|
+
let retryResult;
|
|
1414
|
+
try {
|
|
1415
|
+
retryResult = await evaluateGuardedAutoRetryHookV1({
|
|
1416
|
+
config: stallAlert.guardedAutoAbort,
|
|
1417
|
+
rootDir: stallAlert.rootDir,
|
|
1418
|
+
workflowId: workflow.workflowId,
|
|
1419
|
+
laneId: stalledEntry.laneId,
|
|
1420
|
+
abortEvidenceId: autoAbort.lifecycle_evidence_id,
|
|
1421
|
+
client: stallAlert.guardedAutoAbort.sdkClient,
|
|
1422
|
+
parentSessionId: stalledEntry.laneId,
|
|
1423
|
+
timeoutMs: 30_000,
|
|
1424
|
+
now: new Date(observedAt),
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
catch {
|
|
1428
|
+
// Retry evaluation is best-effort
|
|
1429
|
+
}
|
|
1430
|
+
if (retryResult !== undefined) {
|
|
1431
|
+
autoAbortSummaries.push(`workflow ${workflow.workflowId} lane ${stalledEntry.laneId}: guarded auto-retry ${retryResult.status}`);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1268
1435
|
return {
|
|
1269
1436
|
workflowId: workflow.workflowId,
|
|
1270
|
-
stalledLaneCount:
|
|
1271
|
-
lateLaneCount:
|
|
1437
|
+
stalledLaneCount: scopedStalledCount,
|
|
1438
|
+
lateLaneCount: scopedLateCount,
|
|
1272
1439
|
...(primary?.secondsSinceLastSignal === undefined
|
|
1273
1440
|
? {}
|
|
1274
1441
|
: { secondsSinceLastSignal: primary.secondsSinceLastSignal }),
|
|
@@ -1276,17 +1443,56 @@ async function collectStallAlertSummary(stallAlert, clock) {
|
|
|
1276
1443
|
...(primary?.failureHint === undefined
|
|
1277
1444
|
? {}
|
|
1278
1445
|
: { failureHint: primary.failureHint }),
|
|
1446
|
+
...(stallAlert.includeProgressCards === true
|
|
1447
|
+
? {
|
|
1448
|
+
laneCards: scopedLaneCards
|
|
1449
|
+
.slice(0, stallAlert.maxProgressCards ?? 3)
|
|
1450
|
+
.map((lane) => ({
|
|
1451
|
+
laneId: lane.laneId,
|
|
1452
|
+
state: lane.state,
|
|
1453
|
+
classification: lane.classification,
|
|
1454
|
+
secondsSinceLastSignal: lane.secondsSinceLastSignal,
|
|
1455
|
+
agentRef: lane.agentRef,
|
|
1456
|
+
providerQualifiedModelId: lane.providerQualifiedModelId,
|
|
1457
|
+
verdictLabel: lane.verdictLabel,
|
|
1458
|
+
failureHint: lane.failureHint,
|
|
1459
|
+
})),
|
|
1460
|
+
}
|
|
1461
|
+
: {}),
|
|
1279
1462
|
};
|
|
1280
|
-
});
|
|
1463
|
+
}));
|
|
1464
|
+
const workflowSummaries = workflowSummariesWithEmpty
|
|
1465
|
+
.filter((summary) => summary !== undefined)
|
|
1466
|
+
.slice(0, 3);
|
|
1467
|
+
if (workflowSummaries.length === 0) {
|
|
1468
|
+
return { status: "none" };
|
|
1469
|
+
}
|
|
1470
|
+
const scopedTotalStalled = workflowSummaries.reduce((sum, workflow) => sum + workflow.stalledLaneCount, 0);
|
|
1471
|
+
const scopedTotalLate = workflowSummaries.reduce((sum, workflow) => sum + workflow.lateLaneCount, 0);
|
|
1472
|
+
const scopedWorstClassification = scopedTotalStalled > 0
|
|
1473
|
+
? "stalled"
|
|
1474
|
+
: scopedTotalLate > 0
|
|
1475
|
+
? "progressing_late"
|
|
1476
|
+
: (result.worstLaneStallClassification ?? "unknown");
|
|
1281
1477
|
return {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1478
|
+
status: "ok",
|
|
1479
|
+
data: {
|
|
1480
|
+
worstClassification: scopedWorstClassification,
|
|
1481
|
+
totalStalled: scopedTotalStalled,
|
|
1482
|
+
totalLate: scopedTotalLate,
|
|
1483
|
+
workflowSummaries,
|
|
1484
|
+
...(autoAbortSummaries.length === 0 ? {} : { autoAbortSummaries }),
|
|
1485
|
+
}
|
|
1286
1486
|
};
|
|
1287
1487
|
}
|
|
1288
|
-
catch {
|
|
1289
|
-
|
|
1488
|
+
catch (error) {
|
|
1489
|
+
if (error instanceof FlowDeskTimeoutError) {
|
|
1490
|
+
return { status: "unavailable" };
|
|
1491
|
+
}
|
|
1492
|
+
const errorName = error instanceof Error ? error.name : "UnknownError";
|
|
1493
|
+
const safeName = ALLOWED_ERROR_NAMES.has(errorName) ? errorName : "UnknownError";
|
|
1494
|
+
process.stderr.write(`[flowdesk] collectStallAlertResult error: ${safeName}\n`);
|
|
1495
|
+
return { status: "error" };
|
|
1290
1496
|
}
|
|
1291
1497
|
}
|
|
1292
1498
|
function stallAlertDuplicateKey(request, summary) {
|
|
@@ -1295,7 +1501,15 @@ function stallAlertDuplicateKey(request, summary) {
|
|
|
1295
1501
|
const ageMinutes = typeof entry.secondsSinceLastSignal === "number"
|
|
1296
1502
|
? Math.floor(entry.secondsSinceLastSignal / 60)
|
|
1297
1503
|
: -1;
|
|
1298
|
-
|
|
1504
|
+
const lanes = (entry.laneCards ?? [])
|
|
1505
|
+
.map((lane) => {
|
|
1506
|
+
const laneAge = typeof lane.secondsSinceLastSignal === "number"
|
|
1507
|
+
? Math.floor(lane.secondsSinceLastSignal / 60)
|
|
1508
|
+
: -1;
|
|
1509
|
+
return `${lane.laneId}:${lane.state ?? "unknown"}:${lane.classification}:${laneAge}`;
|
|
1510
|
+
})
|
|
1511
|
+
.join(",");
|
|
1512
|
+
return `${entry.workflowId}:${entry.stalledLaneCount}:${ageMinutes}:${lanes}`;
|
|
1299
1513
|
})
|
|
1300
1514
|
.join("|");
|
|
1301
1515
|
return `${safeToken(request.session_ref, "session")}|stall|${wf}|worst:${summary.worstClassification}`;
|
|
@@ -1303,12 +1517,16 @@ function stallAlertDuplicateKey(request, summary) {
|
|
|
1303
1517
|
function stallAlertText(summary) {
|
|
1304
1518
|
const lines = [];
|
|
1305
1519
|
lines.push("FlowDesk");
|
|
1520
|
+
const progressCardCount = summary.workflowSummaries.reduce((sum, workflow) => sum + (workflow.laneCards?.length ?? 0), 0);
|
|
1306
1521
|
if (summary.worstClassification === "stalled") {
|
|
1307
1522
|
lines.push(`Stalled lanes detected: ${summary.totalStalled} stalled, ${summary.totalLate} progressing-late.`);
|
|
1308
1523
|
}
|
|
1309
1524
|
else if (summary.worstClassification === "progressing_late") {
|
|
1310
1525
|
lines.push(`Late-progressing lanes detected: ${summary.totalLate} late, ${summary.totalStalled} stalled.`);
|
|
1311
1526
|
}
|
|
1527
|
+
else if (progressCardCount > 0) {
|
|
1528
|
+
lines.push(`Lane progress: ${progressCardCount} lane(s) visible on the main screen.`);
|
|
1529
|
+
}
|
|
1312
1530
|
else {
|
|
1313
1531
|
lines.push(`Lane progress check: ${summary.totalStalled} stalled, ${summary.totalLate} progressing-late.`);
|
|
1314
1532
|
}
|
|
@@ -1320,8 +1538,22 @@ function stallAlertText(summary) {
|
|
|
1320
1538
|
? `${workflow.stalledLaneCount} stalled`
|
|
1321
1539
|
: `${workflow.lateLaneCount} progressing-late`;
|
|
1322
1540
|
lines.push(`- workflow ${workflow.workflowId}: ${counts} (last signal ~${minutes}m ago, ${hint}).`);
|
|
1541
|
+
for (const lane of workflow.laneCards?.slice(0, 3) ?? []) {
|
|
1542
|
+
const age = lane.secondsSinceLastSignal === undefined
|
|
1543
|
+
? "unknown"
|
|
1544
|
+
: `~${Math.floor(lane.secondsSinceLastSignal / 60)}m ago`;
|
|
1545
|
+
const model = lane.providerQualifiedModelId ?? "(unknown)";
|
|
1546
|
+
const agent = lane.agentRef ?? "(unknown)";
|
|
1547
|
+
const verdict = lane.verdictLabel ?? "(none)";
|
|
1548
|
+
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}`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
if (summary.autoAbortSummaries !== undefined && summary.autoAbortSummaries.length > 0) {
|
|
1553
|
+
lines.push("Guarded auto-abort diagnostics (evidence-only, opt-in):");
|
|
1554
|
+
for (const line of summary.autoAbortSummaries.slice(0, 3))
|
|
1555
|
+
lines.push(`- ${line}`);
|
|
1323
1556
|
}
|
|
1324
|
-
lines.push("FlowDesk does not auto-retry, auto-abort, or auto-fallback on stall.");
|
|
1325
1557
|
lines.push("Safe next actions:");
|
|
1326
1558
|
for (const action of [
|
|
1327
1559
|
"/flowdesk-status",
|
|
@@ -1332,6 +1564,9 @@ function stallAlertText(summary) {
|
|
|
1332
1564
|
"/flowdesk-export-debug",
|
|
1333
1565
|
])
|
|
1334
1566
|
lines.push(`- ${action}`);
|
|
1567
|
+
if (progressCardCount > 0) {
|
|
1568
|
+
lines.push("Lane log refs are command-based in this MVP; native clickable task UI is not claimed.");
|
|
1569
|
+
}
|
|
1335
1570
|
return lines.join("\n");
|
|
1336
1571
|
}
|
|
1337
1572
|
function isFds1SchemaConversionProbeEnabled(options) {
|
|
@@ -1686,6 +1921,7 @@ function redactedQuickReviewerRunBlocked(reason) {
|
|
|
1686
1921
|
laneCount: 0,
|
|
1687
1922
|
lanes: [],
|
|
1688
1923
|
redactedBlockReason: reason,
|
|
1924
|
+
summaryForUser: `FlowDesk quick reviewer blocked before launch: ${reason}. Safe next actions: /flowdesk-status.`,
|
|
1689
1925
|
safeNextActions: ["/flowdesk-status"],
|
|
1690
1926
|
authority: {
|
|
1691
1927
|
realOpenCodeDispatch: false,
|
|
@@ -1719,6 +1955,7 @@ function redactedQuickReviewerRunToolResult(result) {
|
|
|
1719
1955
|
linkedLifecycleCount: result.linkedLifecycleCount,
|
|
1720
1956
|
acceptedPerspectives: result.acceptedPerspectives,
|
|
1721
1957
|
redactedBlockReason: result.redactedBlockReason,
|
|
1958
|
+
summaryForUser: result.summaryForUser,
|
|
1722
1959
|
safeNextActions: result.safeNextActions,
|
|
1723
1960
|
authority: result.authority,
|
|
1724
1961
|
};
|
|
@@ -1727,12 +1964,13 @@ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
|
|
|
1727
1964
|
return {
|
|
1728
1965
|
[flowdeskQuickReviewerRunToolName]: tool({
|
|
1729
1966
|
description: [
|
|
1730
|
-
"Run a 3-perspective FlowDesk reviewer fan-out (policy_security, architecture, verification_implementation) on a user-supplied prompt, pasted content, or the current conversation context, returning typed reviewer verdicts.",
|
|
1967
|
+
"Run a 3-perspective FlowDesk reviewer fan-out (policy_security, architecture, verification_implementation) on a user-supplied prompt, pasted code/content, or the current conversation context, returning typed reviewer verdicts plus a concise summaryForUser string. Supports optional per-perspective bindings[] for multi-model fan-out when exact provider/model ids and reviewer agents are configured.",
|
|
1731
1968
|
"WHEN TO USE: the user explicitly asks for a code review, security audit, multi-perspective check, critical review, design review, or quality review. Trigger on English phrases such as 'multi-perspective review', 'multi-angle review', 'critical review', 'review from multiple perspectives', 'audit', 'critique', 'assess', or 'evaluate'. Trigger on Korean phrases such as '다관점 리뷰', '다관점리뷰', '다관점 비판적리뷰', '다관점 비판적 리뷰', '다각도 리뷰', '다각도 검토', '여러 관점 리뷰', '여러 관점에서 검토', '복수 관점 리뷰', '비판적 리뷰', '비판적 검토', '심층 리뷰', '아키텍처 리뷰', '보안 리뷰', '품질 리뷰', '검토', or '점검'.",
|
|
1969
|
+
"ALSO PROACTIVELY USE: when the user supplies a code snippet, file reference, or design idea and asks for opinion/critique without explicitly typing 'review' (English: 'what do you think', 'how does this look', 'spot issues', 'find problems'; Korean: '어때', '괜찮아', '문제 있어 보여', '이슈 찾아줘', '봐줘'). When the user gives no snippet, derive a concise review target from the immediate prior turn and call this tool instead of asking for one more clarification.",
|
|
1732
1970
|
"CONTENT SELECTION: code is not required. If the user provides no explicit snippet after the trigger phrase, pass a concise prompt that includes the user's request plus the relevant current conversation context or target they are asking to review.",
|
|
1733
|
-
"WHEN NOT TO USE: general questions, plain chat, or any request that does not explicitly ask for a review
|
|
1971
|
+
"WHEN NOT TO USE: general questions, plain chat, status/usage/recovery questions (use the other FlowDesk natural-language tools), or any request that does not explicitly ask for a review-style judgement.",
|
|
1734
1972
|
"INVOKE WITH: developerModeAcknowledged=true and allowProviderCall=true. The plugin user has already opted into this tool at configuration time, so these flags must be set on every call. Do not ask the user for extra confirmation before calling; just call.",
|
|
1735
|
-
"AFTER CALLING:
|
|
1973
|
+
"AFTER CALLING: read the result.summaryForUser string and surface it verbatim or compressed in your reply to the user. acceptanceStatus=verdicts_accepted and durableLinkageStatus=durable_verdicts_accepted mean all requested perspectives passed. status=quick_reviewer_run_incomplete means at least one lane did not produce a matching typed verdict; lanes[].redactedObservationErrors and lanes[].observationStatus indicate why. status=blocked_before_quick_reviewer_run means the helper refused before launching providers; report result.redactedBlockReason as-is. After surfacing summaryForUser, recommend the safeNextActions in result.safeNextActions for follow-up.",
|
|
1736
1974
|
"LANE HEARTBEAT: each reviewer lane automatically records one durable flowdesk.lane_heartbeat.v1 evidence record on launch through the runtime reviewer execution bridge; status_live and the chat.message stall card consume that heartbeat as the latest progress signal so the lane shows as progressing_normal while it is still working.",
|
|
1737
1975
|
].join(" "),
|
|
1738
1976
|
args: {
|
|
@@ -1757,6 +1995,15 @@ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
|
|
|
1757
1995
|
.array(tool.schema.string())
|
|
1758
1996
|
.optional()
|
|
1759
1997
|
.describe("Optional subset of reviewer perspectives (policy_security, architecture, verification_implementation). Defaults to all three."),
|
|
1998
|
+
bindings: tool.schema
|
|
1999
|
+
.array(tool.schema.object({
|
|
2000
|
+
perspective: tool.schema.string(),
|
|
2001
|
+
providerQualifiedModelId: tool.schema.string(),
|
|
2002
|
+
runtimeAgent: tool.schema.string(),
|
|
2003
|
+
sourceLabel: tool.schema.string().optional(),
|
|
2004
|
+
}))
|
|
2005
|
+
.optional()
|
|
2006
|
+
.describe("Optional per-perspective reviewer bindings for multi-model fan-out. Each entry must include perspective, concrete providerQualifiedModelId, and runtimeAgent. When omitted, all perspectives use providerQualifiedModelId/runtimeAgent."),
|
|
1760
2007
|
parentSessionId: tool.schema
|
|
1761
2008
|
.string()
|
|
1762
2009
|
.optional()
|
|
@@ -1775,12 +2022,28 @@ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
|
|
|
1775
2022
|
record.runtimeAgent.trim().length > 0
|
|
1776
2023
|
? record.runtimeAgent
|
|
1777
2024
|
: defaults.runtimeAgent;
|
|
1778
|
-
if (typeof providerQualifiedModelId !== "string" ||
|
|
1779
|
-
typeof runtimeAgent !== "string")
|
|
1780
|
-
return JSON.stringify(redactedQuickReviewerRunBlocked("Quick reviewer run requires providerQualifiedModelId and runtimeAgent (either as args or plugin defaults)."));
|
|
1781
2025
|
const perspectives = Array.isArray(record.perspectives)
|
|
1782
2026
|
? record.perspectives.filter((value) => typeof value === "string")
|
|
1783
2027
|
: undefined;
|
|
2028
|
+
const bindings = Array.isArray(record.bindings)
|
|
2029
|
+
? record.bindings
|
|
2030
|
+
.filter((value) => isRecord(value))
|
|
2031
|
+
.map((value) => ({
|
|
2032
|
+
perspective: value.perspective,
|
|
2033
|
+
providerQualifiedModelId: value.providerQualifiedModelId,
|
|
2034
|
+
runtimeAgent: value.runtimeAgent,
|
|
2035
|
+
...(typeof value.sourceLabel === "string"
|
|
2036
|
+
? { sourceLabel: value.sourceLabel }
|
|
2037
|
+
: {}),
|
|
2038
|
+
}))
|
|
2039
|
+
.filter((value) => typeof value.perspective === "string" &&
|
|
2040
|
+
typeof value.providerQualifiedModelId === "string" &&
|
|
2041
|
+
typeof value.runtimeAgent === "string")
|
|
2042
|
+
: undefined;
|
|
2043
|
+
if (bindings === undefined &&
|
|
2044
|
+
(typeof providerQualifiedModelId !== "string" ||
|
|
2045
|
+
typeof runtimeAgent !== "string"))
|
|
2046
|
+
return JSON.stringify(redactedQuickReviewerRunBlocked("Quick reviewer run requires providerQualifiedModelId and runtimeAgent (either as args or plugin defaults) unless per-perspective bindings are supplied."));
|
|
1784
2047
|
const result = await executeFlowDeskQuickReviewerRunV1({
|
|
1785
2048
|
client,
|
|
1786
2049
|
prompt,
|
|
@@ -1789,6 +2052,7 @@ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
|
|
|
1789
2052
|
allowProviderCall: record.allowProviderCall === true,
|
|
1790
2053
|
developerModeAcknowledged: record.developerModeAcknowledged === true,
|
|
1791
2054
|
...(perspectives === undefined ? {} : { perspectives }),
|
|
2055
|
+
...(bindings === undefined ? {} : { bindings }),
|
|
1792
2056
|
...(typeof record.parentSessionId === "string" &&
|
|
1793
2057
|
record.parentSessionId.length > 0
|
|
1794
2058
|
? { parentSessionId: record.parentSessionId }
|
|
@@ -1805,6 +2069,78 @@ export function createFlowDeskQuickReviewerRunOptInTools(client, defaults) {
|
|
|
1805
2069
|
}),
|
|
1806
2070
|
};
|
|
1807
2071
|
}
|
|
2072
|
+
export function createFlowDeskAgentTaskRunOptInTools(input) {
|
|
2073
|
+
if (!input.client || !input.durableStateRoot)
|
|
2074
|
+
return undefined;
|
|
2075
|
+
const client = input.client;
|
|
2076
|
+
const rootDir = input.durableStateRoot;
|
|
2077
|
+
return {
|
|
2078
|
+
[flowdeskAgentTaskRunToolName]: tool({
|
|
2079
|
+
description: [
|
|
2080
|
+
"Run a single task on a specific agent and model, returning the result text.",
|
|
2081
|
+
"Use this to delegate a well-defined subtask to a specific model (e.g. Claude Opus for security analysis, GPT for architecture review).",
|
|
2082
|
+
"Requires developerModeAcknowledged=true and allowProviderCall=true per call.",
|
|
2083
|
+
"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).",
|
|
2085
|
+
"After calling, use flowdesk_status_live to check the lane status.",
|
|
2086
|
+
].join(" "),
|
|
2087
|
+
args: {
|
|
2088
|
+
workflowId: tool.schema.string().describe("Workflow id (e.g. workflow-xxx)"),
|
|
2089
|
+
taskDescription: tool.schema.string().max(20_000).describe("The task prompt to send to the agent"),
|
|
2090
|
+
agentName: tool.schema.string().describe("Agent name (e.g. reviewer-claude-opus, reviewer-gpt-frontier)"),
|
|
2091
|
+
providerQualifiedModelId: tool.schema.string().describe("Concrete model id (e.g. anthropic/claude-opus-4-7)"),
|
|
2092
|
+
parentSessionId: tool.schema.string().optional().describe("Parent session id"),
|
|
2093
|
+
developerModeAcknowledged: tool.schema.boolean(),
|
|
2094
|
+
allowProviderCall: tool.schema.boolean(),
|
|
2095
|
+
},
|
|
2096
|
+
async execute(args, ctx) {
|
|
2097
|
+
const record = isRecord(args) ? args : {};
|
|
2098
|
+
if (record.developerModeAcknowledged !== true)
|
|
2099
|
+
return JSON.stringify({ status: "blocked", reason: "developerModeAcknowledged must be true" });
|
|
2100
|
+
if (record.allowProviderCall !== true)
|
|
2101
|
+
return JSON.stringify({ status: "blocked", reason: "allowProviderCall must be true" });
|
|
2102
|
+
const workflowId = typeof record.workflowId === "string" ? record.workflowId : undefined;
|
|
2103
|
+
const taskDescription = typeof record.taskDescription === "string" ? record.taskDescription : undefined;
|
|
2104
|
+
const agentName = typeof record.agentName === "string" ? record.agentName : undefined;
|
|
2105
|
+
const providerQualifiedModelId = typeof record.providerQualifiedModelId === "string" ? record.providerQualifiedModelId : undefined;
|
|
2106
|
+
if (!workflowId || !taskDescription || !agentName || !providerQualifiedModelId)
|
|
2107
|
+
return JSON.stringify({ status: "blocked", reason: "workflowId, taskDescription, agentName, and providerQualifiedModelId are required" });
|
|
2108
|
+
const ctxRecord = isRecord(ctx) ? ctx : {};
|
|
2109
|
+
const parentSessionId = typeof record.parentSessionId === "string" && record.parentSessionId.length > 0
|
|
2110
|
+
? record.parentSessionId
|
|
2111
|
+
: typeof ctxRecord.sessionID === "string" && ctxRecord.sessionID.length > 0
|
|
2112
|
+
? ctxRecord.sessionID
|
|
2113
|
+
: "";
|
|
2114
|
+
const taskId = `task-${Date.now().toString(36)}`;
|
|
2115
|
+
const laneId = `lane-task-${Date.now().toString(36)}`;
|
|
2116
|
+
const result = await executeFlowDeskAgentTaskV1({
|
|
2117
|
+
workflowId,
|
|
2118
|
+
taskId,
|
|
2119
|
+
laneId,
|
|
2120
|
+
agentRef: `agent-${agentName}`,
|
|
2121
|
+
providerQualifiedModelId,
|
|
2122
|
+
promptText: taskDescription,
|
|
2123
|
+
parentSessionId,
|
|
2124
|
+
rootDir,
|
|
2125
|
+
client,
|
|
2126
|
+
});
|
|
2127
|
+
return JSON.stringify({
|
|
2128
|
+
workflowId,
|
|
2129
|
+
laneId,
|
|
2130
|
+
taskId,
|
|
2131
|
+
status: result.status,
|
|
2132
|
+
resultText: result.status === "task_completed" ? result.resultText.slice(0, 4_096) : undefined,
|
|
2133
|
+
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}`,
|
|
2139
|
+
});
|
|
2140
|
+
},
|
|
2141
|
+
}),
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
1808
2144
|
function redactedManagedFallbackRegateBlocked(reason) {
|
|
1809
2145
|
return {
|
|
1810
2146
|
adapterProfile: "managed_fallback_regate_orchestrator",
|
|
@@ -1922,6 +2258,37 @@ function isProviderUsageLiveEnabled(options) {
|
|
|
1922
2258
|
const value = options?.[flowdeskProviderUsageLiveOption];
|
|
1923
2259
|
return value === true || (isRecord(value) && value.enabled === true);
|
|
1924
2260
|
}
|
|
2261
|
+
/**
|
|
2262
|
+
* Returns true when OpenCode auth store has a google/gemini OAuth record that
|
|
2263
|
+
* FlowDesk's Gemini usage collector can auto-detect (via opencode-gemini-auth login),
|
|
2264
|
+
* without requiring explicit geminiOAuthClientId/Secret config.
|
|
2265
|
+
*/
|
|
2266
|
+
function geminiOAuthAutoDetectAvailable(homeDir) {
|
|
2267
|
+
try {
|
|
2268
|
+
const home = homeDir ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
2269
|
+
if (!home)
|
|
2270
|
+
return false;
|
|
2271
|
+
const xdgData = process.env.XDG_DATA_HOME
|
|
2272
|
+
? join(process.env.XDG_DATA_HOME, "opencode")
|
|
2273
|
+
: join(home, ".local", "share", "opencode");
|
|
2274
|
+
const authPath = join(xdgData, "auth.json");
|
|
2275
|
+
const raw = readFileSync(authPath, "utf8");
|
|
2276
|
+
const parsed = JSON.parse(raw);
|
|
2277
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
2278
|
+
return false;
|
|
2279
|
+
const db = parsed;
|
|
2280
|
+
const entry = db.google ?? db.gemini;
|
|
2281
|
+
if (typeof entry !== "object" || entry === null)
|
|
2282
|
+
return false;
|
|
2283
|
+
const rec = entry;
|
|
2284
|
+
return rec.type === "oauth" &&
|
|
2285
|
+
(typeof rec.access === "string" && rec.access.length > 0 ||
|
|
2286
|
+
typeof rec.refresh === "string" && rec.refresh.length > 0);
|
|
2287
|
+
}
|
|
2288
|
+
catch {
|
|
2289
|
+
return false;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
1925
2292
|
function providerUsageLiveConfigFromOptions(options) {
|
|
1926
2293
|
const value = options?.[flowdeskProviderUsageLiveOption];
|
|
1927
2294
|
if (!isRecord(value) || value.enabled !== true)
|
|
@@ -2057,6 +2424,170 @@ function isLaneHeartbeatWriterEnabled(options) {
|
|
|
2057
2424
|
const value = options?.[flowdeskLaneHeartbeatWriterOption];
|
|
2058
2425
|
return value === true || (isRecord(value) && value.enabled === true);
|
|
2059
2426
|
}
|
|
2427
|
+
function isWorkflowDispatchPlanToolEnabled(options) {
|
|
2428
|
+
const value = options?.[flowdeskWorkflowDispatchPlanToolOption];
|
|
2429
|
+
return value === true || (isRecord(value) && value.enabled === true);
|
|
2430
|
+
}
|
|
2431
|
+
function workflowDispatchPlanToolConfigFromOptions(options) {
|
|
2432
|
+
const value = options?.[flowdeskWorkflowDispatchPlanToolOption];
|
|
2433
|
+
const enabledFromBool = value === true;
|
|
2434
|
+
const enabledFromRecord = isRecord(value) && value.enabled === true;
|
|
2435
|
+
if (!enabledFromBool && !enabledFromRecord)
|
|
2436
|
+
return undefined;
|
|
2437
|
+
const explicitRoot = isRecord(value) &&
|
|
2438
|
+
typeof value.rootDir === "string" &&
|
|
2439
|
+
value.rootDir.trim().length > 0
|
|
2440
|
+
? value.rootDir
|
|
2441
|
+
: undefined;
|
|
2442
|
+
const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
|
|
2443
|
+
return rootDir === undefined ? undefined : { rootDir };
|
|
2444
|
+
}
|
|
2445
|
+
function workflowDispatchToolConfigFromOptions(input, options) {
|
|
2446
|
+
const value = options?.[flowdeskWorkflowDispatchOption];
|
|
2447
|
+
if (!isRecord(value) || value.enabled !== true || value.devBetaActualLaneLaunch !== true)
|
|
2448
|
+
return undefined;
|
|
2449
|
+
const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0 ? value.rootDir : undefined;
|
|
2450
|
+
const rootDir = explicitRoot ?? durableStateRootFromOptions(options);
|
|
2451
|
+
if (rootDir === undefined)
|
|
2452
|
+
return undefined;
|
|
2453
|
+
const client = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
2454
|
+
return client === undefined ? undefined : { rootDir, client };
|
|
2455
|
+
}
|
|
2456
|
+
function controlledWriteApplyConfigFromOptions(input, options) {
|
|
2457
|
+
const value = options?.[flowdeskControlledWriteApplyOption];
|
|
2458
|
+
if (!isRecord(value) || value.enabled !== true || value.devBetaControlledWriteApply !== true)
|
|
2459
|
+
return undefined;
|
|
2460
|
+
const explicitRoot = typeof value.rootDir === "string" && value.rootDir.trim().length > 0
|
|
2461
|
+
? value.rootDir
|
|
2462
|
+
: undefined;
|
|
2463
|
+
const durableStateRoot = explicitRoot ?? durableStateRootFromOptions(options);
|
|
2464
|
+
if (durableStateRoot === undefined)
|
|
2465
|
+
return undefined;
|
|
2466
|
+
const optionWorkspaceRoot = typeof value.workspaceRoot === "string" && value.workspaceRoot.trim().length > 0
|
|
2467
|
+
? value.workspaceRoot
|
|
2468
|
+
: undefined;
|
|
2469
|
+
const inputWorkspaceRoot = isRecord(input) && typeof input.workspace === "string" && input.workspace.trim().length > 0
|
|
2470
|
+
? input.workspace
|
|
2471
|
+
: isRecord(input) && typeof input.directory === "string" && input.directory.trim().length > 0
|
|
2472
|
+
? input.directory
|
|
2473
|
+
: undefined;
|
|
2474
|
+
const workspaceRoot = optionWorkspaceRoot ?? inputWorkspaceRoot ?? process.cwd();
|
|
2475
|
+
return { durableStateRoot, workspaceRoot };
|
|
2476
|
+
}
|
|
2477
|
+
export function createFlowDeskWorkflowDispatchPlanOptInTools(config) {
|
|
2478
|
+
return {
|
|
2479
|
+
[flowdeskWorkflowDispatchPlanToolName]: tool({
|
|
2480
|
+
description: [
|
|
2481
|
+
"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.",
|
|
2482
|
+
"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.",
|
|
2483
|
+
"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.",
|
|
2484
|
+
"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.",
|
|
2485
|
+
"AFTER CALLING: surface summaryForUser and safeNextActions. On blocked_before_workflow_dispatch_plan, report redactedBlockReason. Never claim dispatch/provider/runtime/lane/fallback authority.",
|
|
2486
|
+
].join(" "),
|
|
2487
|
+
args: {
|
|
2488
|
+
workflowId: tool.schema
|
|
2489
|
+
.string()
|
|
2490
|
+
.optional()
|
|
2491
|
+
.describe("Optional workflow id to bind the durable planning evidence to. Auto-generated when omitted."),
|
|
2492
|
+
goalSummary: tool.schema
|
|
2493
|
+
.string()
|
|
2494
|
+
.describe("Bounded redacted summary of the workflow planning goal."),
|
|
2495
|
+
selectedAgentRoles: tool.schema
|
|
2496
|
+
.array(tool.schema.string())
|
|
2497
|
+
.optional()
|
|
2498
|
+
.describe("Optional FlowDesk role categories to include in the planning evidence."),
|
|
2499
|
+
tasks: tool.schema
|
|
2500
|
+
.array(tool.schema.object({
|
|
2501
|
+
agentRole: tool.schema.string(),
|
|
2502
|
+
title: tool.schema.string().optional(),
|
|
2503
|
+
summary: tool.schema.string(),
|
|
2504
|
+
agentRoleRef: tool.schema.string().optional(),
|
|
2505
|
+
dependsOnTaskIds: tool.schema.array(tool.schema.string()).optional(),
|
|
2506
|
+
}))
|
|
2507
|
+
.describe("One or more planning-only task labels with agentRole and summary. No raw prompts or provider payloads."),
|
|
2508
|
+
},
|
|
2509
|
+
async execute(input) {
|
|
2510
|
+
const result = executeFlowDeskWorkflowDispatchPlanToolV1({
|
|
2511
|
+
config,
|
|
2512
|
+
rawInput: input,
|
|
2513
|
+
});
|
|
2514
|
+
return JSON.stringify(result);
|
|
2515
|
+
},
|
|
2516
|
+
}),
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
export function createFlowDeskWorkflowDispatchOptInTools(config) {
|
|
2520
|
+
return {
|
|
2521
|
+
[flowdeskWorkflowDispatchToolName]: tool({
|
|
2522
|
+
description: [
|
|
2523
|
+
"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.",
|
|
2524
|
+
"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.",
|
|
2525
|
+
"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.",
|
|
2526
|
+
"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.",
|
|
2527
|
+
"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.",
|
|
2528
|
+
].join(" "),
|
|
2529
|
+
args: {
|
|
2530
|
+
workflowId: tool.schema.string().optional().describe("Optional workflow id. Auto-generated when omitted."),
|
|
2531
|
+
goalSummary: tool.schema.string().describe("Bounded redacted summary of the one-task dev-mode workflow goal."),
|
|
2532
|
+
parentSessionId: tool.schema.string().describe("Existing OpenCode parent session id. Required; no unrelated silent parent session is created."),
|
|
2533
|
+
task: tool.schema.object({
|
|
2534
|
+
agentRole: tool.schema.string().describe("FlowDesk role category for the single task."),
|
|
2535
|
+
summary: tool.schema.string().describe("Bounded task summary label for evidence."),
|
|
2536
|
+
promptText: tool.schema.string().describe("Bounded prompt text for the one launched lane."),
|
|
2537
|
+
agentName: tool.schema.string().describe("OpenCode agent name or agent-* ref for the lane."),
|
|
2538
|
+
providerQualifiedModelId: tool.schema.string().describe("Concrete provider/model id such as openai/gpt-5.5."),
|
|
2539
|
+
outputContractRef: tool.schema.string().optional().describe("Optional; only contract-task-result-v1 is supported in this pass."),
|
|
2540
|
+
}),
|
|
2541
|
+
developerModeAcknowledged: tool.schema.boolean().describe("Must be true to acknowledge dev-mode beta lane launch."),
|
|
2542
|
+
allowProviderCall: tool.schema.boolean().describe("Must be true to allow the provider call for this one lane."),
|
|
2543
|
+
allowActualLaneLaunch: tool.schema.boolean().describe("Must be true to allow actual one-lane runtime launch."),
|
|
2544
|
+
},
|
|
2545
|
+
async execute(input) {
|
|
2546
|
+
const result = await executeFlowDeskWorkflowDispatchToolV1({
|
|
2547
|
+
config,
|
|
2548
|
+
rawInput: input,
|
|
2549
|
+
});
|
|
2550
|
+
return JSON.stringify(result);
|
|
2551
|
+
},
|
|
2552
|
+
}),
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
export function createFlowDeskControlledWriteApplyOptInTools(config) {
|
|
2556
|
+
return {
|
|
2557
|
+
[flowdeskControlledWriteApplyToolName]: tool({
|
|
2558
|
+
description: [
|
|
2559
|
+
"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.",
|
|
2560
|
+
"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.",
|
|
2561
|
+
"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.",
|
|
2562
|
+
"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.",
|
|
2563
|
+
"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.",
|
|
2564
|
+
].join(" "),
|
|
2565
|
+
args: {
|
|
2566
|
+
workflowId: tool.schema.string().describe("Stable FlowDesk workflow id for durable ledger evidence."),
|
|
2567
|
+
targetFilePath: tool.schema.string().describe("Workspace-relative target file path only. Absolute paths and traversal are rejected."),
|
|
2568
|
+
expectedSha256: tool.schema.string().optional().describe("Optional sha256-<hex> hash of the current target file content."),
|
|
2569
|
+
expectedContentSha256: tool.schema.string().optional().describe("Optional sha256-<hex> hash alias for the current target file content."),
|
|
2570
|
+
allowMissingExpectedHashForDevMode: tool.schema.boolean().optional().describe("Must be true to proceed without an expected current-content hash."),
|
|
2571
|
+
replacementText: tool.schema.string().describe("Complete replacement file text, bounded and non-binary."),
|
|
2572
|
+
reasonSummary: tool.schema.string().describe("Bounded redacted reason for the controlled write."),
|
|
2573
|
+
developerModeAcknowledged: tool.schema.boolean().describe("Must be true to acknowledge dev/beta controlled write authority."),
|
|
2574
|
+
userApprovalRef: tool.schema.string().describe("Bounded opaque user approval reference for this write."),
|
|
2575
|
+
allowControlledWrite: tool.schema.boolean().describe("Must be true to permit this one controlled local write."),
|
|
2576
|
+
},
|
|
2577
|
+
async execute(input) {
|
|
2578
|
+
const result = executeFlowDeskControlledWriteApplyToolV1({
|
|
2579
|
+
config,
|
|
2580
|
+
rawInput: input,
|
|
2581
|
+
});
|
|
2582
|
+
return JSON.stringify(result);
|
|
2583
|
+
},
|
|
2584
|
+
}),
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
function isAgentTaskRunEnabled(options) {
|
|
2588
|
+
const value = options?.[flowdeskAgentTaskRunOption];
|
|
2589
|
+
return value === true || (isRecord(value) && value.enabled === true);
|
|
2590
|
+
}
|
|
2060
2591
|
function laneHeartbeatWriterConfigFromOptions(options) {
|
|
2061
2592
|
const value = options?.[flowdeskLaneHeartbeatWriterOption];
|
|
2062
2593
|
const enabledFromBool = value === true;
|
|
@@ -2209,10 +2740,11 @@ export function createFlowDeskStatusLiveOptInTools(config) {
|
|
|
2209
2740
|
[flowdeskStatusLiveToolName]: tool({
|
|
2210
2741
|
description: [
|
|
2211
2742
|
"Return a live FlowDesk status summary by reloading durable session evidence under the configured FlowDesk state root, including reviewer verdict counts, reviewer fan-out plans, runtime lane lifecycle records, fallback regate plans, exact-model availability cache entries, provider acquisition results, and a lane heartbeat stall projection that classifies each FlowDesk-owned lane as progressing_normal, progressing_late, stalled, terminal, or unknown based on the most recent lifecycle update.",
|
|
2212
|
-
"WHEN TO USE: the user asks about recent FlowDesk activity, current workflow progress, recent reviewer results, ongoing or stalled runs, lanes that have stopped logging, lanes that look stuck, or what has been recorded so far. Trigger on English phrases such as 'status', 'what happened', 'recent activity', 'progress', 'where are we', 'how is it going', 'recent reviews', 'recent runs', 'is it stuck', 'stalled', 'no log', 'no update', 'is anything frozen', and Korean phrases such as '상태', '어디까지', '진행 상황', '진행됐', '오늘 작업', '오늘 뭐했', '최근 활동', '최근 리뷰', '지금 어디', '상태 요약', '워크플로우 상태', '멈춘 것 같아', '멈췄어', '응답이 없어', '아무 로그도 없', '오래 걸리는', '진행이 안돼'.",
|
|
2743
|
+
"WHEN TO USE: the user asks about recent FlowDesk activity, current workflow progress, recent reviewer results, ongoing or stalled runs, lanes that have stopped logging, lanes that look stuck, or what has been recorded so far. Trigger on English phrases such as 'status', 'what happened', 'recent activity', 'progress', 'where are we', 'how is it going', 'recent reviews', 'recent runs', 'is it stuck', 'stalled', 'no log', 'no update', 'is anything frozen', 'last review', 'just now', 'earlier result', and Korean phrases such as '상태', '어디까지', '진행 상황', '진행됐', '오늘 작업', '오늘 뭐했', '최근 활동', '최근 리뷰', '지금 어디', '상태 요약', '워크플로우 상태', '멈춘 것 같아', '멈췄어', '응답이 없어', '아무 로그도 없', '오래 걸리는', '진행이 안돼', '방금', '직전', '조금 전', '이전', '최근에 한 거', '아까 한 거', '결과 보여줘'.",
|
|
2744
|
+
"ALSO PROACTIVELY USE: as a follow-up after invoking a real-work FlowDesk tool (quick_reviewer_run, quick_fallback_run, managed_dispatch). If the user asks a vague follow-up about the just-completed work ('잘 됐어?', 'how did it go?', '결과는?'), call this tool with the just-created workflowId to pull durable evidence instead of guessing from memory.",
|
|
2213
2745
|
"WHEN NOT TO USE: provider usage/quota questions (use flowdesk_provider_usage_live), multi-perspective code reviews (use flowdesk_quick_reviewer_run), or unrelated general chat.",
|
|
2214
2746
|
"INVOKE WITH: optional workflowId. When omitted, the tool lists the most recently modified durable workflows (default up to 5). The plugin user already opted in to durable status evidence reload at configuration time, so this tool can be called automatically without per-call confirmation.",
|
|
2215
|
-
"AFTER CALLING:
|
|
2747
|
+
"AFTER CALLING: read result.summaryForUser and surface that as the headline reply. If the user needs more detail, mention reviewer verdict labels (pass / changes_required / blocked / inconclusive), lane lifecycle states (running, complete, invocation_failed), the most recent fallback_regate_plan state, the most recent provider acquisition status, and any stalled or progressing_late lanes reported in worstLaneStallClassification with totalStalledLaneCount and totalProgressingLateLaneCount. Per-lane entries inside laneStallProjection.entries can also carry expectedNextHeartbeatOverdue=true plus secondsPastExpectedNextHeartbeat to indicate that the heartbeat's own expected_next_heartbeat_at has passed, which is a diagnostic hint independent of the configurable stall threshold. If stalled lanes are present, surface the laneStallProjection safe next actions (/flowdesk-status, /flowdesk-retry, /flowdesk-resume, /flowdesk-abort, /flowdesk-doctor, /flowdesk-export-debug) without auto-retrying, auto-aborting, or auto-fallbacking on the user's behalf. If no workflow returned evidence, say so plainly. Never echo raw provider/auth/token payloads.",
|
|
2216
2748
|
].join(" "),
|
|
2217
2749
|
args: {
|
|
2218
2750
|
workflowId: tool.schema
|
|
@@ -2283,6 +2815,11 @@ function quickReviewerRunDefaultsFromOptions(options) {
|
|
|
2283
2815
|
const option = options?.[flowdeskQuickReviewerRunOption];
|
|
2284
2816
|
if (!isRecord(option))
|
|
2285
2817
|
return {};
|
|
2818
|
+
const optionRoot = typeof option.durableStateRoot === "string" &&
|
|
2819
|
+
option.durableStateRoot.trim().length > 0
|
|
2820
|
+
? option.durableStateRoot
|
|
2821
|
+
: undefined;
|
|
2822
|
+
const rootDir = optionRoot ?? durableStateRootFromOptions(options);
|
|
2286
2823
|
return {
|
|
2287
2824
|
...(typeof option.providerQualifiedModelId === "string" &&
|
|
2288
2825
|
option.providerQualifiedModelId.trim().length > 0
|
|
@@ -2296,10 +2833,18 @@ function quickReviewerRunDefaultsFromOptions(options) {
|
|
|
2296
2833
|
option.sourceLabel.trim().length > 0
|
|
2297
2834
|
? { sourceLabel: option.sourceLabel }
|
|
2298
2835
|
: {}),
|
|
2299
|
-
...(
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2836
|
+
...(rootDir === undefined ? {} : { rootDir }),
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
function watchdogConfigFromOptions(options) {
|
|
2840
|
+
const w = options?.[flowdeskWatchdogOption];
|
|
2841
|
+
if (!isRecord(w) || w.enabled !== true)
|
|
2842
|
+
return undefined;
|
|
2843
|
+
return {
|
|
2844
|
+
enabled: true,
|
|
2845
|
+
intervalMs: Math.max(10_000, typeof w.intervalMs === "number" ? w.intervalMs : 30_000),
|
|
2846
|
+
stallThresholdMs: typeof w.stallThresholdMs === "number" ? w.stallThresholdMs : 3 * 60_000,
|
|
2847
|
+
mcpTriggerEnabled: w.mcpTriggerEnabled === true,
|
|
2303
2848
|
};
|
|
2304
2849
|
}
|
|
2305
2850
|
const flowdeskServerPlugin = async (input, options) => {
|
|
@@ -2350,6 +2895,9 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2350
2895
|
const laneHeartbeatWriterConfigForDoctor = isLaneHeartbeatWriterEnabled(options)
|
|
2351
2896
|
? laneHeartbeatWriterConfigFromOptions(options)
|
|
2352
2897
|
: undefined;
|
|
2898
|
+
const workflowDispatchPlanConfigForDoctor = isWorkflowDispatchPlanToolEnabled(options)
|
|
2899
|
+
? workflowDispatchPlanToolConfigFromOptions(options)
|
|
2900
|
+
: undefined;
|
|
2353
2901
|
const quickReviewerRunRegistered = isQuickReviewerRunEnabled(options) &&
|
|
2354
2902
|
quickReviewerRunClientFrom(input, options) !== undefined;
|
|
2355
2903
|
const naturalLanguageTools = {
|
|
@@ -2369,14 +2917,15 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2369
2917
|
"string",
|
|
2370
2918
|
persistWorkflowId: providerUsageLiveConfigForDoctor?.persistWorkflowId,
|
|
2371
2919
|
geminiOAuthConfigured: providerUsageLiveConfigForDoctor !== undefined &&
|
|
2372
|
-
(
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_SECRET ===
|
|
2379
|
-
|
|
2920
|
+
(
|
|
2921
|
+
// Explicit inline config
|
|
2922
|
+
(providerUsageLiveConfigForDoctor.geminiOAuthClientId !== undefined &&
|
|
2923
|
+
providerUsageLiveConfigForDoctor.geminiOAuthClientSecret !== undefined) ||
|
|
2924
|
+
// Explicit env vars
|
|
2925
|
+
typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_ID === "string" ||
|
|
2926
|
+
typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_SECRET === "string" ||
|
|
2927
|
+
// Auto-detect: OpenCode auth store (opencode-gemini-auth login)
|
|
2928
|
+
geminiOAuthAutoDetectAvailable(providerUsageLiveConfigForDoctor.homeDir)),
|
|
2380
2929
|
hint: isProviderUsageLiveEnabled(options) &&
|
|
2381
2930
|
providerUsageLiveConfigForDoctor === undefined
|
|
2382
2931
|
? "providerUsageLive.enabled=true but no provider family configured; set providers=['claude','openai','gemini']"
|
|
@@ -2412,6 +2961,23 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2412
2961
|
? "laneHeartbeatWriter.enabled=true but no durable state root resolved; set laneHeartbeatWriter.rootDir or top-level durableStateRoot"
|
|
2413
2962
|
: undefined,
|
|
2414
2963
|
},
|
|
2964
|
+
workflowDispatchPlanTool: {
|
|
2965
|
+
enabled: isWorkflowDispatchPlanToolEnabled(options),
|
|
2966
|
+
registered: workflowDispatchPlanConfigForDoctor !== undefined,
|
|
2967
|
+
rootDir: workflowDispatchPlanConfigForDoctor?.rootDir,
|
|
2968
|
+
persistsWorkflowDispatchPlanEvidence: workflowDispatchPlanConfigForDoctor !== undefined,
|
|
2969
|
+
authority: {
|
|
2970
|
+
realOpenCodeDispatch: false,
|
|
2971
|
+
providerCall: false,
|
|
2972
|
+
runtimeExecution: false,
|
|
2973
|
+
actualLaneLaunch: false,
|
|
2974
|
+
fallbackAuthority: false,
|
|
2975
|
+
},
|
|
2976
|
+
hint: isWorkflowDispatchPlanToolEnabled(options) &&
|
|
2977
|
+
workflowDispatchPlanConfigForDoctor === undefined
|
|
2978
|
+
? "workflowDispatchPlanTool.enabled=true but no durable state root resolved; set workflowDispatchPlanTool.rootDir or top-level durableStateRoot"
|
|
2979
|
+
: undefined,
|
|
2980
|
+
},
|
|
2415
2981
|
chatMessageStallAlert: {
|
|
2416
2982
|
enabled: options?.[flowdeskChatMessageStallAlertOption] === true ||
|
|
2417
2983
|
(isRecord(options?.[flowdeskChatMessageStallAlertOption]) &&
|
|
@@ -2522,16 +3088,140 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2522
3088
|
: undefined;
|
|
2523
3089
|
if (laneHeartbeatWriterConfig !== undefined)
|
|
2524
3090
|
Object.assign(tools, createFlowDeskLaneHeartbeatWriterOptInTools(laneHeartbeatWriterConfig));
|
|
3091
|
+
const workflowDispatchPlanConfig = isWorkflowDispatchPlanToolEnabled(options)
|
|
3092
|
+
? workflowDispatchPlanToolConfigFromOptions(options)
|
|
3093
|
+
: undefined;
|
|
3094
|
+
if (workflowDispatchPlanConfig !== undefined)
|
|
3095
|
+
Object.assign(tools, createFlowDeskWorkflowDispatchPlanOptInTools(workflowDispatchPlanConfig));
|
|
3096
|
+
const workflowDispatchConfig = workflowDispatchToolConfigFromOptions(input, options);
|
|
3097
|
+
if (workflowDispatchConfig !== undefined)
|
|
3098
|
+
Object.assign(tools, createFlowDeskWorkflowDispatchOptInTools(workflowDispatchConfig));
|
|
3099
|
+
const controlledWriteApplyConfig = controlledWriteApplyConfigFromOptions(input, options);
|
|
3100
|
+
if (controlledWriteApplyConfig !== undefined)
|
|
3101
|
+
Object.assign(tools, createFlowDeskControlledWriteApplyOptInTools(controlledWriteApplyConfig));
|
|
3102
|
+
const agentTaskRunEnabled = isAgentTaskRunEnabled(options);
|
|
3103
|
+
if (agentTaskRunEnabled) {
|
|
3104
|
+
const agentTaskRunClient = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
3105
|
+
const agentTaskRunRoot = durableStateRootFromOptions(options);
|
|
3106
|
+
const agentTaskRunTools = createFlowDeskAgentTaskRunOptInTools({
|
|
3107
|
+
client: agentTaskRunClient,
|
|
3108
|
+
durableStateRoot: agentTaskRunRoot,
|
|
3109
|
+
});
|
|
3110
|
+
if (agentTaskRunTools !== undefined)
|
|
3111
|
+
Object.assign(tools, agentTaskRunTools);
|
|
3112
|
+
}
|
|
3113
|
+
// P8 Background Watchdog
|
|
3114
|
+
const watchdogConfig = watchdogConfigFromOptions(options);
|
|
3115
|
+
const chatStallAlertRaw = options?.[flowdeskChatMessageStallAlertOption];
|
|
3116
|
+
const guardedAutoAbortForWatchdog = isRecord(chatStallAlertRaw) && isRecord(chatStallAlertRaw.guardedAutoAbort)
|
|
3117
|
+
? (() => {
|
|
3118
|
+
const raw = chatStallAlertRaw.guardedAutoAbort;
|
|
3119
|
+
const cfg = {
|
|
3120
|
+
autoAbortOnStall: raw.autoAbortOnStall === true,
|
|
3121
|
+
};
|
|
3122
|
+
if (typeof raw.preAbortWarningMs === "number")
|
|
3123
|
+
cfg.preAbortWarningMs = Math.floor(raw.preAbortWarningMs);
|
|
3124
|
+
if (typeof raw.guardSignOffPath === "string")
|
|
3125
|
+
cfg.guardSignOffPath = raw.guardSignOffPath;
|
|
3126
|
+
if (typeof raw.guardHmacKey === "string")
|
|
3127
|
+
cfg.guardHmacKey = raw.guardHmacKey;
|
|
3128
|
+
if (typeof raw.productionMode === "boolean")
|
|
3129
|
+
cfg.productionMode = raw.productionMode;
|
|
3130
|
+
if (raw.autoRetryAfterAbort === true)
|
|
3131
|
+
cfg.autoRetryAfterAbort = true;
|
|
3132
|
+
if (typeof raw.maxAutoRetries === "number")
|
|
3133
|
+
cfg.maxAutoRetries = Math.min(2, Math.max(1, Math.floor(raw.maxAutoRetries)));
|
|
3134
|
+
return cfg;
|
|
3135
|
+
})()
|
|
3136
|
+
: undefined;
|
|
3137
|
+
const durableStateRoot = durableStateRootFromOptions(options);
|
|
3138
|
+
if (watchdogConfig?.enabled === true && guardedAutoAbortForWatchdog !== undefined && durableStateRoot !== undefined) {
|
|
3139
|
+
const capturedClient = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
3140
|
+
const capturedParentSessionId = "";
|
|
3141
|
+
const capturedRootDir = durableStateRoot;
|
|
3142
|
+
const capturedConfig = guardedAutoAbortForWatchdog;
|
|
3143
|
+
const watchdogInterval = setInterval(() => {
|
|
3144
|
+
runFlowDeskWatchdogCycleV1({
|
|
3145
|
+
config: capturedConfig,
|
|
3146
|
+
rootDir: capturedRootDir,
|
|
3147
|
+
client: capturedClient,
|
|
3148
|
+
parentSessionId: capturedParentSessionId,
|
|
3149
|
+
now: new Date(),
|
|
3150
|
+
}).catch(() => {
|
|
3151
|
+
// errors are swallowed — watchdog must not crash the plugin
|
|
3152
|
+
});
|
|
3153
|
+
}, watchdogConfig.intervalMs ?? 30_000);
|
|
3154
|
+
// Allow the process to exit even if the interval is still active
|
|
3155
|
+
watchdogInterval.unref();
|
|
3156
|
+
process.once("exit", () => clearInterval(watchdogInterval));
|
|
3157
|
+
process.once("SIGTERM", () => clearInterval(watchdogInterval));
|
|
3158
|
+
if (watchdogConfig.mcpTriggerEnabled === true) {
|
|
3159
|
+
tools[flowdeskWatchdogTriggerToolName] = tool({
|
|
3160
|
+
description: "Trigger one watchdog cycle manually. Called by external flowdesk-watchdog process (Option A). Requires guardedAutoAbort config.",
|
|
3161
|
+
args: { parentSessionId: tool.schema.string().optional() },
|
|
3162
|
+
async execute(args) {
|
|
3163
|
+
const psi = isRecord(args) && typeof args.parentSessionId === "string" ? args.parentSessionId : "";
|
|
3164
|
+
const result = await runFlowDeskWatchdogCycleV1({
|
|
3165
|
+
config: capturedConfig,
|
|
3166
|
+
rootDir: capturedRootDir,
|
|
3167
|
+
client: capturedClient,
|
|
3168
|
+
parentSessionId: psi,
|
|
3169
|
+
now: new Date(),
|
|
3170
|
+
});
|
|
3171
|
+
return JSON.stringify({
|
|
3172
|
+
cycleAt: result.cycleAt,
|
|
3173
|
+
guardValid: result.guardValid,
|
|
3174
|
+
lanesChecked: result.lanesChecked,
|
|
3175
|
+
lanesAborted: result.lanesAborted,
|
|
3176
|
+
lanesRetried: result.lanesRetried,
|
|
3177
|
+
lanesFailed: result.lanesFailed,
|
|
3178
|
+
skippedReason: result.skippedReason,
|
|
3179
|
+
});
|
|
3180
|
+
},
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
else if (watchdogConfig?.mcpTriggerEnabled === true && guardedAutoAbortForWatchdog !== undefined && durableStateRoot !== undefined) {
|
|
3185
|
+
// mcpTriggerEnabled without setInterval (Option A standalone)
|
|
3186
|
+
const capturedClient = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
3187
|
+
const capturedRootDir = durableStateRoot;
|
|
3188
|
+
const capturedConfig = guardedAutoAbortForWatchdog;
|
|
3189
|
+
tools[flowdeskWatchdogTriggerToolName] = tool({
|
|
3190
|
+
description: "Trigger one watchdog cycle manually. Called by external flowdesk-watchdog process (Option A). Requires guardedAutoAbort config.",
|
|
3191
|
+
args: { parentSessionId: tool.schema.string().optional() },
|
|
3192
|
+
async execute(args) {
|
|
3193
|
+
const psi = isRecord(args) && typeof args.parentSessionId === "string" ? args.parentSessionId : "";
|
|
3194
|
+
const result = await runFlowDeskWatchdogCycleV1({
|
|
3195
|
+
config: capturedConfig,
|
|
3196
|
+
rootDir: capturedRootDir,
|
|
3197
|
+
client: capturedClient,
|
|
3198
|
+
parentSessionId: psi,
|
|
3199
|
+
now: new Date(),
|
|
3200
|
+
});
|
|
3201
|
+
return JSON.stringify({
|
|
3202
|
+
cycleAt: result.cycleAt,
|
|
3203
|
+
guardValid: result.guardValid,
|
|
3204
|
+
lanesChecked: result.lanesChecked,
|
|
3205
|
+
lanesAborted: result.lanesAborted,
|
|
3206
|
+
lanesRetried: result.lanesRetried,
|
|
3207
|
+
lanesFailed: result.lanesFailed,
|
|
3208
|
+
skippedReason: result.skippedReason,
|
|
3209
|
+
});
|
|
3210
|
+
},
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
2525
3213
|
if (!naturalLanguageRoutingEnabled)
|
|
2526
3214
|
return { tool: tools };
|
|
2527
|
-
const stallAlertOption = chatMessageStallAlertOptionsFrom(options, statusLiveConfig)
|
|
3215
|
+
const stallAlertOption = chatMessageStallAlertOptionsFrom(options, statusLiveConfig, isRecord(input) && isManagedDispatchBetaClient(input.client)
|
|
3216
|
+
? input.client
|
|
3217
|
+
: undefined);
|
|
2528
3218
|
return {
|
|
2529
3219
|
tool: tools,
|
|
2530
3220
|
"chat.message": createFlowDeskNaturalLanguageChatMessageHook(() => new Date(), localSession, stallAlertOption, durableStateRootFromOptions(options)),
|
|
2531
3221
|
};
|
|
2532
3222
|
};
|
|
2533
3223
|
export const flowdeskChatMessageStallAlertOption = "chatMessageStallAlert";
|
|
2534
|
-
function chatMessageStallAlertOptionsFrom(options, statusLiveConfig) {
|
|
3224
|
+
function chatMessageStallAlertOptionsFrom(options, statusLiveConfig, sdkClient) {
|
|
2535
3225
|
const raw = options?.[flowdeskChatMessageStallAlertOption];
|
|
2536
3226
|
if (raw === false)
|
|
2537
3227
|
return undefined;
|
|
@@ -2567,6 +3257,55 @@ function chatMessageStallAlertOptionsFrom(options, statusLiveConfig) {
|
|
|
2567
3257
|
if (recordRaw !== undefined &&
|
|
2568
3258
|
typeof recordRaw.includeProgressingLate === "boolean")
|
|
2569
3259
|
config.includeProgressingLate = recordRaw.includeProgressingLate;
|
|
3260
|
+
if (recordRaw !== undefined &&
|
|
3261
|
+
typeof recordRaw.includeProgressCards === "boolean")
|
|
3262
|
+
config.includeProgressCards = recordRaw.includeProgressCards;
|
|
3263
|
+
if (recordRaw !== undefined &&
|
|
3264
|
+
typeof recordRaw.maxProgressCards === "number" &&
|
|
3265
|
+
recordRaw.maxProgressCards > 0)
|
|
3266
|
+
config.maxProgressCards = Math.min(6, Math.max(1, Math.floor(recordRaw.maxProgressCards)));
|
|
3267
|
+
if (recordRaw !== undefined && isRecord(recordRaw.guardedAutoAbort)) {
|
|
3268
|
+
const rawGuard = recordRaw.guardedAutoAbort;
|
|
3269
|
+
const guardedAutoAbort = {
|
|
3270
|
+
autoAbortOnStall: rawGuard.autoAbortOnStall === true,
|
|
3271
|
+
};
|
|
3272
|
+
if (typeof rawGuard.preAbortWarningMs === "number" && rawGuard.preAbortWarningMs > 0)
|
|
3273
|
+
guardedAutoAbort.preAbortWarningMs = Math.floor(rawGuard.preAbortWarningMs);
|
|
3274
|
+
if (typeof rawGuard.guardSignOffPath === "string" && rawGuard.guardSignOffPath.trim().length > 0)
|
|
3275
|
+
guardedAutoAbort.guardSignOffPath = rawGuard.guardSignOffPath;
|
|
3276
|
+
if (typeof rawGuard.guardHmacKey === "string" && rawGuard.guardHmacKey.length > 0)
|
|
3277
|
+
guardedAutoAbort.guardHmacKey = rawGuard.guardHmacKey;
|
|
3278
|
+
if (typeof rawGuard.productionMode === "boolean")
|
|
3279
|
+
guardedAutoAbort.productionMode = rawGuard.productionMode;
|
|
3280
|
+
if (rawGuard.autoRetryAfterAbort === true)
|
|
3281
|
+
guardedAutoAbort.autoRetryAfterAbort = true;
|
|
3282
|
+
if (typeof rawGuard.maxAutoRetries === "number")
|
|
3283
|
+
guardedAutoAbort.maxAutoRetries = Math.min(2, Math.max(1, Math.floor(rawGuard.maxAutoRetries)));
|
|
3284
|
+
if (typeof rawGuard.useLiveSdkSessionHealth === "boolean")
|
|
3285
|
+
guardedAutoAbort.useLiveSdkSessionHealth = rawGuard.useLiveSdkSessionHealth;
|
|
3286
|
+
if (guardedAutoAbort.useLiveSdkSessionHealth === true && sdkClient !== undefined)
|
|
3287
|
+
guardedAutoAbort.sdkClient = sdkClient;
|
|
3288
|
+
if (isRecord(rawGuard.sdkSessionHealth)) {
|
|
3289
|
+
const status = rawGuard.sdkSessionHealth.status;
|
|
3290
|
+
if (status === "api_responsive")
|
|
3291
|
+
guardedAutoAbort.sdkSessionHealth = { status };
|
|
3292
|
+
if (status === "api_timeout")
|
|
3293
|
+
guardedAutoAbort.sdkSessionHealth = {
|
|
3294
|
+
status,
|
|
3295
|
+
reason: typeof rawGuard.sdkSessionHealth.reason === "string"
|
|
3296
|
+
? rawGuard.sdkSessionHealth.reason
|
|
3297
|
+
: "configured_api_timeout",
|
|
3298
|
+
};
|
|
3299
|
+
if (status === "unknown")
|
|
3300
|
+
guardedAutoAbort.sdkSessionHealth = {
|
|
3301
|
+
status,
|
|
3302
|
+
reason: typeof rawGuard.sdkSessionHealth.reason === "string"
|
|
3303
|
+
? rawGuard.sdkSessionHealth.reason
|
|
3304
|
+
: "configured_unknown",
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3307
|
+
config.guardedAutoAbort = guardedAutoAbort;
|
|
3308
|
+
}
|
|
2570
3309
|
return config;
|
|
2571
3310
|
}
|
|
2572
3311
|
export const flowdeskOpenCodeServerPlugin = {
|