@flowdesk/opencode-plugin 0.1.12 → 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 +1 -0
- package/dist/agent-task-runner.d.ts.map +1 -1
- package/dist/agent-task-runner.js +172 -26
- package/dist/agent-task-runner.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/managed-dispatch-adapter.d.ts +1 -0
- package/dist/managed-dispatch-adapter.d.ts.map +1 -1
- package/dist/managed-dispatch-adapter.js +93 -26
- 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 +3 -0
- package/dist/quick-reviewer-run.d.ts.map +1 -1
- package/dist/quick-reviewer-run.js +20 -8
- 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 +238 -0
- package/dist/runtime-reviewer-execution-bridge.js.map +1 -1
- package/dist/server.d.ts +25 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +356 -25
- package/dist/server.js.map +1 -1
- package/dist/stall-recovery.d.ts +27 -0
- package/dist/stall-recovery.d.ts.map +1 -1
- package/dist/stall-recovery.js +305 -10
- package/dist/stall-recovery.js.map +1 -1
- package/dist/status-live-tool.d.ts +27 -0
- package/dist/status-live-tool.d.ts.map +1 -1
- package/dist/status-live-tool.js +179 -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 +9 -1
package/dist/server.js
CHANGED
|
@@ -12,7 +12,10 @@ import { executeFlowDeskQuickFallbackRunV1, } from "./quick-fallback-run.js";
|
|
|
12
12
|
import { executeFlowDeskQuickReviewerRunV1, } from "./quick-reviewer-run.js";
|
|
13
13
|
import { executeFlowDeskRuntimeReviewerExecutionBridgeV1, redactedRuntimeReviewerExecutionBlocked, runtimeReviewerExecutionExpectationsFromValue, } from "./runtime-reviewer-execution-bridge.js";
|
|
14
14
|
import { executeFlowDeskAgentTaskV1 } from "./agent-task-runner.js";
|
|
15
|
+
import { executeFlowDeskControlledWriteApplyToolV1, } from "./controlled-write-tool.js";
|
|
15
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";
|
|
16
19
|
import { evaluateGuardedAutoAbortHookV1, evaluateGuardedAutoRetryHookV1, reconcileStalePendingRetryPlansV1, checkSdkSessionApiHealthV1, runFlowDeskWatchdogCycleV1, } from "./stall-recovery.js";
|
|
17
20
|
import { withTimeout, FlowDeskTimeoutError } from "./shared/with-timeout.js";
|
|
18
21
|
import { FLOWDESK_PRE_SPIKE_PLUGIN_TOOL_STUBS, getFlowDeskRelease1HandlerReadinessSummary, getFlowDeskRelease1ProductionReadinessSummary, hasPassingFds1SchemaConversionSpike, runFlowDeskPreSpikePluginToolStub, } from "./tool-stubs.js";
|
|
@@ -34,6 +37,9 @@ export const flowdeskProviderUsageLiveOption = "providerUsageLive";
|
|
|
34
37
|
export const flowdeskStatusLiveOption = "statusLive";
|
|
35
38
|
export const flowdeskQuickFallbackRunOption = "quickFallbackRun";
|
|
36
39
|
export const flowdeskLaneHeartbeatWriterOption = "laneHeartbeatWriter";
|
|
40
|
+
export const flowdeskWorkflowDispatchPlanToolOption = "workflowDispatchPlanTool";
|
|
41
|
+
export const flowdeskWorkflowDispatchOption = "workflowDispatch";
|
|
42
|
+
export const flowdeskControlledWriteApplyOption = "controlledWriteApply";
|
|
37
43
|
export const flowdeskDefaultManagedDispatchAuthorizationOption = "defaultManagedDispatchAuthorization";
|
|
38
44
|
export const flowdeskWatchdogOption = "watchdog";
|
|
39
45
|
export const flowdeskWatchdogTriggerToolName = "flowdesk_watchdog_trigger";
|
|
@@ -46,6 +52,9 @@ export const flowdeskProviderUsageLiveToolName = "flowdesk_provider_usage_live";
|
|
|
46
52
|
export const flowdeskStatusLiveToolName = "flowdesk_status_live";
|
|
47
53
|
export const flowdeskQuickFallbackRunToolName = "flowdesk_quick_fallback_run";
|
|
48
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";
|
|
49
58
|
export const flowdeskAgentTaskRunOption = "agentTaskRun";
|
|
50
59
|
export const flowdeskAgentTaskRunToolName = "flowdesk_agent_task_run";
|
|
51
60
|
const flowdeskChatSuggestionDuplicateWindowMs = 10_000;
|
|
@@ -1194,7 +1203,9 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1194
1203
|
recentStallAlerts.delete(key);
|
|
1195
1204
|
}
|
|
1196
1205
|
const stallResult = stallAlert
|
|
1197
|
-
? await collectStallAlertResult(stallAlert, now
|
|
1206
|
+
? await collectStallAlertResult(stallAlert, now, {
|
|
1207
|
+
currentSessionRef: partSessionID || request.session_ref,
|
|
1208
|
+
})
|
|
1198
1209
|
: { status: "none" };
|
|
1199
1210
|
let stallTextToAppend = undefined;
|
|
1200
1211
|
let stallDedupKey = undefined;
|
|
@@ -1215,7 +1226,9 @@ export function createFlowDeskNaturalLanguageChatMessageHook(now = () => new Dat
|
|
|
1215
1226
|
const lateAlertReady = stallAlert?.includeProgressingLate === true &&
|
|
1216
1227
|
summary.worstClassification === "progressing_late" &&
|
|
1217
1228
|
summary.totalLate > 0;
|
|
1218
|
-
|
|
1229
|
+
const progressCardReady = stallAlert?.includeProgressCards === true &&
|
|
1230
|
+
summary.workflowSummaries.some((workflow) => (workflow.laneCards?.length ?? 0) > 0);
|
|
1231
|
+
if (stalledAlertReady || lateAlertReady || progressCardReady) {
|
|
1219
1232
|
stallDedupKey = stallAlertDuplicateKey(request, summary);
|
|
1220
1233
|
stallTextToAppend = stallAlertText(summary);
|
|
1221
1234
|
}
|
|
@@ -1257,6 +1270,28 @@ const ALLOWED_ERROR_NAMES = new Set(["FlowDeskDiskError", "FlowDeskStateError"])
|
|
|
1257
1270
|
export function assertNever(x) {
|
|
1258
1271
|
throw new Error("Unexpected object: " + x);
|
|
1259
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
|
+
}
|
|
1260
1295
|
function latestParentSessionRefForLane(rootDir, workflowId, laneId) {
|
|
1261
1296
|
const reload = reloadFlowDeskSessionEvidenceV1({ rootDir, workflowId });
|
|
1262
1297
|
if (!reload.ok)
|
|
@@ -1310,18 +1345,34 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
|
|
|
1310
1345
|
if (result.status !== "status_live_collected")
|
|
1311
1346
|
return { status: "none" };
|
|
1312
1347
|
const autoAbortSummaries = [];
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
(stallAlert.includeProgressingLate === true &&
|
|
1316
|
-
(workflow.progressingLateLaneCount ?? 0) > 0))
|
|
1317
|
-
.slice(0, 3)
|
|
1348
|
+
const currentSessionRef = deps.currentSessionRef;
|
|
1349
|
+
const workflowSummariesWithEmpty = await Promise.all(result.workflows
|
|
1318
1350
|
.map(async (workflow) => {
|
|
1319
|
-
const
|
|
1320
|
-
const
|
|
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");
|
|
1321
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;
|
|
1322
1373
|
if (stallAlert.guardedAutoAbort !== undefined &&
|
|
1323
1374
|
stalledEntry !== undefined &&
|
|
1324
|
-
|
|
1375
|
+
scopedStalledCount > 0) {
|
|
1325
1376
|
// Reconcile stale pending retry plans on each stall check
|
|
1326
1377
|
try {
|
|
1327
1378
|
reconcileStalePendingRetryPlansV1({
|
|
@@ -1383,8 +1434,8 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
|
|
|
1383
1434
|
}
|
|
1384
1435
|
return {
|
|
1385
1436
|
workflowId: workflow.workflowId,
|
|
1386
|
-
stalledLaneCount:
|
|
1387
|
-
lateLaneCount:
|
|
1437
|
+
stalledLaneCount: scopedStalledCount,
|
|
1438
|
+
lateLaneCount: scopedLateCount,
|
|
1388
1439
|
...(primary?.secondsSinceLastSignal === undefined
|
|
1389
1440
|
? {}
|
|
1390
1441
|
: { secondsSinceLastSignal: primary.secondsSinceLastSignal }),
|
|
@@ -1392,17 +1443,43 @@ export async function collectStallAlertResult(stallAlert, clock, deps = {}) {
|
|
|
1392
1443
|
...(primary?.failureHint === undefined
|
|
1393
1444
|
? {}
|
|
1394
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
|
+
: {}),
|
|
1395
1462
|
};
|
|
1396
1463
|
}));
|
|
1464
|
+
const workflowSummaries = workflowSummariesWithEmpty
|
|
1465
|
+
.filter((summary) => summary !== undefined)
|
|
1466
|
+
.slice(0, 3);
|
|
1397
1467
|
if (workflowSummaries.length === 0) {
|
|
1398
1468
|
return { status: "none" };
|
|
1399
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");
|
|
1400
1477
|
return {
|
|
1401
1478
|
status: "ok",
|
|
1402
1479
|
data: {
|
|
1403
|
-
worstClassification:
|
|
1404
|
-
totalStalled:
|
|
1405
|
-
totalLate:
|
|
1480
|
+
worstClassification: scopedWorstClassification,
|
|
1481
|
+
totalStalled: scopedTotalStalled,
|
|
1482
|
+
totalLate: scopedTotalLate,
|
|
1406
1483
|
workflowSummaries,
|
|
1407
1484
|
...(autoAbortSummaries.length === 0 ? {} : { autoAbortSummaries }),
|
|
1408
1485
|
}
|
|
@@ -1424,7 +1501,15 @@ function stallAlertDuplicateKey(request, summary) {
|
|
|
1424
1501
|
const ageMinutes = typeof entry.secondsSinceLastSignal === "number"
|
|
1425
1502
|
? Math.floor(entry.secondsSinceLastSignal / 60)
|
|
1426
1503
|
: -1;
|
|
1427
|
-
|
|
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}`;
|
|
1428
1513
|
})
|
|
1429
1514
|
.join("|");
|
|
1430
1515
|
return `${safeToken(request.session_ref, "session")}|stall|${wf}|worst:${summary.worstClassification}`;
|
|
@@ -1432,12 +1517,16 @@ function stallAlertDuplicateKey(request, summary) {
|
|
|
1432
1517
|
function stallAlertText(summary) {
|
|
1433
1518
|
const lines = [];
|
|
1434
1519
|
lines.push("FlowDesk");
|
|
1520
|
+
const progressCardCount = summary.workflowSummaries.reduce((sum, workflow) => sum + (workflow.laneCards?.length ?? 0), 0);
|
|
1435
1521
|
if (summary.worstClassification === "stalled") {
|
|
1436
1522
|
lines.push(`Stalled lanes detected: ${summary.totalStalled} stalled, ${summary.totalLate} progressing-late.`);
|
|
1437
1523
|
}
|
|
1438
1524
|
else if (summary.worstClassification === "progressing_late") {
|
|
1439
1525
|
lines.push(`Late-progressing lanes detected: ${summary.totalLate} late, ${summary.totalStalled} stalled.`);
|
|
1440
1526
|
}
|
|
1527
|
+
else if (progressCardCount > 0) {
|
|
1528
|
+
lines.push(`Lane progress: ${progressCardCount} lane(s) visible on the main screen.`);
|
|
1529
|
+
}
|
|
1441
1530
|
else {
|
|
1442
1531
|
lines.push(`Lane progress check: ${summary.totalStalled} stalled, ${summary.totalLate} progressing-late.`);
|
|
1443
1532
|
}
|
|
@@ -1449,8 +1538,17 @@ function stallAlertText(summary) {
|
|
|
1449
1538
|
? `${workflow.stalledLaneCount} stalled`
|
|
1450
1539
|
: `${workflow.lateLaneCount} progressing-late`;
|
|
1451
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
|
+
}
|
|
1452
1551
|
}
|
|
1453
|
-
lines.push("FlowDesk does not auto-retry, auto-abort, or auto-fallback on stall.");
|
|
1454
1552
|
if (summary.autoAbortSummaries !== undefined && summary.autoAbortSummaries.length > 0) {
|
|
1455
1553
|
lines.push("Guarded auto-abort diagnostics (evidence-only, opt-in):");
|
|
1456
1554
|
for (const line of summary.autoAbortSummaries.slice(0, 3))
|
|
@@ -1466,6 +1564,9 @@ function stallAlertText(summary) {
|
|
|
1466
1564
|
"/flowdesk-export-debug",
|
|
1467
1565
|
])
|
|
1468
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
|
+
}
|
|
1469
1570
|
return lines.join("\n");
|
|
1470
1571
|
}
|
|
1471
1572
|
function isFds1SchemaConversionProbeEnabled(options) {
|
|
@@ -2157,6 +2258,37 @@ function isProviderUsageLiveEnabled(options) {
|
|
|
2157
2258
|
const value = options?.[flowdeskProviderUsageLiveOption];
|
|
2158
2259
|
return value === true || (isRecord(value) && value.enabled === true);
|
|
2159
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
|
+
}
|
|
2160
2292
|
function providerUsageLiveConfigFromOptions(options) {
|
|
2161
2293
|
const value = options?.[flowdeskProviderUsageLiveOption];
|
|
2162
2294
|
if (!isRecord(value) || value.enabled !== true)
|
|
@@ -2292,6 +2424,166 @@ function isLaneHeartbeatWriterEnabled(options) {
|
|
|
2292
2424
|
const value = options?.[flowdeskLaneHeartbeatWriterOption];
|
|
2293
2425
|
return value === true || (isRecord(value) && value.enabled === true);
|
|
2294
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
|
+
}
|
|
2295
2587
|
function isAgentTaskRunEnabled(options) {
|
|
2296
2588
|
const value = options?.[flowdeskAgentTaskRunOption];
|
|
2297
2589
|
return value === true || (isRecord(value) && value.enabled === true);
|
|
@@ -2603,6 +2895,9 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2603
2895
|
const laneHeartbeatWriterConfigForDoctor = isLaneHeartbeatWriterEnabled(options)
|
|
2604
2896
|
? laneHeartbeatWriterConfigFromOptions(options)
|
|
2605
2897
|
: undefined;
|
|
2898
|
+
const workflowDispatchPlanConfigForDoctor = isWorkflowDispatchPlanToolEnabled(options)
|
|
2899
|
+
? workflowDispatchPlanToolConfigFromOptions(options)
|
|
2900
|
+
: undefined;
|
|
2606
2901
|
const quickReviewerRunRegistered = isQuickReviewerRunEnabled(options) &&
|
|
2607
2902
|
quickReviewerRunClientFrom(input, options) !== undefined;
|
|
2608
2903
|
const naturalLanguageTools = {
|
|
@@ -2622,14 +2917,15 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2622
2917
|
"string",
|
|
2623
2918
|
persistWorkflowId: providerUsageLiveConfigForDoctor?.persistWorkflowId,
|
|
2624
2919
|
geminiOAuthConfigured: providerUsageLiveConfigForDoctor !== undefined &&
|
|
2625
|
-
(
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
typeof process.env.FLOWDESK_GEMINI_OAUTH_CLIENT_SECRET ===
|
|
2632
|
-
|
|
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)),
|
|
2633
2929
|
hint: isProviderUsageLiveEnabled(options) &&
|
|
2634
2930
|
providerUsageLiveConfigForDoctor === undefined
|
|
2635
2931
|
? "providerUsageLive.enabled=true but no provider family configured; set providers=['claude','openai','gemini']"
|
|
@@ -2665,6 +2961,23 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2665
2961
|
? "laneHeartbeatWriter.enabled=true but no durable state root resolved; set laneHeartbeatWriter.rootDir or top-level durableStateRoot"
|
|
2666
2962
|
: undefined,
|
|
2667
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
|
+
},
|
|
2668
2981
|
chatMessageStallAlert: {
|
|
2669
2982
|
enabled: options?.[flowdeskChatMessageStallAlertOption] === true ||
|
|
2670
2983
|
(isRecord(options?.[flowdeskChatMessageStallAlertOption]) &&
|
|
@@ -2775,6 +3088,17 @@ const flowdeskServerPlugin = async (input, options) => {
|
|
|
2775
3088
|
: undefined;
|
|
2776
3089
|
if (laneHeartbeatWriterConfig !== undefined)
|
|
2777
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));
|
|
2778
3102
|
const agentTaskRunEnabled = isAgentTaskRunEnabled(options);
|
|
2779
3103
|
if (agentTaskRunEnabled) {
|
|
2780
3104
|
const agentTaskRunClient = isRecord(input) && isManagedDispatchBetaClient(input.client) ? input.client : undefined;
|
|
@@ -2933,6 +3257,13 @@ function chatMessageStallAlertOptionsFrom(options, statusLiveConfig, sdkClient)
|
|
|
2933
3257
|
if (recordRaw !== undefined &&
|
|
2934
3258
|
typeof recordRaw.includeProgressingLate === "boolean")
|
|
2935
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)));
|
|
2936
3267
|
if (recordRaw !== undefined && isRecord(recordRaw.guardedAutoAbort)) {
|
|
2937
3268
|
const rawGuard = recordRaw.guardedAutoAbort;
|
|
2938
3269
|
const guardedAutoAbort = {
|