@flowdesk/opencode-plugin 0.1.8 → 0.1.10

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.
@@ -1,4 +1,6 @@
1
- import { applyFlowDeskSessionEvidenceWriteIntentsV1, applyWriteIntentsToDurableState, applyWriteIntentsToInMemoryState, evaluateFlowDeskProductionEnablementV1, invalid, loadFlowDeskDurableWorkflowState, mergePolicyPacksV1, planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1, prepareAttemptRecordWriteIntent, prepareDebugExportManifestWriteIntent, prepareFlowDeskSessionEvidenceWriteIntentV1, prepareLaneRecordWriteIntent, prepareWorkflowActiveWriteIntent, prepareWorkflowRecordWriteIntent, reloadFlowDeskSessionEvidenceV1, valid } from "@flowdesk/core";
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve, sep } from "node:path";
3
+ import { applyFlowDeskSessionEvidenceWriteIntentsV1, applyWriteIntentsToDurableState, applyWriteIntentsToInMemoryState, evaluateFlowDeskProductionEnablementV1, invalid, loadFlowDeskDurableWorkflowState, mergePolicyPacksV1, planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1, prepareAttemptRecordWriteIntent, prepareDebugExportManifestWriteIntent, prepareDebugSectionFileWriteIntent, prepareFlowDeskSessionEvidenceWriteIntentV1, prepareLaneRecordWriteIntent, prepareWorkflowActiveWriteIntent, prepareWorkflowRecordWriteIntent, reloadFlowDeskSessionEvidenceV1, valid, validatePolicyPackV1, validateProjectConfigV1, } from "@flowdesk/core";
2
4
  import { evaluateFlowDeskCommandBackedHandlerV1 } from "./command-handlers.js";
3
5
  export const flowdeskLocalNonDispatchAdapterProfile = "local_non_dispatch_command_adapter";
4
6
  export function hasFlowDeskLocalPlanningEvidenceV1(session, workflowId, sessionRef) {
@@ -14,7 +16,9 @@ export function hasFlowDeskLocalPlanningEvidenceV1(session, workflowId, sessionR
14
16
  lane.lane_class === "planning_draft" &&
15
17
  (workflowId === undefined || lane.workflow_id === workflowId)))
16
18
  return true;
17
- const rootDir = typeof state.durableStateRootDir === "string" ? state.durableStateRootDir : undefined;
19
+ const rootDir = typeof state.durableStateRootDir === "string"
20
+ ? state.durableStateRootDir
21
+ : undefined;
18
22
  if (rootDir === undefined || workflowId === undefined)
19
23
  return false;
20
24
  const loaded = loadFlowDeskDurableWorkflowState(rootDir, {
@@ -32,7 +36,7 @@ const disabledAuthority = {
32
36
  providerCall: false,
33
37
  runtimeExecution: false,
34
38
  fallbackAuthority: false,
35
- hardCancelOrNoReplyAuthority: false
39
+ hardCancelOrNoReplyAuthority: false,
36
40
  };
37
41
  const taxonomy = {
38
42
  primary_category: "coding",
@@ -44,7 +48,7 @@ const taxonomy = {
44
48
  domain_uncertainty: "low",
45
49
  verification_hardness: "low",
46
50
  operational_risk: "low",
47
- policy_professional_boundary: "ordinary"
51
+ policy_professional_boundary: "ordinary",
48
52
  };
49
53
  function isRecord(value) {
50
54
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -67,14 +71,18 @@ function id(prefix, request, fallback = "local") {
67
71
  return `${prefix}-${safeToken(request.request_id ?? request.workflow_id, fallback)}`;
68
72
  }
69
73
  function workflowIdFrom(request) {
70
- return isRecord(request) && typeof request.workflow_id === "string" && request.workflow_id.length > 0 ? request.workflow_id : "workflow-local";
74
+ return isRecord(request) &&
75
+ typeof request.workflow_id === "string" &&
76
+ request.workflow_id.length > 0
77
+ ? request.workflow_id
78
+ : "workflow-local";
71
79
  }
72
80
  function nowParts(now = new Date()) {
73
81
  const nowMs = now.getTime();
74
82
  return {
75
83
  nowIso: now.toISOString(),
76
84
  expiresAtIso: new Date(nowMs + 24 * 60 * 60 * 1000).toISOString(),
77
- nowMs
85
+ nowMs,
78
86
  };
79
87
  }
80
88
  function retention() {
@@ -83,7 +91,7 @@ function retention() {
83
91
  debug_staging_max_days: 7,
84
92
  conformance_summary_max_days: 30,
85
93
  allow_user_longer_retention: false,
86
- deletion_behavior: "delete_after_expiry"
94
+ deletion_behavior: "delete_after_expiry",
87
95
  };
88
96
  }
89
97
  function usagePolicy() {
@@ -95,7 +103,7 @@ function usagePolicy() {
95
103
  shared_limit_suspected_dispatchability: "non_dispatchable",
96
104
  fallback_derived_dispatchability: "non_dispatchable",
97
105
  allow_local_history_source: false,
98
- allow_provider_console_scraping: false
106
+ allow_provider_console_scraping: false,
99
107
  };
100
108
  }
101
109
  function providerHealthPolicy() {
@@ -105,7 +113,7 @@ function providerHealthPolicy() {
105
113
  degraded_dispatchability: "diagnostic_only",
106
114
  opencode_go_usage_without_official_quota: "unknown",
107
115
  z_ai_usage_without_official_quota: "unknown",
108
- allow_automatic_provider_fallback: false
116
+ allow_automatic_provider_fallback: false,
109
117
  };
110
118
  }
111
119
  function projectConfig(parts) {
@@ -124,9 +132,14 @@ function projectConfig(parts) {
124
132
  retention: retention(),
125
133
  usage_policy: usagePolicy(),
126
134
  provider_health_policy: providerHealthPolicy(),
127
- disabled_modes: ["real_dispatch", "managed_fallback", "lane_launch", "hard_chat_blocking"],
135
+ disabled_modes: [
136
+ "real_dispatch",
137
+ "managed_fallback",
138
+ "lane_launch",
139
+ "hard_chat_blocking",
140
+ ],
128
141
  extension_namespaces: ["flowdesk.project"],
129
- audit_refs: ["audit-local"]
142
+ audit_refs: ["audit-local"],
130
143
  };
131
144
  }
132
145
  function policyPack() {
@@ -139,15 +152,139 @@ function policyPack() {
139
152
  source_ref: "policy-source-local",
140
153
  applies_to_release_modes: ["release1"],
141
154
  priority: 1,
142
- rules: [{ rule_id: "rule-local-approval", effect: "require_approval", target: "permission_class", summary_label: "Require scoped non-dispatch approval for local writes.", refs: ["approval-local"] }],
155
+ rules: [
156
+ {
157
+ rule_id: "rule-local-approval",
158
+ effect: "require_approval",
159
+ target: "permission_class",
160
+ summary_label: "Require scoped non-dispatch approval for local writes.",
161
+ refs: ["approval-local"],
162
+ },
163
+ ],
143
164
  hard_ban_refs: ["ban-real-dispatch-local"],
144
165
  allowed_extension_namespaces: ["flowdesk.project"],
145
- redaction_baseline_ref: "redaction-local"
166
+ redaction_baseline_ref: "redaction-local",
146
167
  };
147
168
  }
148
- function effectivePolicy(parts) {
169
+ function defaultLocalPolicyContext(parts, errors = []) {
149
170
  const config = projectConfig(parts);
150
- return mergePolicyPacksV1(config, [policyPack()], { effectivePolicyId: "effective-local", computedAt: parts.nowIso, auditRef: "audit-local" });
171
+ const packs = [policyPack()];
172
+ return {
173
+ config,
174
+ policyPacks: packs,
175
+ effectivePolicy: mergePolicyPacksV1(config, packs, {
176
+ effectivePolicyId: "effective-local",
177
+ computedAt: parts.nowIso,
178
+ auditRef: "audit-local",
179
+ }),
180
+ configHash: config.config_hash,
181
+ policyPackId: packs[0].policy_pack_id,
182
+ policyPackHash: packs[0].policy_pack_hash,
183
+ policyPackHashes: packs.map((pack) => pack.policy_pack_hash),
184
+ projectRootRef: config.project_root_ref,
185
+ auditRef: "audit-local",
186
+ scopeRef: "scope-local",
187
+ errors,
188
+ };
189
+ }
190
+ function resolveInsideRoot(rootDir, relativeOrAbsolutePath) {
191
+ const root = resolve(rootDir);
192
+ const target = resolve(root, relativeOrAbsolutePath);
193
+ return target === root || target.startsWith(`${root}${sep}`)
194
+ ? target
195
+ : undefined;
196
+ }
197
+ function parseJsonFile(path) {
198
+ try {
199
+ return { value: JSON.parse(readFileSync(path, "utf8")) };
200
+ }
201
+ catch (error) {
202
+ const code = error instanceof Error && "code" in error
203
+ ? String(error.code)
204
+ : "read_or_parse_failed";
205
+ return {
206
+ error: code === "ENOENT" ? "file_missing" : "file_unreadable_or_invalid_json",
207
+ };
208
+ }
209
+ }
210
+ function loadLocalPolicyContext(parts, options) {
211
+ if (options?.enabled !== true)
212
+ return defaultLocalPolicyContext(parts);
213
+ const rootDir = typeof options.rootDir === "string" && options.rootDir.trim().length > 0
214
+ ? options.rootDir
215
+ : undefined;
216
+ if (rootDir === undefined)
217
+ return defaultLocalPolicyContext(parts, ["project_config_root_missing"]);
218
+ const configPath = resolveInsideRoot(rootDir, ".flowdesk/config.json");
219
+ if (configPath === undefined)
220
+ return defaultLocalPolicyContext(parts, ["project_config_path_unsafe"]);
221
+ const configJson = parseJsonFile(configPath);
222
+ if (configJson.error !== undefined)
223
+ return defaultLocalPolicyContext(parts, [
224
+ `project_config_${configJson.error}`,
225
+ ]);
226
+ const configValidation = validateProjectConfigV1(configJson.value);
227
+ if (!configValidation.ok)
228
+ return defaultLocalPolicyContext(parts, configValidation.errors.map((error) => `project_config_${error}`));
229
+ const config = configJson.value;
230
+ const policyPackPaths = options.policyPackPaths ?? [];
231
+ if (policyPackPaths.length !== config.policy_pack_hashes.length)
232
+ return defaultLocalPolicyContext(parts, [
233
+ "policy_pack_file_count_mismatch",
234
+ ]);
235
+ const policyPacks = [];
236
+ const errors = [];
237
+ for (const [index, policyPackPath] of policyPackPaths.entries()) {
238
+ if (typeof policyPackPath !== "string" ||
239
+ policyPackPath.trim().length === 0) {
240
+ errors.push(`policy_pack_path_${index}_invalid`);
241
+ continue;
242
+ }
243
+ const resolved = resolveInsideRoot(rootDir, policyPackPath);
244
+ if (resolved === undefined) {
245
+ errors.push(`policy_pack_path_${index}_unsafe`);
246
+ continue;
247
+ }
248
+ const policyJson = parseJsonFile(resolved);
249
+ if (policyJson.error !== undefined) {
250
+ errors.push(`policy_pack_${index}_${policyJson.error}`);
251
+ continue;
252
+ }
253
+ const policyValidation = validatePolicyPackV1(policyJson.value, {
254
+ expectedPolicyPackHashes: config.policy_pack_hashes,
255
+ });
256
+ if (!policyValidation.ok) {
257
+ errors.push(...policyValidation.errors.map((error) => `policy_pack_${index}_${error}`));
258
+ continue;
259
+ }
260
+ policyPacks.push(policyJson.value);
261
+ }
262
+ const loadedHashes = policyPacks.map((pack) => pack.policy_pack_hash);
263
+ for (const expectedHash of config.policy_pack_hashes) {
264
+ if (!loadedHashes.includes(expectedHash))
265
+ errors.push(`policy_pack_hash_not_loaded_${expectedHash}`);
266
+ }
267
+ if (errors.length > 0)
268
+ return defaultLocalPolicyContext(parts, errors);
269
+ const auditRef = config.audit_refs[0] ?? "audit-local";
270
+ const effectivePolicy = mergePolicyPacksV1(config, policyPacks, {
271
+ effectivePolicyId: `effective-${config.config_id}`,
272
+ computedAt: parts.nowIso,
273
+ auditRef,
274
+ });
275
+ return {
276
+ config,
277
+ policyPacks,
278
+ effectivePolicy,
279
+ configHash: config.config_hash,
280
+ policyPackId: policyPacks[0]?.policy_pack_id ?? "policy-none",
281
+ policyPackHash: policyPacks[0]?.policy_pack_hash ?? "policy-none",
282
+ policyPackHashes: policyPacks.map((pack) => pack.policy_pack_hash),
283
+ projectRootRef: config.project_root_ref,
284
+ auditRef,
285
+ scopeRef: "scope-local",
286
+ errors: [],
287
+ };
151
288
  }
152
289
  export function createFlowDeskLocalNonDispatchPermissionProvider() {
153
290
  return ({ nowIso, expiresAtIso, workflowId, permissionClass }) => ({
@@ -162,22 +299,33 @@ export function createFlowDeskLocalNonDispatchPermissionProvider() {
162
299
  config_hash: "config-hash-local",
163
300
  policy_pack_hash: "policy-hash-local",
164
301
  release_mode: "release1",
165
- audit_ref: "audit-local"
302
+ audit_ref: "audit-local",
166
303
  });
167
304
  }
168
- function permission(provider, parts, workflowId, permissionClass) {
169
- return provider({ nowIso: parts.nowIso, expiresAtIso: parts.expiresAtIso, workflowId, permissionClass });
305
+ function permission(provider, parts, workflowId, permissionClass, policyContext) {
306
+ return {
307
+ ...provider({
308
+ nowIso: parts.nowIso,
309
+ expiresAtIso: parts.expiresAtIso,
310
+ workflowId,
311
+ permissionClass,
312
+ }),
313
+ scope_ref: policyContext.scopeRef,
314
+ config_hash: policyContext.configHash,
315
+ policy_pack_hash: policyContext.policyPackHash,
316
+ audit_ref: policyContext.auditRef,
317
+ };
170
318
  }
171
- function guardBoundary(provider, parts, workflowId, permissionClass) {
319
+ function guardBoundary(provider, parts, workflowId, permissionClass, policyContext) {
172
320
  return {
173
- configHash: "config-hash-local",
174
- scopeRef: "scope-local",
175
- policy: effectivePolicy(parts),
176
- auditRef: "audit-local",
321
+ configHash: policyContext.configHash,
322
+ scopeRef: policyContext.scopeRef,
323
+ policy: policyContext.effectivePolicy,
324
+ auditRef: policyContext.auditRef,
177
325
  conformanceRef: "conformance-local-non-dispatch",
178
326
  runtimeCapabilityRef: "runtime-local-fake",
179
- nonDispatchPermission: permission(provider, parts, workflowId, permissionClass),
180
- now: parts.nowMs
327
+ nonDispatchPermission: permission(provider, parts, workflowId, permissionClass, policyContext),
328
+ now: parts.nowMs,
181
329
  };
182
330
  }
183
331
  function providerHealth(parts) {
@@ -194,7 +342,7 @@ function providerHealth(parts) {
194
342
  failure_class: "none",
195
343
  dispatchability: "diagnostic_only",
196
344
  source_ref: "health-local-source",
197
- safe_remediation: "Use FlowDesk local non-dispatch diagnostics before any retry."
345
+ safe_remediation: "Use FlowDesk local non-dispatch diagnostics before any retry.",
198
346
  };
199
347
  }
200
348
  function makePlanContext(request, parts) {
@@ -211,7 +359,7 @@ function makePlanContext(request, parts) {
211
359
  auditRef: "audit-local",
212
360
  routeRef: id("route", request),
213
361
  nowIso: parts.nowIso,
214
- taxonomy
362
+ taxonomy,
215
363
  };
216
364
  }
217
365
  function laneObservabilityRefFor(request, laneClass) {
@@ -219,13 +367,13 @@ function laneObservabilityRefFor(request, laneClass) {
219
367
  ? id("lane-observability", request)
220
368
  : id("lane-observability-run", request);
221
369
  }
222
- function makeRunContext(request, parts, permissionProvider) {
370
+ function makeRunContext(request, parts, permissionProvider, policyContext) {
223
371
  const workflowId = workflowIdFrom(request);
224
372
  const runMode = request.run_mode;
225
373
  if (runMode === "guarded-dry-run") {
226
374
  return {
227
375
  guardedDryRun: {
228
- guardBoundary: guardBoundary(permissionProvider, parts, workflowId, "audit_write"),
376
+ guardBoundary: guardBoundary(permissionProvider, parts, workflowId, "audit_write", policyContext),
229
377
  sessionId: "session-local",
230
378
  attemptId: id("attempt", request),
231
379
  auditEventId: id("event-audit", request),
@@ -235,17 +383,17 @@ function makeRunContext(request, parts, permissionProvider) {
235
383
  commandShapeHash: id("shape", request),
236
384
  runResultRef: id("run-result", request),
237
385
  verificationSummaryRef: id("verification", request),
238
- redactionVersion: "redaction-v1"
239
- }
386
+ redactionVersion: "redaction-v1",
387
+ },
240
388
  };
241
389
  }
242
390
  if (runMode === "managed-dispatch")
243
391
  return {};
244
392
  return {
245
393
  fakeRuntime: {
246
- guardBoundary: guardBoundary(permissionProvider, parts, workflowId, "fake_runtime_write"),
247
- auditWritePermission: permission(permissionProvider, parts, workflowId, "audit_write"),
248
- stateWritePermission: permission(permissionProvider, parts, workflowId, "state_write"),
394
+ guardBoundary: guardBoundary(permissionProvider, parts, workflowId, "fake_runtime_write", policyContext),
395
+ auditWritePermission: permission(permissionProvider, parts, workflowId, "audit_write", policyContext),
396
+ stateWritePermission: permission(permissionProvider, parts, workflowId, "state_write", policyContext),
249
397
  sessionId: "session-local",
250
398
  attemptId: id("attempt", request),
251
399
  auditEventId: id("event-audit", request),
@@ -264,21 +412,29 @@ function makeRunContext(request, parts, permissionProvider) {
264
412
  laneSummaryRef: id("lane-summary-run", request),
265
413
  laneObservabilityRef: laneObservabilityRefFor(request, "verification"),
266
414
  laneEventRef: id("event-run", request),
267
- laneDebugRef: "debug-local"
268
- }
415
+ laneDebugRef: "debug-local",
416
+ },
269
417
  };
270
418
  }
271
419
  function laneRecordFor(request, parts, laneClass) {
272
420
  const workflowId = workflowIdFrom(request);
273
- const planRevisionId = typeof request.plan_revision_id === "string" ? request.plan_revision_id : id("plan", request);
421
+ const planRevisionId = typeof request.plan_revision_id === "string"
422
+ ? request.plan_revision_id
423
+ : id("plan", request);
274
424
  const observabilityRef = laneObservabilityRefFor(request, laneClass);
275
425
  return {
276
426
  schema_version: "flowdesk.lane_record.v1",
277
- lane_id: laneClass === "planning_draft" ? id("lane-plan", request) : id("lane-run", request),
427
+ lane_id: laneClass === "planning_draft"
428
+ ? id("lane-plan", request)
429
+ : id("lane-run", request),
278
430
  workflow_id: workflowId,
279
431
  plan_revision_id: planRevisionId,
280
- ...(laneClass === "verification" ? { attempt_id: id("attempt", request) } : {}),
281
- task_ref: laneClass === "planning_draft" ? id("task-plan", request) : id("task-run", request),
432
+ ...(laneClass === "verification"
433
+ ? { attempt_id: id("attempt", request) }
434
+ : {}),
435
+ task_ref: laneClass === "planning_draft"
436
+ ? id("task-plan", request)
437
+ : id("task-run", request),
282
438
  lane_class: laneClass,
283
439
  state: "completed",
284
440
  created_at: parts.nowIso,
@@ -286,15 +442,25 @@ function laneRecordFor(request, parts, laneClass) {
286
442
  updated_at: parts.nowIso,
287
443
  completed_at: parts.nowIso,
288
444
  safe_next_action: "/flowdesk-status",
289
- refs: [laneClass === "planning_draft" ? id("lane-summary", request) : id("lane-summary-run", request), observabilityRef],
290
- event_refs: [laneClass === "planning_draft" ? id("event-plan", request) : id("event-run", request)],
445
+ refs: [
446
+ laneClass === "planning_draft"
447
+ ? id("lane-summary", request)
448
+ : id("lane-summary-run", request),
449
+ observabilityRef,
450
+ ],
451
+ event_refs: [
452
+ laneClass === "planning_draft"
453
+ ? id("event-plan", request)
454
+ : id("event-run", request),
455
+ ],
291
456
  audit_refs: ["audit-local"],
292
457
  observability_ref: observabilityRef,
293
- ...(laneClass === "verification" ? { debug_ref: "debug-local" } : {})
458
+ ...(laneClass === "verification" ? { debug_ref: "debug-local" } : {}),
294
459
  };
295
460
  }
296
461
  function requiresRunConfirmation(request) {
297
- return typeof request.risk_hint === "string" && /requires explicit user confirmation/i.test(request.risk_hint);
462
+ return (typeof request.risk_hint === "string" &&
463
+ /requires explicit user confirmation/i.test(request.risk_hint));
298
464
  }
299
465
  function pendingApprovalRef(request) {
300
466
  return id("approval", request);
@@ -307,12 +473,16 @@ function createPendingConfirmation(request, parts, planRevisionId) {
307
473
  status: "pending",
308
474
  approvalRef: pendingApprovalRef(request),
309
475
  workflowId: workflowIdFrom(request),
310
- ...(typeof request.session_ref === "string" ? { sessionRef: request.session_ref } : {}),
311
- ...(typeof request.redacted_intake_ref === "string" ? { redactedIntakeRef: request.redacted_intake_ref } : {}),
476
+ ...(typeof request.session_ref === "string"
477
+ ? { sessionRef: request.session_ref }
478
+ : {}),
479
+ ...(typeof request.redacted_intake_ref === "string"
480
+ ? { redactedIntakeRef: request.redacted_intake_ref }
481
+ : {}),
312
482
  sourceSummaryHash: safeHash(request.goal_summary ?? request.intake_summary),
313
483
  planRevisionId,
314
484
  createdAtIso: parts.nowIso,
315
- expiresAtIso: pendingExpiresAt(parts)
485
+ expiresAtIso: pendingExpiresAt(parts),
316
486
  };
317
487
  }
318
488
  function matchesOptionalScope(expected, actual) {
@@ -328,8 +498,10 @@ function validatePendingConfirmation(state, request, parts) {
328
498
  pending.status = "expired";
329
499
  return invalid("pending confirmation expired");
330
500
  }
331
- const summaryMatches = pending.sourceSummaryHash === safeHash(request.goal_summary ?? request.intake_summary);
332
- const sourceRefMatches = pending.redactedIntakeRef !== undefined && request.redacted_intake_ref === pending.redactedIntakeRef;
501
+ const summaryMatches = pending.sourceSummaryHash ===
502
+ safeHash(request.goal_summary ?? request.intake_summary);
503
+ const sourceRefMatches = pending.redactedIntakeRef !== undefined &&
504
+ request.redacted_intake_ref === pending.redactedIntakeRef;
333
505
  const errors = [];
334
506
  if (request.user_approval_ref !== pending.approvalRef)
335
507
  errors.push("user_approval_ref does not match pending confirmation");
@@ -342,11 +514,16 @@ function validatePendingConfirmation(state, request, parts) {
342
514
  return errors.length === 0 ? valid() : invalid(...errors);
343
515
  }
344
516
  function shouldGateRun(state, request) {
345
- return request.input_mode === "chat_routed" || state.workflow?.state === "plan_pending_approval";
517
+ return (request.input_mode === "chat_routed" ||
518
+ state.workflow?.state === "plan_pending_approval");
346
519
  }
347
520
  function consumePendingConfirmation(state, parts) {
348
521
  if (state.pendingConfirmation !== undefined) {
349
- state.pendingConfirmation = { ...state.pendingConfirmation, status: "consumed", consumedAtIso: parts.nowIso };
522
+ state.pendingConfirmation = {
523
+ ...state.pendingConfirmation,
524
+ status: "consumed",
525
+ consumedAtIso: parts.nowIso,
526
+ };
350
527
  }
351
528
  }
352
529
  function cancelPendingConfirmation(state, request, parts) {
@@ -357,7 +534,11 @@ function cancelPendingConfirmation(state, request, parts) {
357
534
  return;
358
535
  if (!matchesOptionalScope(pending.sessionRef, request.session_ref))
359
536
  return;
360
- state.pendingConfirmation = { ...pending, status: "cancelled", cancelledAtIso: parts.nowIso };
537
+ state.pendingConfirmation = {
538
+ ...pending,
539
+ status: "cancelled",
540
+ cancelledAtIso: parts.nowIso,
541
+ };
361
542
  }
362
543
  function applyAdapterWriteIntents(state, intents, parts) {
363
544
  try {
@@ -381,50 +562,67 @@ function applyAdapterWriteIntents(state, intents, parts) {
381
562
  }
382
563
  function updateWorkflowState(state, request, parts, nextState) {
383
564
  const workflowId = workflowIdFrom(request);
384
- const planRevisionId = typeof request.plan_revision_id === "string" ? request.plan_revision_id : id("plan", request);
565
+ const planRevisionId = typeof request.plan_revision_id === "string"
566
+ ? request.plan_revision_id
567
+ : id("plan", request);
385
568
  const laneRefs = state.laneRecords.flatMap((record) => record.refs);
386
569
  const pendingConfirmation = nextState === "plan_pending_approval";
387
570
  const workflow = {
388
571
  schema_version: "flowdesk.workflow_record.v1",
389
572
  workflow_id: workflowId,
390
- session_ref: typeof request.session_ref === "string" ? request.session_ref : "session-local-ref",
573
+ session_ref: typeof request.session_ref === "string"
574
+ ? request.session_ref
575
+ : "session-local-ref",
391
576
  created_at: state.workflow?.created_at ?? parts.nowIso,
392
577
  updated_at: parts.nowIso,
393
578
  state: nextState,
394
579
  latest_plan_revision_id: planRevisionId,
395
- current_step_id: typeof request.step_id === "string" ? request.step_id : id("step-plan", request),
396
- project_root_ref: "project-root-local",
397
- config_hash: "config-hash-local",
398
- policy_pack_id: "policy-local",
399
- policy_pack_hash: "policy-hash-local",
400
- ...(nextState === "complete" ? { current_attempt_id: id("attempt", request) } : {}),
580
+ current_step_id: typeof request.step_id === "string"
581
+ ? request.step_id
582
+ : id("step-plan", request),
583
+ project_root_ref: state.policyContext.projectRootRef,
584
+ config_hash: state.policyContext.configHash,
585
+ policy_pack_id: state.policyContext.policyPackId,
586
+ policy_pack_hash: state.policyContext.policyPackHash,
587
+ ...(nextState === "complete"
588
+ ? { current_attempt_id: id("attempt", request) }
589
+ : {}),
401
590
  attempt_refs: nextState === "complete" ? [id("attempt", request)] : [],
402
591
  checkpoint_refs: [],
403
592
  lane_refs: laneRefs,
404
593
  latest_lane_summary_refs: laneRefs,
405
- audit_refs: ["audit-local"],
594
+ audit_refs: [state.policyContext.auditRef],
406
595
  status_summary_ref: "status-summary-local",
407
596
  artifact_disposition: "quarantined",
408
- ...(pendingConfirmation ? {
409
- blocker_summary: {
410
- category: "policy",
411
- summary: "Execution-like chat intake is waiting for explicit user confirmation before any run.",
412
- safe_remediation: "Review the FlowDesk plan and provide typed confirmation before using /flowdesk-run.",
413
- refs: [id("confirmation-pending", request)]
597
+ ...(pendingConfirmation
598
+ ? {
599
+ blocker_summary: {
600
+ category: "policy",
601
+ summary: "Execution-like chat intake is waiting for explicit user confirmation before any run.",
602
+ safe_remediation: "Review the FlowDesk plan and provide typed confirmation before using /flowdesk-run.",
603
+ refs: [id("confirmation-pending", request)],
604
+ },
414
605
  }
415
- } : {}),
416
- safe_next_actions: pendingConfirmation ? ["/flowdesk-plan", "/flowdesk-status"] : ["/flowdesk-status"]
606
+ : {}),
607
+ safe_next_actions: pendingConfirmation
608
+ ? ["/flowdesk-plan", "/flowdesk-status"]
609
+ : ["/flowdesk-status"],
417
610
  };
418
611
  const active = {
419
612
  schema_version: "flowdesk.workflow_active.v1",
420
613
  active_workflow_id: workflowId,
421
- ...(nextState === "complete" ? { active_attempt_id: id("attempt", request) } : {}),
614
+ ...(nextState === "complete"
615
+ ? { active_attempt_id: id("attempt", request) }
616
+ : {}),
422
617
  state: nextState,
423
618
  updated_at: parts.nowIso,
424
619
  status_summary_ref: "status-summary-local",
425
- audit_refs: ["audit-local"]
620
+ audit_refs: [state.policyContext.auditRef],
426
621
  };
427
- const intents = [prepareWorkflowActiveWriteIntent(active).writeIntent, prepareWorkflowRecordWriteIntent(workflow).writeIntent].filter((intent) => intent !== undefined);
622
+ const intents = [
623
+ prepareWorkflowActiveWriteIntent(active).writeIntent,
624
+ prepareWorkflowRecordWriteIntent(workflow).writeIntent,
625
+ ].filter((intent) => intent !== undefined);
428
626
  if (intents.length !== 2)
429
627
  return false;
430
628
  if (!applyAdapterWriteIntents(state, intents, parts))
@@ -448,7 +646,9 @@ function recordPlanningState(state, request, parts) {
448
646
  return false;
449
647
  }
450
648
  if (pendingConfirmation)
451
- state.pendingConfirmation = createPendingConfirmation(request, parts, typeof request.plan_revision_id === "string" ? request.plan_revision_id : id("plan", request));
649
+ state.pendingConfirmation = createPendingConfirmation(request, parts, typeof request.plan_revision_id === "string"
650
+ ? request.plan_revision_id
651
+ : id("plan", request));
452
652
  return true;
453
653
  }
454
654
  function recordRunState(state, request, parts, permissionProvider) {
@@ -457,23 +657,29 @@ function recordRunState(state, request, parts, permissionProvider) {
457
657
  schema_version: "flowdesk.attempt_record.v1",
458
658
  attempt_id: id("attempt", request),
459
659
  workflow_id: workflowId,
460
- ...(typeof request.step_id === "string" ? { step_id: request.step_id } : {}),
660
+ ...(typeof request.step_id === "string"
661
+ ? { step_id: request.step_id }
662
+ : {}),
461
663
  created_at: parts.nowIso,
462
664
  updated_at: parts.nowIso,
463
- run_mode: request.run_mode === "guarded-dry-run" ? "guarded-dry-run" : "fake-runtime",
665
+ run_mode: request.run_mode === "guarded-dry-run"
666
+ ? "guarded-dry-run"
667
+ : "fake-runtime",
464
668
  state_at_start: "ready_to_run",
465
669
  state_at_end: "complete",
466
670
  attempt_state: "complete",
467
671
  guard_decision_ref: id("decision", request),
468
- non_dispatch_permission_ref: permission(permissionProvider, parts, workflowId, request.run_mode === "guarded-dry-run" ? "audit_write" : "fake_runtime_write").permission_id,
672
+ non_dispatch_permission_ref: permission(permissionProvider, parts, workflowId, request.run_mode === "guarded-dry-run"
673
+ ? "audit_write"
674
+ : "fake_runtime_write", state.policyContext).permission_id,
469
675
  command_shape_hash: id("shape", request),
470
676
  runtime_capability_ref: "runtime-local-fake",
471
- pre_run_audit_ref: "audit-local",
677
+ pre_run_audit_ref: state.policyContext.auditRef,
472
678
  runtime_echo_validation: "not_applicable",
473
679
  verification_ref: id("verification", request),
474
680
  artifact_disposition: "quarantined",
475
681
  outcome_audit_ref: "audit-outcome-local",
476
- safe_next_actions: ["/flowdesk-status", "/flowdesk-export-debug"]
682
+ safe_next_actions: ["/flowdesk-status", "/flowdesk-export-debug"],
477
683
  };
478
684
  const lane = laneRecordFor(request, parts, "verification");
479
685
  const attemptIntent = prepareAttemptRecordWriteIntent(attempt).writeIntent;
@@ -485,7 +691,10 @@ function recordRunState(state, request, parts, permissionProvider) {
485
691
  const previousAttempt = state.currentAttempt;
486
692
  const previousLaneRecords = state.laneRecords;
487
693
  state.currentAttempt = attempt;
488
- state.laneRecords = [...state.laneRecords.filter((record) => record.lane_id !== lane.lane_id), lane];
694
+ state.laneRecords = [
695
+ ...state.laneRecords.filter((record) => record.lane_id !== lane.lane_id),
696
+ lane,
697
+ ];
489
698
  if (!updateWorkflowState(state, request, parts, "complete")) {
490
699
  state.currentAttempt = previousAttempt;
491
700
  state.laneRecords = previousLaneRecords;
@@ -493,46 +702,163 @@ function recordRunState(state, request, parts, permissionProvider) {
493
702
  }
494
703
  return true;
495
704
  }
705
+ function debugSectionSummaryLabels(section, state) {
706
+ const labels = [`flowdesk debug section ${section}`];
707
+ switch (section) {
708
+ case "doctor":
709
+ labels.push("disabled_modes: real_dispatch managed_fallback lane_launch hard_chat_blocking", "production_enablement: disabled", "command_backed_runs: ready");
710
+ break;
711
+ case "conformance":
712
+ labels.push("plugin_package: @flowdesk/opencode-plugin", "release_mode: release1", "dispatch_mode: command-steering");
713
+ break;
714
+ case "workflow_state": {
715
+ const workflowId = state.workflow?.workflow_id;
716
+ const workflowState = state.workflow?.state;
717
+ labels.push(workflowId
718
+ ? `workflow_id: ${workflowId}`
719
+ : "workflow_id: <none>", workflowState
720
+ ? `workflow_state: ${workflowState}`
721
+ : "workflow_state: <none>", `lane_records: ${state.laneRecords.length}`);
722
+ break;
723
+ }
724
+ case "audit_refs": {
725
+ const auditRefs = Array.isArray(state.workflow?.audit_refs)
726
+ ? state.workflow?.audit_refs
727
+ : [];
728
+ labels.push(`audit_ref_count: ${auditRefs.length}`, ...auditRefs
729
+ .slice(0, 3)
730
+ .map((ref) => `audit_ref: ${ref}`));
731
+ break;
732
+ }
733
+ case "usage_summary":
734
+ labels.push("usage_unknown_default: non_dispatchable", "usage_stale_default: non_dispatchable", "provider_console_scraping: disabled");
735
+ break;
736
+ case "policy_summary": {
737
+ const policyContext = state.policyContext;
738
+ const policyPackHashes = policyContext.config.policy_pack_hashes ?? [];
739
+ labels.push(`config_hash: ${policyContext.configHash}`, `release_mode: ${policyContext.config.release_mode}`, `loaded_policy_packs: ${policyContext.policyPacks.length}`, `policy_pack_hashes: ${policyPackHashes.length}`);
740
+ break;
741
+ }
742
+ case "redaction_summary":
743
+ labels.push("redaction_version: redaction-v1", "raw_payload_markers: blocked", "absolute_path_markers: blocked");
744
+ break;
745
+ }
746
+ return labels;
747
+ }
496
748
  function recordDebugExportManifestState(state, request, response, parts) {
497
749
  if (!isRecord(response))
498
750
  return false;
499
751
  const exportId = safeToken(response.export_manifest_ref, id("debug-export", request));
500
- const includedSections = Array.isArray(response.included_sections) ? response.included_sections : [];
752
+ const rawSections = Array.isArray(response.included_sections)
753
+ ? response.included_sections
754
+ : [];
755
+ const workflowId = typeof request.workflow_id === "string" ? request.workflow_id : undefined;
756
+ const sessionRef = typeof request.session_ref === "string" ? request.session_ref : undefined;
757
+ const sourceRefs = [
758
+ safeToken(request.redacted_intake_ref, "debug-export-request"),
759
+ ];
760
+ const sectionFiles = [];
761
+ const updatedSummaries = [];
762
+ for (const entry of rawSections) {
763
+ if (!isRecord(entry) || typeof entry.section !== "string")
764
+ continue;
765
+ const sectionName = entry.section;
766
+ const sectionRef = `debug-section-file-${exportId}-${sectionName.replaceAll("_", "-")}`;
767
+ const redactionStatus = entry.redaction_status === "partial" ||
768
+ entry.redaction_status === "blocked"
769
+ ? entry.redaction_status
770
+ : "passed";
771
+ const warningCount = typeof entry.warning_count === "number" && entry.warning_count >= 0
772
+ ? entry.warning_count
773
+ : 0;
774
+ const excludedCategories = Array.isArray(entry.excluded_categories)
775
+ ? entry.excluded_categories
776
+ : [];
777
+ const sectionFile = {
778
+ schema_version: "flowdesk.debug_section_file.v1",
779
+ export_id: exportId,
780
+ section: sectionName,
781
+ ref: sectionRef,
782
+ ...(workflowId !== undefined ? { workflow_id: workflowId } : {}),
783
+ ...(sessionRef !== undefined ? { session_ref: sessionRef } : {}),
784
+ generated_at: parts.nowIso,
785
+ redaction_version: "redaction-v1",
786
+ redaction_status: redactionStatus,
787
+ warning_count: warningCount,
788
+ excluded_categories: excludedCategories,
789
+ source_refs: [...sourceRefs],
790
+ summary_labels: debugSectionSummaryLabels(sectionName, state),
791
+ audit_refs: ["audit-local"],
792
+ };
793
+ sectionFiles.push(sectionFile);
794
+ updatedSummaries.push({
795
+ schema_version: "flowdesk.debug_section_summary.v1",
796
+ export_id: exportId,
797
+ section: sectionName,
798
+ ref: sectionRef,
799
+ redaction_status: redactionStatus,
800
+ warning_count: warningCount,
801
+ excluded_categories: excludedCategories,
802
+ });
803
+ }
804
+ const byteCount = sectionFiles.reduce((acc, file) => acc + Buffer.byteLength(JSON.stringify(file), "utf8"), 0);
501
805
  const manifest = {
502
806
  schema_version: "flowdesk.debug_export_manifest.v1",
503
807
  export_id: exportId,
504
808
  manifest_ref: safeToken(response.export_manifest_ref, `manifest-${exportId}`),
505
- ...(typeof request.workflow_id === "string" ? { workflow_id: request.workflow_id } : {}),
506
- ...(typeof request.session_ref === "string" ? { session_ref: request.session_ref } : {}),
809
+ ...(workflowId !== undefined ? { workflow_id: workflowId } : {}),
810
+ ...(sessionRef !== undefined ? { session_ref: sessionRef } : {}),
507
811
  created_at: parts.nowIso,
508
- delete_after: typeof response.delete_after === "string" ? response.delete_after : parts.expiresAtIso,
509
- included_sections: includedSections,
812
+ delete_after: typeof response.delete_after === "string"
813
+ ? response.delete_after
814
+ : parts.expiresAtIso,
815
+ included_sections: updatedSummaries,
510
816
  redaction_version: "redaction-v1",
511
- source_refs: [safeToken(request.redacted_intake_ref, "debug-export-request")],
512
- file_count: 0,
513
- byte_count: 0,
817
+ source_refs: sourceRefs,
818
+ file_count: sectionFiles.length,
819
+ byte_count: byteCount,
514
820
  warnings: [],
515
- deletion_state: request.retention_hint === "delete_after_export" ? "deleted" : "retained_by_policy",
516
- ...(request.retention_hint === "delete_after_export" ? { deletion_proof_ref: `deletion-${exportId}` } : {}),
517
- audit_refs: ["audit-local"]
821
+ deletion_state: request.retention_hint === "delete_after_export"
822
+ ? "deleted"
823
+ : "retained_by_policy",
824
+ ...(request.retention_hint === "delete_after_export"
825
+ ? { deletion_proof_ref: `deletion-${exportId}` }
826
+ : {}),
827
+ audit_refs: ["audit-local"],
518
828
  };
519
- const intent = prepareDebugExportManifestWriteIntent("session-local", manifest).writeIntent;
520
- if (intent === undefined)
829
+ const manifestIntent = prepareDebugExportManifestWriteIntent("session-local", manifest).writeIntent;
830
+ if (manifestIntent === undefined)
521
831
  return false;
522
- return applyAdapterWriteIntents(state, [intent], parts);
832
+ const sectionIntents = [];
833
+ for (const file of sectionFiles) {
834
+ const prep = prepareDebugSectionFileWriteIntent("session-local", file);
835
+ if (prep.writeIntent === undefined)
836
+ return false;
837
+ sectionIntents.push(prep.writeIntent);
838
+ }
839
+ return applyAdapterWriteIntents(state, [manifestIntent, ...sectionIntents], parts);
523
840
  }
524
841
  function fallbackRegatePlanContext(state, request) {
525
842
  if (state.durableStateRootDir === undefined)
526
843
  return undefined;
527
844
  const workflowId = workflowIdFrom(request);
528
- const evidenceReload = reloadFlowDeskSessionEvidenceV1({ workflowId, rootDir: state.durableStateRootDir });
529
- const entry = evidenceReload.entries.find(e => e.evidenceClass === "fallback_regate_plan");
845
+ const evidenceReload = reloadFlowDeskSessionEvidenceV1({
846
+ workflowId,
847
+ rootDir: state.durableStateRootDir,
848
+ });
849
+ const entry = evidenceReload.entries.find((e) => e.evidenceClass === "fallback_regate_plan");
530
850
  return entry?.record;
531
851
  }
532
852
  function statusContext(state, request, parts) {
533
853
  const requestedWorkflowId = workflowIdFrom(request);
534
- if (state.durableStateRootDir !== undefined && (state.workflow === undefined || state.workflow.workflow_id !== requestedWorkflowId)) {
535
- const loaded = loadFlowDeskDurableWorkflowState(state.durableStateRootDir, { workflowId: requestedWorkflowId, sessionId: "session-local", now: parts.nowMs });
854
+ if (state.durableStateRootDir !== undefined &&
855
+ (state.workflow === undefined ||
856
+ state.workflow.workflow_id !== requestedWorkflowId)) {
857
+ const loaded = loadFlowDeskDurableWorkflowState(state.durableStateRootDir, {
858
+ workflowId: requestedWorkflowId,
859
+ sessionId: "session-local",
860
+ now: parts.nowMs,
861
+ });
536
862
  state.lastDurableStateReadErrors = loaded.ok ? [] : loaded.errors;
537
863
  if (loaded.ok && loaded.snapshot !== undefined) {
538
864
  state.active = loaded.snapshot.active;
@@ -544,27 +870,40 @@ function statusContext(state, request, parts) {
544
870
  else {
545
871
  state.lastDurableStateReadErrors = [];
546
872
  }
547
- if (state.lastDurableStateReadErrors.length === 0 && (state.workflow === undefined || state.active === undefined))
873
+ if (state.lastDurableStateReadErrors.length === 0 &&
874
+ (state.workflow === undefined || state.active === undefined))
548
875
  updateWorkflowState(state, request, parts, "ready_to_run");
549
876
  const fanoutDiagnostics = reviewerFanoutDiagnosticsContext(state, request, parts);
550
877
  return {
551
878
  active: state.active,
552
879
  workflow: state.workflow,
553
- ...(state.currentAttempt === undefined ? {} : { currentAttempt: state.currentAttempt }),
880
+ ...(state.currentAttempt === undefined
881
+ ? {}
882
+ : { currentAttempt: state.currentAttempt }),
554
883
  laneRecords: state.laneRecords,
555
884
  providerHealthSnapshot: providerHealth(parts),
556
- ...(fanoutDiagnostics?.selection.cacheRefreshPlan === undefined ? {} : { exactModelAvailabilityCacheRefreshPlan: fanoutDiagnostics.selection.cacheRefreshPlan }),
557
- ...(fanoutDiagnostics === undefined ? {} : { reviewerFanoutPlan: fanoutDiagnostics.fanoutPlan }),
885
+ ...(fanoutDiagnostics?.selection.cacheRefreshPlan === undefined
886
+ ? {}
887
+ : {
888
+ exactModelAvailabilityCacheRefreshPlan: fanoutDiagnostics.selection.cacheRefreshPlan,
889
+ }),
890
+ ...(fanoutDiagnostics === undefined
891
+ ? {}
892
+ : { reviewerFanoutPlan: fanoutDiagnostics.fanoutPlan }),
558
893
  auditRef: "audit-local",
559
894
  debugRef: "debug-local",
560
- now: parts.nowMs
895
+ now: parts.nowMs,
561
896
  };
562
897
  }
563
898
  function productionEnablementContext(state, request) {
564
- if (state.productionEnablement?.enabled !== true || state.durableStateRootDir === undefined)
899
+ if (state.productionEnablement?.enabled !== true ||
900
+ state.durableStateRootDir === undefined)
565
901
  return undefined;
566
902
  const workflowId = workflowIdFrom(request);
567
- const evidenceReload = reloadFlowDeskSessionEvidenceV1({ workflowId, rootDir: state.durableStateRootDir });
903
+ const evidenceReload = reloadFlowDeskSessionEvidenceV1({
904
+ workflowId,
905
+ rootDir: state.durableStateRootDir,
906
+ });
568
907
  return evaluateFlowDeskProductionEnablementV1({
569
908
  workflowId,
570
909
  evidenceReload,
@@ -578,26 +917,32 @@ function productionEnablementContext(state, request) {
578
917
  externalAuthProviderPolicyResult: state.productionEnablement.externalAuthProviderPolicyResult,
579
918
  laneConformanceRefs: state.productionEnablement.laneConformanceRefs,
580
919
  allowIncompleteConformance: state.productionEnablement.allowIncompleteConformance,
581
- approvalDecision: state.productionEnablement.approvalDecision
920
+ approvalDecision: state.productionEnablement.approvalDecision,
582
921
  });
583
922
  }
584
923
  function reviewerFanoutDiagnosticsContext(state, request, parts) {
585
- if (state.reviewerFanoutDiagnostics?.enabled !== true || state.durableStateRootDir === undefined)
924
+ if (state.reviewerFanoutDiagnostics?.enabled !== true ||
925
+ state.durableStateRootDir === undefined)
586
926
  return undefined;
587
927
  const workflowId = workflowIdFrom(request);
588
- const reloadedEvidence = reloadFlowDeskSessionEvidenceV1({ workflowId, rootDir: state.durableStateRootDir });
928
+ const reloadedEvidence = reloadFlowDeskSessionEvidenceV1({
929
+ workflowId,
930
+ rootDir: state.durableStateRootDir,
931
+ });
589
932
  const plan = planFlowDeskReviewerFanoutFromReloadedCacheEvidenceV1({
590
933
  ...state.reviewerFanoutDiagnostics,
591
934
  workflowId,
592
935
  reloadedEvidence,
593
- requestedAt: state.reviewerFanoutDiagnostics.requestedAt ?? parts.nowIso
936
+ requestedAt: state.reviewerFanoutDiagnostics.requestedAt ?? parts.nowIso,
594
937
  });
595
- if (state.reviewerFanoutDiagnostics.persistDerivedFanoutPlanEvidence === true && plan.state === "fanout_ready") {
938
+ if (state.reviewerFanoutDiagnostics.persistDerivedFanoutPlanEvidence === true &&
939
+ plan.state === "fanout_ready") {
596
940
  const materialized = materializeReviewerFanoutPlanEvidence({
597
941
  rootDir: state.durableStateRootDir,
598
942
  workflowId,
599
- evidenceId: state.reviewerFanoutDiagnostics.fanoutPlanEvidenceId ?? reviewerFanoutPlanEvidenceId(workflowId, plan.fanoutPlan),
600
- fanoutPlan: plan.fanoutPlan
943
+ evidenceId: state.reviewerFanoutDiagnostics.fanoutPlanEvidenceId ??
944
+ reviewerFanoutPlanEvidenceId(workflowId, plan.fanoutPlan),
945
+ fanoutPlan: plan.fanoutPlan,
601
946
  });
602
947
  if (!materialized.ok)
603
948
  state.lastDurableStateReadErrors = materialized.errors;
@@ -610,54 +955,80 @@ function reviewerFanoutPlanEvidenceId(workflowId, plan) {
610
955
  function materializeReviewerFanoutPlanEvidence(input) {
611
956
  if (input.fanoutPlan.state !== "fanout_ready" || !input.fanoutPlan.ok)
612
957
  return invalid("reviewer fanout plan evidence requires a ready fanout plan");
613
- const prepared = prepareFlowDeskSessionEvidenceWriteIntentV1({ workflowId: input.workflowId, evidenceId: input.evidenceId, record: input.fanoutPlan });
958
+ const prepared = prepareFlowDeskSessionEvidenceWriteIntentV1({
959
+ workflowId: input.workflowId,
960
+ evidenceId: input.evidenceId,
961
+ record: input.fanoutPlan,
962
+ });
614
963
  if (!prepared.ok || prepared.writeIntent === undefined)
615
964
  return prepared;
616
- return applyFlowDeskSessionEvidenceWriteIntentsV1(input.rootDir, [prepared.writeIntent]);
965
+ return applyFlowDeskSessionEvidenceWriteIntentsV1(input.rootDir, [
966
+ prepared.writeIntent,
967
+ ]);
617
968
  }
618
969
  function contextFor(toolName, request, state, parts, permissionProvider) {
619
970
  const record = isRecord(request) ? request : {};
620
971
  if (toolName === "flowdesk_plan")
621
972
  return { plan: makePlanContext(record, parts) };
622
973
  if (toolName === "flowdesk_run")
623
- return { run: makeRunContext(record, parts, permissionProvider) };
974
+ return {
975
+ run: makeRunContext(record, parts, permissionProvider, state.policyContext),
976
+ };
624
977
  if (toolName === "flowdesk_status")
625
978
  return { status: statusContext(state, record, parts) };
626
979
  const fallbackRegatePlan = fallbackRegatePlanContext(state, record);
627
980
  if (toolName === "flowdesk_retry") {
628
- const retry = { request: record, providerHealthSnapshot: providerHealth(parts), newAttemptId: id("attempt-retry", record), auditRef: "audit-local", debugRef: "debug-local" };
981
+ const retry = {
982
+ request: record,
983
+ providerHealthSnapshot: providerHealth(parts),
984
+ newAttemptId: id("attempt-retry", record),
985
+ auditRef: "audit-local",
986
+ debugRef: "debug-local",
987
+ };
629
988
  return { retry };
630
989
  }
631
990
  const fanoutDiagnostics = reviewerFanoutDiagnosticsContext(state, record, parts);
632
- return { diagnostic: {
991
+ return {
992
+ diagnostic: {
633
993
  nowIso: parts.nowIso,
634
994
  deleteAfterIso: parts.expiresAtIso,
635
995
  providerHealthSnapshotRef: "health-local",
636
996
  productionEnablement: productionEnablementContext(state, record),
637
- ...(fanoutDiagnostics?.selection.cacheRefreshPlan === undefined ? {} : { exactModelAvailabilityCacheRefreshPlan: fanoutDiagnostics.selection.cacheRefreshPlan }),
638
- ...(fanoutDiagnostics === undefined ? {} : { reviewerFanoutPlan: fanoutDiagnostics.fanoutPlan }),
639
- fallbackRegatePlan
640
- } };
997
+ ...(fanoutDiagnostics?.selection.cacheRefreshPlan === undefined
998
+ ? {}
999
+ : {
1000
+ exactModelAvailabilityCacheRefreshPlan: fanoutDiagnostics.selection.cacheRefreshPlan,
1001
+ }),
1002
+ ...(fanoutDiagnostics === undefined
1003
+ ? {}
1004
+ : { reviewerFanoutPlan: fanoutDiagnostics.fanoutPlan }),
1005
+ fallbackRegatePlan,
1006
+ },
1007
+ };
641
1008
  }
642
1009
  function summarize(state, stateWriteApplied) {
643
1010
  const pending = state.pendingConfirmation;
644
1011
  return {
645
1012
  workflowId: state.workflow?.workflow_id,
646
1013
  workflowState: state.workflow?.state,
647
- ...(pending === undefined ? { pendingConfirmationStatus: "missing" } : {
648
- pendingConfirmationStatus: pending.status,
649
- pendingConfirmationRef: pending.approvalRef,
650
- pendingConfirmationWorkflowId: pending.workflowId,
651
- pendingConfirmationExpiresAt: pending.expiresAtIso
652
- }),
1014
+ ...(pending === undefined
1015
+ ? { pendingConfirmationStatus: "missing" }
1016
+ : {
1017
+ pendingConfirmationStatus: pending.status,
1018
+ pendingConfirmationRef: pending.approvalRef,
1019
+ pendingConfirmationWorkflowId: pending.workflowId,
1020
+ pendingConfirmationExpiresAt: pending.expiresAtIso,
1021
+ }),
653
1022
  laneRecords: state.laneRecords.length,
654
1023
  inMemoryStateEntries: state.inMemoryState.size,
655
- durableStateMode: state.durableStateRootDir === undefined ? "memory_only" : "durable_flowdesk_root",
1024
+ durableStateMode: state.durableStateRootDir === undefined
1025
+ ? "memory_only"
1026
+ : "durable_flowdesk_root",
656
1027
  durableStateWriteApplied: state.lastDurableStateWriteApplied,
657
1028
  durableStateWrites: state.durableStateWrites,
658
1029
  stateWriteApplied,
659
1030
  permissionSource: "tool_boundary_injected",
660
- ...disabledAuthority
1031
+ ...disabledAuthority,
661
1032
  };
662
1033
  }
663
1034
  function blockedRunHandler(toolName, validation) {
@@ -668,14 +1039,25 @@ function blockedRunHandler(toolName, validation) {
668
1039
  requestSchemaValid: false,
669
1040
  responseSchemaValid: false,
670
1041
  coreEvaluationOk: false,
671
- ...disabledAuthority
1042
+ ...disabledAuthority,
672
1043
  };
673
1044
  }
674
1045
  function clockDate(clock) {
675
1046
  return typeof clock === "function" ? clock() : clock;
676
1047
  }
677
1048
  export function createFlowDeskLocalNonDispatchAdapterSession(now = new Date(), permissionProvider = createFlowDeskLocalNonDispatchPermissionProvider(), options = {}) {
678
- const state = { laneRecords: [], inMemoryState: new Map(), durableStateRootDir: options.durableStateRootDir, productionEnablement: options.productionEnablement, reviewerFanoutDiagnostics: options.reviewerFanoutDiagnostics, durableStateWrites: 0, lastDurableStateWriteApplied: false, lastDurableStateReadErrors: [] };
1049
+ const initialParts = nowParts(clockDate(now));
1050
+ const state = {
1051
+ laneRecords: [],
1052
+ inMemoryState: new Map(),
1053
+ durableStateRootDir: options.durableStateRootDir,
1054
+ productionEnablement: options.productionEnablement,
1055
+ reviewerFanoutDiagnostics: options.reviewerFanoutDiagnostics,
1056
+ policyContext: loadLocalPolicyContext(initialParts, options.projectConfig),
1057
+ durableStateWrites: 0,
1058
+ lastDurableStateWriteApplied: false,
1059
+ lastDurableStateReadErrors: [],
1060
+ };
679
1061
  return {
680
1062
  state,
681
1063
  evaluate(toolName, request) {
@@ -691,7 +1073,7 @@ export function createFlowDeskLocalNonDispatchAdapterSession(now = new Date(), p
691
1073
  toolName,
692
1074
  handler,
693
1075
  localState: summarize(state, false),
694
- ...disabledAuthority
1076
+ ...disabledAuthority,
695
1077
  };
696
1078
  }
697
1079
  consumePendingConfirmation(state, parts);
@@ -707,20 +1089,26 @@ export function createFlowDeskLocalNonDispatchAdapterSession(now = new Date(), p
707
1089
  stateWriteApplied = recordDebugExportManifestState(state, record, handler.response, parts);
708
1090
  if (handler.ok && toolName === "flowdesk_abort")
709
1091
  cancelPendingConfirmation(state, record, parts);
710
- const adapterValidation = state.lastDurableStateReadErrors.length > 0
711
- ? invalid(...state.lastDurableStateReadErrors)
712
- : handler.ok && (toolName === "flowdesk_plan" || toolName === "flowdesk_run" || toolName === "flowdesk_export_debug") && !stateWriteApplied
713
- ? invalid("local adapter state write failed")
714
- : valid();
1092
+ const adapterValidation = state.policyContext.errors.length > 0
1093
+ ? invalid(...state.policyContext.errors)
1094
+ : state.lastDurableStateReadErrors.length > 0
1095
+ ? invalid(...state.lastDurableStateReadErrors)
1096
+ : handler.ok &&
1097
+ (toolName === "flowdesk_plan" ||
1098
+ toolName === "flowdesk_run" ||
1099
+ toolName === "flowdesk_export_debug") &&
1100
+ !stateWriteApplied
1101
+ ? invalid("local adapter state write failed")
1102
+ : valid();
715
1103
  return {
716
1104
  ...adapterValidation,
717
1105
  adapterProfile: flowdeskLocalNonDispatchAdapterProfile,
718
1106
  toolName,
719
1107
  handler,
720
1108
  localState: summarize(state, stateWriteApplied),
721
- ...disabledAuthority
1109
+ ...disabledAuthority,
722
1110
  };
723
- }
1111
+ },
724
1112
  };
725
1113
  }
726
1114
  //# sourceMappingURL=local-adapter.js.map