clinkx 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +97 -13
  3. package/clinkx-workflows/dist/allowed-roots.js +2 -0
  4. package/clinkx-workflows/dist/allowed-roots.js.map +1 -1
  5. package/clinkx-workflows/dist/architecture-comparison/admission.d.ts +5 -0
  6. package/clinkx-workflows/dist/architecture-comparison/admission.js +50 -0
  7. package/clinkx-workflows/dist/architecture-comparison/admission.js.map +1 -0
  8. package/clinkx-workflows/dist/architecture-comparison/contract.d.ts +995 -0
  9. package/clinkx-workflows/dist/architecture-comparison/contract.js +149 -0
  10. package/clinkx-workflows/dist/architecture-comparison/contract.js.map +1 -0
  11. package/clinkx-workflows/dist/architecture-comparison/runtime.d.ts +29 -0
  12. package/clinkx-workflows/dist/architecture-comparison/runtime.js +387 -0
  13. package/clinkx-workflows/dist/architecture-comparison/runtime.js.map +1 -0
  14. package/clinkx-workflows/dist/child-env.d.ts +3 -1
  15. package/clinkx-workflows/dist/child-env.js +6 -2
  16. package/clinkx-workflows/dist/child-env.js.map +1 -1
  17. package/clinkx-workflows/dist/config.d.ts +28 -5
  18. package/clinkx-workflows/dist/config.js +137 -20
  19. package/clinkx-workflows/dist/config.js.map +1 -1
  20. package/clinkx-workflows/dist/contained-file.js +10 -1
  21. package/clinkx-workflows/dist/contained-file.js.map +1 -1
  22. package/clinkx-workflows/dist/council-execution-policy.d.ts +4 -2
  23. package/clinkx-workflows/dist/council-execution-policy.js +5 -1
  24. package/clinkx-workflows/dist/council-execution-policy.js.map +1 -1
  25. package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +72 -72
  26. package/clinkx-workflows/dist/dashboard/contracts.d.ts +1564 -122
  27. package/clinkx-workflows/dist/dashboard/contracts.js +4 -0
  28. package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -1
  29. package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +12 -12
  30. package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +8 -8
  31. package/clinkx-workflows/dist/dashboard/scanner.d.ts +3 -0
  32. package/clinkx-workflows/dist/dashboard/scanner.js +27 -0
  33. package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -1
  34. package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +48 -48
  35. package/clinkx-workflows/dist/dashboard/session-log-identity.js +2 -1
  36. package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -1
  37. package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +2 -0
  38. package/clinkx-workflows/dist/dashboard/session-log-redaction.js +4 -0
  39. package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -1
  40. package/clinkx-workflows/dist/dashboard/verified-repair-contract.d.ts +191 -0
  41. package/clinkx-workflows/dist/dashboard/verified-repair-contract.js +28 -0
  42. package/clinkx-workflows/dist/dashboard/verified-repair-contract.js.map +1 -0
  43. package/clinkx-workflows/dist/dashboard-ui/assets/index-BdpmbPps.js +115 -0
  44. package/clinkx-workflows/dist/dashboard-ui/assets/style-Im5i1iAs.css +1 -0
  45. package/clinkx-workflows/dist/dashboard-ui/index.html +2 -2
  46. package/clinkx-workflows/dist/engine.d.ts +6 -0
  47. package/clinkx-workflows/dist/engine.js +78 -7
  48. package/clinkx-workflows/dist/engine.js.map +1 -1
  49. package/clinkx-workflows/dist/index.d.ts +3 -0
  50. package/clinkx-workflows/dist/index.js +3 -2
  51. package/clinkx-workflows/dist/index.js.map +1 -1
  52. package/clinkx-workflows/dist/reducers/index.d.ts +9 -1
  53. package/clinkx-workflows/dist/reducers/index.js +22 -0
  54. package/clinkx-workflows/dist/reducers/index.js.map +1 -1
  55. package/clinkx-workflows/dist/root-path-validation.d.ts +12 -0
  56. package/clinkx-workflows/dist/root-path-validation.js +63 -0
  57. package/clinkx-workflows/dist/root-path-validation.js.map +1 -0
  58. package/clinkx-workflows/dist/rosters.d.ts +1 -0
  59. package/clinkx-workflows/dist/rosters.js +16 -6
  60. package/clinkx-workflows/dist/rosters.js.map +1 -1
  61. package/clinkx-workflows/dist/server.js +5 -2
  62. package/clinkx-workflows/dist/server.js.map +1 -1
  63. package/clinkx-workflows/dist/state-schema.d.ts +13 -0
  64. package/clinkx-workflows/dist/state.d.ts +7 -1
  65. package/clinkx-workflows/dist/state.js +90 -19
  66. package/clinkx-workflows/dist/state.js.map +1 -1
  67. package/clinkx-workflows/dist/validators/architecture-comparison.d.ts +2 -0
  68. package/clinkx-workflows/dist/validators/architecture-comparison.js +12 -0
  69. package/clinkx-workflows/dist/validators/architecture-comparison.js.map +1 -0
  70. package/clinkx-workflows/dist/validators/index.js +4 -0
  71. package/clinkx-workflows/dist/validators/index.js.map +1 -1
  72. package/clinkx-workflows/dist/validators/verified-repair.d.ts +2 -0
  73. package/clinkx-workflows/dist/validators/verified-repair.js +12 -0
  74. package/clinkx-workflows/dist/validators/verified-repair.js.map +1 -0
  75. package/clinkx-workflows/dist/verification-proof.d.ts +254 -0
  76. package/clinkx-workflows/dist/verification-proof.js +195 -0
  77. package/clinkx-workflows/dist/verification-proof.js.map +1 -0
  78. package/clinkx-workflows/dist/verified-repair/admission.d.ts +72 -0
  79. package/clinkx-workflows/dist/verified-repair/admission.js +63 -0
  80. package/clinkx-workflows/dist/verified-repair/admission.js.map +1 -0
  81. package/clinkx-workflows/dist/verified-repair/contract.d.ts +1910 -0
  82. package/clinkx-workflows/dist/verified-repair/contract.js +127 -0
  83. package/clinkx-workflows/dist/verified-repair/contract.js.map +1 -0
  84. package/clinkx-workflows/dist/verified-repair/host.d.ts +32 -0
  85. package/clinkx-workflows/dist/verified-repair/host.js +2 -0
  86. package/clinkx-workflows/dist/verified-repair/host.js.map +1 -0
  87. package/clinkx-workflows/dist/verified-repair/prompts.d.ts +12 -0
  88. package/clinkx-workflows/dist/verified-repair/prompts.js +58 -0
  89. package/clinkx-workflows/dist/verified-repair/prompts.js.map +1 -0
  90. package/clinkx-workflows/dist/verified-repair/result.d.ts +16 -0
  91. package/clinkx-workflows/dist/verified-repair/result.js +102 -0
  92. package/clinkx-workflows/dist/verified-repair/result.js.map +1 -0
  93. package/clinkx-workflows/dist/verified-repair/runtime.d.ts +28 -0
  94. package/clinkx-workflows/dist/verified-repair/runtime.js +186 -0
  95. package/clinkx-workflows/dist/verified-repair/runtime.js.map +1 -0
  96. package/clinkx-workflows/dist/workflow-completion.d.ts +83 -0
  97. package/clinkx-workflows/dist/workflow-completion.js +250 -0
  98. package/clinkx-workflows/dist/workflow-completion.js.map +1 -0
  99. package/clinkx-workflows/dist/workflow-tools.d.ts +7 -3
  100. package/clinkx-workflows/dist/workflow-tools.js +487 -46
  101. package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
  102. package/clinkx-workflows/dist/workflow-verification-contract.d.ts +539 -0
  103. package/clinkx-workflows/dist/workflow-verification-contract.js +52 -0
  104. package/clinkx-workflows/dist/workflow-verification-contract.js.map +1 -0
  105. package/clinkx-workflows/dist/workflow-verification.d.ts +15 -0
  106. package/clinkx-workflows/dist/workflow-verification.js +218 -0
  107. package/clinkx-workflows/dist/workflow-verification.js.map +1 -0
  108. package/clinkx-workflows/templates/architecture-comparison/candidate.txt +29 -0
  109. package/clinkx-workflows/templates/architecture-comparison/judge.txt +31 -0
  110. package/clinkx-workflows/templates/verified-repair/investigate.txt +20 -0
  111. package/clinkx-workflows/templates/verified-repair/repair.txt +11 -0
  112. package/clinkx-workflows/templates/verified-repair/review.txt +18 -0
  113. package/clinkx-workflows/workflows/architecture-comparison.yaml +133 -0
  114. package/clinkx-workflows/workflows/verified-repair.yaml +104 -0
  115. package/dist/allowed-roots.d.ts +1 -1
  116. package/dist/allowed-roots.js +128 -15
  117. package/dist/allowed-roots.js.map +1 -1
  118. package/dist/codex/app-server-client.d.ts +1 -0
  119. package/dist/codex/app-server-client.js +3 -2
  120. package/dist/codex/app-server-client.js.map +1 -1
  121. package/dist/context-builder/config.d.ts +1 -0
  122. package/dist/context-builder/config.js +21 -9
  123. package/dist/context-builder/config.js.map +1 -1
  124. package/dist/context-builder/diagnostics.d.ts +1 -0
  125. package/dist/context-builder/diagnostics.js +1 -1
  126. package/dist/context-builder/diagnostics.js.map +1 -1
  127. package/dist/context-builder/discovery.d.ts +18 -1
  128. package/dist/context-builder/discovery.js +69 -1
  129. package/dist/context-builder/discovery.js.map +1 -1
  130. package/dist/context-builder/handler.js +8 -1
  131. package/dist/context-builder/handler.js.map +1 -1
  132. package/dist/context-builder/lifecycle-contract.d.ts +24 -24
  133. package/dist/context-builder/mutation-lock.d.ts +4 -0
  134. package/dist/context-builder/mutation-lock.js +7 -1
  135. package/dist/context-builder/mutation-lock.js.map +1 -1
  136. package/dist/context-builder/refine-handler.js +16 -1
  137. package/dist/context-builder/refine-handler.js.map +1 -1
  138. package/dist/context-builder/secrets.d.ts +11 -0
  139. package/dist/context-builder/secrets.js +75 -0
  140. package/dist/context-builder/secrets.js.map +1 -1
  141. package/dist/context-builder/storage.d.ts +10 -2
  142. package/dist/context-builder/storage.js +213 -16
  143. package/dist/context-builder/storage.js.map +1 -1
  144. package/dist/context-builder/versions.d.ts +5 -2
  145. package/dist/context-builder/versions.js +5 -2
  146. package/dist/context-builder/versions.js.map +1 -1
  147. package/dist/dashboard-cli.d.ts +6 -0
  148. package/dist/dashboard-cli.js +18 -32
  149. package/dist/dashboard-cli.js.map +1 -1
  150. package/dist/dashboard-gateway/main.js +8 -3
  151. package/dist/dashboard-gateway/main.js.map +1 -1
  152. package/dist/index.d.ts +2 -0
  153. package/dist/index.js +8 -2
  154. package/dist/index.js.map +1 -1
  155. package/dist/project-verification/contract.d.ts +537 -0
  156. package/dist/project-verification/contract.js +40 -0
  157. package/dist/project-verification/contract.js.map +1 -0
  158. package/dist/project-verification/files.d.ts +23 -0
  159. package/dist/project-verification/files.js +80 -0
  160. package/dist/project-verification/files.js.map +1 -0
  161. package/dist/project-verification/process.d.ts +7 -0
  162. package/dist/project-verification/process.js +115 -0
  163. package/dist/project-verification/process.js.map +1 -0
  164. package/dist/project-verification/runner.d.ts +35 -0
  165. package/dist/project-verification/runner.js +185 -0
  166. package/dist/project-verification/runner.js.map +1 -0
  167. package/dist/project-verification/service.d.ts +37 -0
  168. package/dist/project-verification/service.js +151 -0
  169. package/dist/project-verification/service.js.map +1 -0
  170. package/dist/provider-doctor.d.ts +22 -0
  171. package/dist/provider-doctor.js +22 -0
  172. package/dist/provider-doctor.js.map +1 -0
  173. package/dist/registry.d.ts +1 -1
  174. package/dist/registry.js +5 -1
  175. package/dist/registry.js.map +1 -1
  176. package/dist/server.js +41 -6
  177. package/dist/server.js.map +1 -1
  178. package/dist/verification-tools.d.ts +90 -0
  179. package/dist/verification-tools.js +120 -0
  180. package/dist/verification-tools.js.map +1 -0
  181. package/dist/verified-repair/files.d.ts +22 -0
  182. package/dist/verified-repair/files.js +114 -0
  183. package/dist/verified-repair/files.js.map +1 -0
  184. package/dist/verified-repair/host.d.ts +14 -0
  185. package/dist/verified-repair/host.js +612 -0
  186. package/dist/verified-repair/host.js.map +1 -0
  187. package/docs/configuration.md +224 -12
  188. package/docs/engineering-recipes.md +415 -0
  189. package/docs/security.md +3 -0
  190. package/docs/verification/README.md +59 -0
  191. package/docs/verification/dashboard-real-disk.md +436 -0
  192. package/docs/verification/project-checks.example.json +14 -0
  193. package/docs/verification/project-checks.md +444 -0
  194. package/docs/verification/workflow-handoff.md +113 -0
  195. package/package.json +10 -2
  196. package/scripts/provider-doctor-core.mjs +380 -0
  197. package/scripts/provider-doctor-probe.mjs +34 -0
  198. package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +0 -106
  199. package/clinkx-workflows/dist/dashboard-ui/assets/style-DOMHk-j6.css +0 -1
@@ -5,11 +5,12 @@
5
5
  * - Standalone STDIO server (ClinkSession child process backend)
6
6
  * - In-process integration (LocalClinkBackend via root ClinkX)
7
7
  *
8
- * The `registerWorkflowTools()` function registers five tools on a Server:
8
+ * The `registerWorkflowTools()` function registers six tools on a Server:
9
9
  * - run_workflow: start a new workflow execution
10
10
  * - resume_workflow: resume a failed/cancelled run
11
11
  * - list_workflows: list available workflow definitions
12
12
  * - workflow_run_status: read workflow run progress/result
13
+ * - workflow_run_wait: observe local execution settlement
13
14
  * - cancel_workflow: cancel a live or orphaned workflow run
14
15
  *
15
16
  * The caller provides a `createBackend` factory that determines how clink
@@ -25,7 +26,7 @@ import { readRunStateForStatus, reapRunCancelled, WorkflowStateStore } from "./s
25
26
  import { isTerminalContextBuilderLifecycleStatus, parseContextBuilderLifecycleStateV1, } from "./context-builder-lifecycle-contract.js";
26
27
  import { validateRunId } from "./run-id.js";
27
28
  import { validateDynamicPaths } from "./path-validation.js";
28
- import { getMaxPromptBytes, getRunStateDir, getTemplatesPath, getWorkflowsContextDefault, getWorkflowsContextDefaultCurator, getWorkflowsMaxConcurrentAsync, resolveWorkspaceRoot, } from "./config.js";
29
+ import { getMaxPromptBytes, getRunStateDir, getRunsDir, getTemplatesPath, getWorkflowsContextDefault, getWorkflowsContextDefaultCurator, getWorkflowsMaxConcurrentAsync, resolveWorkspaceRoot, } from "./config.js";
29
30
  import { WorkflowCancellationError, WorkflowValidationError, } from "./errors.js";
30
31
  import { shutdownCoordinator } from "./shutdown.js";
31
32
  import { logger } from "./logger.js";
@@ -51,9 +52,19 @@ import { restorePlanReviewRun, } from "./plan-review/resume.js";
51
52
  import { PlanReviewStageError } from "./plan-review/stage-error.js";
52
53
  import { CouncilPlanReviewErrorSchema, createPlanReviewPublicationError, inspectPlanReviewPublication, } from "./plan-review/publication.js";
53
54
  import { validatePlanReviewCallBindings, validatePlanReviewPreparedInputPolicy, } from "./plan-review/runtime-policy.js";
54
- import { ContainedFilePolicyError, readContainedRelativeFile, } from "./contained-file.js";
55
+ import { ContainedFileChangedError, ContainedFilePolicyError, readContainedRelativeFile, } from "./contained-file.js";
55
56
  import { createDashboardRunReader } from "./dashboard/read-model.js";
56
57
  import { ROSTER_NAME_PATTERN, ResolvedRosterSchema, } from "./roster-contract.js";
58
+ import { materializeArchitectureRoutes } from "./architecture-comparison/admission.js";
59
+ import { ARCHITECTURE_WORKFLOW } from "./architecture-comparison/contract.js";
60
+ import { inspectArchitectureComparisonRecord } from "./architecture-comparison/runtime.js";
61
+ import { VERIFIED_REPAIR_WORKFLOW, MAX_REPAIR_RECORD_BYTES, RepairRecordSchema, assertRepairBounded } from "./verified-repair/contract.js";
62
+ import { materializeVerifiedRepairRoutes, repairRoutesFromVariables } from "./verified-repair/admission.js";
63
+ import { VerifiedRepairRuntime } from "./verified-repair/runtime.js";
64
+ import { inspectVerifiedRepairRecord } from "./verified-repair/result.js";
65
+ import { readWorkflowVerification } from "./workflow-verification.js";
66
+ import { readSessionLogGeneration } from "./dashboard/session-log-identity.js";
67
+ import { WorkflowCompletionRegistry, inspectPublication, persistedIdentityFailure, WORKFLOW_WAIT_BUDGET_MS, } from "./workflow-completion.js";
57
68
  async function inspectLegacyWorkflowSessionLogGeneration(runDir, runId) {
58
69
  try {
59
70
  const result = await createDashboardRunReader().inspectGeneration?.(dirname(runDir), runId);
@@ -96,6 +107,7 @@ function createPreflightOnlyBackend(advertisedCliNames) {
96
107
  // Admission control
97
108
  // ---------------------------------------------------------------------------
98
109
  const activeRuns = new Map();
110
+ const completions = new WorkflowCompletionRegistry();
99
111
  /**
100
112
  * Number of workflow tool handlers currently executing in the standalone
101
113
  * registration path. This includes non-run tools such as status/list/cancel;
@@ -165,6 +177,9 @@ function releaseMutex(runId) {
165
177
  /** Own setup before its first await, and keep that ownership through cleanup. */
166
178
  function acquireWorkflowRun(runId, options, requestSignal) {
167
179
  acquireMutex(runId, options);
180
+ // Bound lazily: a request rejected before it owns state never becomes an
181
+ // execution, so it cannot displace a settled one or consume a retained slot.
182
+ const owner = completions.owner(runId, getRunStateDir(runId));
168
183
  const controller = new AbortController();
169
184
  const signal = requestSignal === undefined
170
185
  ? controller.signal
@@ -193,6 +208,15 @@ function acquireWorkflowRun(runId, options, requestSignal) {
193
208
  if (requestSignal === undefined)
194
209
  cancelRegistry.set(runId, controller);
195
210
  return {
211
+ get executionId() { return owner.entry?.executionId; },
212
+ bindStore: async (store) => {
213
+ // Register first, so a pin on the previous execution already fails as
214
+ // identity_changed while this execution's id is being persisted.
215
+ const entry = owner.bind(store);
216
+ await store.bindExecution(entry.executionId);
217
+ completions.markStamped(entry);
218
+ return entry.executionId;
219
+ },
196
220
  signal,
197
221
  throwIfCancelled: () => {
198
222
  if (signal.aborted)
@@ -207,6 +231,14 @@ function acquireWorkflowRun(runId, options, requestSignal) {
207
231
  catch (err) {
208
232
  logger.warn({ err }, "backend.terminate() failed during cleanup");
209
233
  }
234
+ try {
235
+ await store?.flush?.();
236
+ if (owner.entry !== undefined)
237
+ await completions.recordPublication(owner.entry);
238
+ }
239
+ catch (err) {
240
+ logger.warn({ err }, "workflow publication identity unavailable during cleanup");
241
+ }
210
242
  try {
211
243
  await store?.close();
212
244
  }
@@ -218,6 +250,8 @@ function acquireWorkflowRun(runId, options, requestSignal) {
218
250
  cancelRegistry.delete(runId);
219
251
  releaseMutex(runId);
220
252
  shutdownCoordinator.unregister(handle);
253
+ if (owner.entry !== undefined)
254
+ completions.settle(owner.entry);
221
255
  complete();
222
256
  }
223
257
  },
@@ -1116,18 +1150,22 @@ function appendDuplicateReplayBlock(content, store) {
1116
1150
  });
1117
1151
  }
1118
1152
  /**
1119
- * Trailing additive content block for the admission-time requested-value
1120
- * roster. Omitted when the run did not persist one (non-council, plan-review,
1121
- * or pre-roster state). Defensive optional access: partial store mocks may
1122
- * omit runState.
1153
+ * Trailing additive identity block, always last: the admission-time
1154
+ * requested-value roster (omitted for non-council, plan-review, or pre-roster
1155
+ * state) and the execution_id bound to this request (omitted when the request
1156
+ * was rejected before it owned state). No block when neither is present.
1157
+ * Defensive optional access: partial store mocks may omit runState.
1123
1158
  */
1124
- function appendResolvedRosterBlock(content, store) {
1159
+ function appendIdentityBlock(content, store, executionId) {
1125
1160
  const resolvedRoster = store?.runState?.resolvedRoster;
1126
- if (resolvedRoster === undefined)
1161
+ if (resolvedRoster === undefined && executionId === undefined)
1127
1162
  return;
1128
1163
  content.push({
1129
1164
  type: "text",
1130
- text: JSON.stringify({ resolved_roster: resolvedRoster }),
1165
+ text: JSON.stringify({
1166
+ ...(resolvedRoster !== undefined ? { resolved_roster: resolvedRoster } : {}),
1167
+ ...(executionId !== undefined ? { execution_id: executionId } : {}),
1168
+ }),
1131
1169
  });
1132
1170
  }
1133
1171
  // ---------------------------------------------------------------------------
@@ -1339,7 +1377,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1339
1377
  throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
1340
1378
  }
1341
1379
  const validatorRegistry = createValidatorRegistry();
1342
- const loaded = loadWorkflow(workflow, {
1380
+ let loaded = loadWorkflow(workflow, {
1343
1381
  templatesRoot: options.templatesRoot ?? getTemplatesPath(),
1344
1382
  workflowsPath: options.workflowsPath,
1345
1383
  validatorRegistry,
@@ -1400,20 +1438,30 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1400
1438
  throw new McpError(ErrorCode.InvalidParams, `chairman_effort parameter is not supported for workflow "${workflow}"`);
1401
1439
  }
1402
1440
  resolvedVariables = validateVariables(suppliedVariables, loaded.definition.variables);
1441
+ try {
1442
+ loaded = { ...loaded, normalized: materializeArchitectureRoutes(loaded.normalized, resolvedVariables, options.planReviewAvailableCliNames ?? options.availableCliNames, options.validateInvocationOverrides) };
1443
+ loaded = { ...loaded, normalized: materializeVerifiedRepairRoutes(loaded.normalized, resolvedVariables, options.planReviewAvailableCliNames ?? options.availableCliNames, options.validateInvocationOverrides) };
1444
+ if (workflow === VERIFIED_REPAIR_WORKFLOW && options.verifiedRepairHost === undefined) {
1445
+ throw new Error("verified-repair requires the root host capability");
1446
+ }
1447
+ }
1448
+ catch (error) {
1449
+ throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
1450
+ }
1403
1451
  // When the runner is absent (preflight disabled / hard kill), the boundary
1404
1452
  // validator is inert. Skip it WITHOUT evaluating the context-default env
1405
1453
  // parsers — getWorkflowsContextDefaultCurator() throws on empty env, and a
1406
1454
  // runner-less run_workflow must never trip context env parsing.
1407
1455
  let contextPolicy;
1408
- if (options.contextBuilderRunner != null) {
1456
+ if (workflow !== VERIFIED_REPAIR_WORKFLOW && options.contextBuilderRunner != null) {
1409
1457
  contextPolicy = validateContextBuilderAnchorSufficiency(resolvedVariables, options.contextBuilderRunner, options.contextDefault ?? getWorkflowsContextDefault(), options.defaultCurator ?? getWorkflowsContextDefaultCurator());
1410
1458
  }
1411
1459
  validateContextSeedSourceAtAdmission(resolvedVariables, contextPolicy);
1412
1460
  // Pre-mutex, pre-state, pre-preflight: an out-of-roots caller path costs
1413
1461
  // zero delegate round-trips and leaves no run directory behind.
1414
1462
  assertCallerDynamicPathsAllowed(loaded.normalized, resolvedVariables, workflow, runId);
1415
- initialContextPolicy = contextPolicy ?? resolveWorkflowContextMode(undefined, options.contextDefault ?? "off", false, undefined, options.defaultCurator);
1416
- if (options.contextBuilderRunner != null) {
1463
+ initialContextPolicy = contextPolicy ?? resolveWorkflowContextMode(undefined, workflow === VERIFIED_REPAIR_WORKFLOW ? "off" : options.contextDefault ?? "off", false, undefined, options.defaultCurator);
1464
+ if (workflow !== VERIFIED_REPAIR_WORKFLOW && options.contextBuilderRunner != null) {
1417
1465
  try {
1418
1466
  options.contextBuilderRunner.assertTaskSecretsAllowed(resolvedVariables);
1419
1467
  }
@@ -1475,6 +1523,32 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1475
1523
  }
1476
1524
  const isAsync = args["async"] === true;
1477
1525
  const ownership = acquireWorkflowRun(runId, options, isAsync ? undefined : signal);
1526
+ let verifiedRepair;
1527
+ if (workflow === VERIFIED_REPAIR_WORKFLOW) {
1528
+ try {
1529
+ ownership.throwIfCancelled();
1530
+ const runDir = getRunStateDir(runId);
1531
+ if (existsSync(runDir))
1532
+ throw new McpError(ErrorCode.InvalidParams, `run_id ${runId} already has persisted state at ${runDir}`);
1533
+ const prepared = await options.verifiedRepairHost.prepare({
1534
+ runId, runDir, contractPath: resolvedVariables["repair_contract_path"],
1535
+ routes: repairRoutesFromVariables(resolvedVariables), signal: ownership.signal,
1536
+ });
1537
+ verifiedRepair = new VerifiedRepairRuntime(prepared, runId, runDir);
1538
+ ownership.throwIfCancelled();
1539
+ }
1540
+ catch (error) {
1541
+ try {
1542
+ await verifiedRepair?.cleanupUnadmitted(error);
1543
+ }
1544
+ finally {
1545
+ await ownership.finish(undefined);
1546
+ }
1547
+ if (ownership.signal.aborted)
1548
+ throw new WorkflowCancellationError();
1549
+ throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
1550
+ }
1551
+ }
1478
1552
  if (isAsync) {
1479
1553
  // Everything between acquireMutex and the detached spawn must release the
1480
1554
  // mutex on ANY throw, or the run slot leaks permanently (bricks the server
@@ -1483,6 +1557,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1483
1557
  let effectiveDefinition;
1484
1558
  let initializedPlanReview;
1485
1559
  let preparedSnapshot;
1560
+ let executionId;
1486
1561
  try {
1487
1562
  ownership.throwIfCancelled();
1488
1563
  const runDir = getRunStateDir(runId);
@@ -1515,6 +1590,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1515
1590
  catch (err) {
1516
1591
  throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
1517
1592
  }
1593
+ executionId = await ownership.bindStore(store);
1518
1594
  ownership.throwIfCancelled();
1519
1595
  // A-10: the async acknowledgement is a durability boundary. A caller
1520
1596
  // cannot observe `running` until every plan-review snapshot and the
@@ -1530,7 +1606,9 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1530
1606
  await markWorkflowCancelled(store);
1531
1607
  throw new WorkflowCancellationError();
1532
1608
  }
1533
- if (store !== undefined && planReviewPlan !== undefined) {
1609
+ // Once bound (plan-review init, or a failed id stamp), the run owns a
1610
+ // persisted record: terminalize it and disclose the bound identity.
1611
+ if (store !== undefined && ownership.executionId !== undefined) {
1534
1612
  try {
1535
1613
  await markWorkflowFailed(store, runId, err);
1536
1614
  }
@@ -1557,19 +1635,31 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1557
1635
  summary: failureSummary,
1558
1636
  debugBlock: debug ? buildErrorDebugBlock(runId, store) : undefined,
1559
1637
  });
1638
+ // Bound even if persisting the id then failed: the entry exists.
1639
+ appendIdentityBlock(content, store, ownership.executionId);
1560
1640
  return { content, isError: true };
1561
1641
  }
1562
1642
  throw err;
1563
1643
  }
1564
1644
  finally {
1565
- await ownership.finish(store);
1645
+ try {
1646
+ if (verifiedRepair !== undefined) {
1647
+ if (store === undefined)
1648
+ await verifiedRepair.cleanupUnadmitted(err);
1649
+ else
1650
+ await verifiedRepair.finalize(ownership.signal.aborted ? "cancelled" : "failed", err);
1651
+ }
1652
+ }
1653
+ finally {
1654
+ await ownership.finish(store);
1655
+ }
1566
1656
  }
1567
1657
  }
1568
- if (store === undefined || effectiveDefinition === undefined) {
1658
+ if (store === undefined || effectiveDefinition === undefined || executionId === undefined) {
1569
1659
  await ownership.finish(store);
1570
1660
  throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}`);
1571
1661
  }
1572
- void runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, true, ownership, undefined, store, effectiveDefinition, undefined, initializedPlanReview, initialContextPolicy, admittedRosterName).catch((err) => {
1662
+ void runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, true, ownership, undefined, store, effectiveDefinition, undefined, initializedPlanReview, initialContextPolicy, admittedRosterName, verifiedRepair).catch((err) => {
1573
1663
  if (err instanceof WorkflowCancellationError) {
1574
1664
  logger.info({ runId }, "workflow run cancelled");
1575
1665
  return;
@@ -1586,6 +1676,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1586
1676
  type: "text",
1587
1677
  text: JSON.stringify({
1588
1678
  run_id: runId,
1679
+ execution_id: executionId,
1589
1680
  status: "running",
1590
1681
  ...(duplicateOfFailedRun !== undefined
1591
1682
  ? { duplicate_of_failed_run: duplicateOfFailedRun }
@@ -1597,7 +1688,7 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
1597
1688
  }],
1598
1689
  };
1599
1690
  }
1600
- return await runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, false, ownership, progressToken, undefined, undefined, planReviewPlan, undefined, initialContextPolicy, admittedRosterName);
1691
+ return await runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, false, ownership, progressToken, undefined, undefined, planReviewPlan, undefined, initialContextPolicy, admittedRosterName, verifiedRepair);
1601
1692
  }
1602
1693
  async function preparePlanReviewSnapshot(plan) {
1603
1694
  try {
@@ -1789,7 +1880,7 @@ preparedSnapshot) {
1789
1880
  ? expandCouncilComposition(definition, parsedMembers, workflow, parsedChairman, parsedChairmanModel, parsedChairmanEffort)
1790
1881
  : definition;
1791
1882
  }
1792
- async function runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, isAsync, ownership, progressToken, preCreatedStore, preExpandedDefinition, planReviewPlan, preInitializedPlanReview, initialContextPolicy, rosterName) {
1883
+ async function runWorkflowDetached(server, options, workflow, runId, resolvedVariables, debug, receipt, validatorRegistry, loaded, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, isAsync, ownership, progressToken, preCreatedStore, preExpandedDefinition, planReviewPlan, preInitializedPlanReview, initialContextPolicy, rosterName, verifiedRepair) {
1793
1884
  let store = preCreatedStore ?? null;
1794
1885
  let backend = null;
1795
1886
  let reporter;
@@ -1823,6 +1914,7 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1823
1914
  ...(rosterName !== undefined ? { rosterName } : {}),
1824
1915
  });
1825
1916
  }
1917
+ await ownership.bindStore(store);
1826
1918
  ownership.throwIfCancelled();
1827
1919
  releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
1828
1920
  // A-5/A-6: snapshot + contract + typed subtree land here — after state
@@ -1849,6 +1941,24 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1849
1941
  const rpt = reporter; // local const for closure narrowing
1850
1942
  const hooks = {
1851
1943
  ...storeRef.hooks,
1944
+ ...(verifiedRepair === undefined ? {} : {
1945
+ onRunStateChange: async (event) => {
1946
+ if (event.state === "succeeded" || event.state === "failed" || event.state === "cancelled") {
1947
+ try {
1948
+ await backend?.terminate();
1949
+ await verifiedRepair.finalize(event.state === "succeeded" ? "completed" : event.state, event.error, signal);
1950
+ if (event.state === "succeeded")
1951
+ ownership.throwIfCancelled();
1952
+ }
1953
+ catch (error) {
1954
+ if (event.state === "succeeded")
1955
+ throw error;
1956
+ logger.error({ err: error, runId }, "repair terminal publication failed; preserving original execution failure");
1957
+ }
1958
+ }
1959
+ await storeRef.hooks.onRunStateChange?.(event);
1960
+ },
1961
+ }),
1852
1962
  ...(rpt != null
1853
1963
  ? {
1854
1964
  onProgress: async (event) => {
@@ -1872,11 +1982,12 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1872
1982
  runDir: storeRef.runDir,
1873
1983
  signal,
1874
1984
  hooks,
1875
- contextBuilderRunner: options.contextBuilderRunner,
1985
+ contextBuilderRunner: verifiedRepair === undefined ? options.contextBuilderRunner : undefined,
1876
1986
  contextDefault: options.contextDefault,
1877
1987
  defaultCurator: options.defaultCurator,
1878
1988
  commitPreflightContext: storeRef.commitPreflightContext?.bind(storeRef),
1879
1989
  initialContextPolicy,
1990
+ verifiedRepair,
1880
1991
  ...(options.deriveInvocationAudit === undefined
1881
1992
  ? {}
1882
1993
  : { deriveInvocationAudit: options.deriveInvocationAudit }),
@@ -1893,6 +2004,7 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1893
2004
  : {}),
1894
2005
  });
1895
2006
  const result = await engine.execute();
2007
+ const terminalRepair = await verifiedRepair?.finalize("completed", undefined, signal);
1896
2008
  // Build summary (fail-open)
1897
2009
  let summary;
1898
2010
  if (receipt) {
@@ -1905,17 +2017,29 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1905
2017
  }
1906
2018
  }
1907
2019
  const content = finalizeWorkflowPresentation({
1908
- outputText: result.outputText,
2020
+ outputText: terminalRepair ?? result.outputText,
1909
2021
  receipt,
1910
2022
  debug,
1911
2023
  summary,
1912
2024
  debugBlock: debug ? buildDebugBlock(runId, result, store) : undefined,
1913
2025
  });
1914
2026
  appendDuplicateReplayBlock(content, store);
1915
- appendResolvedRosterBlock(content, store);
2027
+ appendIdentityBlock(content, store, ownership.executionId);
1916
2028
  return { content };
1917
2029
  }
1918
2030
  catch (error) {
2031
+ if (verifiedRepair !== undefined) {
2032
+ try {
2033
+ await backend?.terminate();
2034
+ if (store === null)
2035
+ await verifiedRepair.cleanupUnadmitted(error);
2036
+ else
2037
+ await verifiedRepair.finalize(signal.aborted || error instanceof WorkflowCancellationError ? "cancelled" : "failed", error);
2038
+ }
2039
+ catch (publicationError) {
2040
+ logger.error({ err: publicationError, runId }, "repair error publication failed; preserving original execution failure");
2041
+ }
2042
+ }
1919
2043
  if (signal.aborted || error instanceof WorkflowCancellationError) {
1920
2044
  await markWorkflowCancelled(store);
1921
2045
  throw new WorkflowCancellationError();
@@ -1953,7 +2077,7 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1953
2077
  debugBlock: debug && store != null ? buildErrorDebugBlock(runId, store) : undefined,
1954
2078
  });
1955
2079
  appendDuplicateReplayBlock(content, store);
1956
- appendResolvedRosterBlock(content, store);
2080
+ appendIdentityBlock(content, store, ownership.executionId);
1957
2081
  return { content, isError: true };
1958
2082
  }
1959
2083
  finally {
@@ -1967,18 +2091,9 @@ async function runWorkflowDetached(server, options, workflow, runId, resolvedVar
1967
2091
  }
1968
2092
  }
1969
2093
  async function executeResumeWorkflow(server, options, args, signal, progressToken) {
1970
- const runId = args["run_id"];
2094
+ const runId = runIdArg(args);
1971
2095
  const debug = args["debug"] === true;
1972
2096
  const receipt = args["receipt"] === true;
1973
- if (typeof runId !== "string" || runId === "") {
1974
- throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid "run_id" parameter');
1975
- }
1976
- try {
1977
- validateRunId(runId);
1978
- }
1979
- catch {
1980
- throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
1981
- }
1982
2097
  // Plan-review terminal state is authoritative even while the detached run's
1983
2098
  // finally still owns the in-process mutex. Read it before duplicate-run
1984
2099
  // arbitration so terminal-before-cleanup never leaks as "already running".
@@ -1992,6 +2107,9 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
1992
2107
  persistedBeforeMutex = null;
1993
2108
  }
1994
2109
  const persistedState = persistedBeforeMutex?.state;
2110
+ if (persistedState?.workflowName === VERIFIED_REPAIR_WORKFLOW) {
2111
+ throw new McpError(ErrorCode.InvalidParams, "verified-repair mutation cannot be resumed; inspect the retained result and submit a fresh explicit contract");
2112
+ }
1995
2113
  if (persistedState != null &&
1996
2114
  isPlanReviewRunState(persistedState) &&
1997
2115
  (persistedState.status === "failed" ||
@@ -2022,9 +2140,13 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
2022
2140
  debugKeepArtifacts: debug,
2023
2141
  };
2024
2142
  store = await WorkflowStateStore.resume(resumeOptions);
2143
+ if (store.runState.workflowName === VERIFIED_REPAIR_WORKFLOW) {
2144
+ throw new McpError(ErrorCode.InvalidParams, "verified-repair mutation cannot be resumed; inspect the retained result and submit a fresh explicit contract");
2145
+ }
2025
2146
  // Open this attempt before classifying an abort during reconstruction;
2026
2147
  // otherwise a prior terminal record would suppress markCancelled.
2027
2148
  await store.beginResumeAttempt();
2149
+ await ownership.bindStore(store);
2028
2150
  ownership.throwIfCancelled();
2029
2151
  releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
2030
2152
  const validatorRegistry = createValidatorRegistry();
@@ -2201,16 +2323,27 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
2201
2323
  summary,
2202
2324
  debugBlock: debug ? buildDebugBlock(runId, result, store) : undefined,
2203
2325
  });
2326
+ appendIdentityBlock(content, null, ownership.executionId);
2204
2327
  return { content };
2205
2328
  }
2206
2329
  catch (error) {
2330
+ // Only a bound attempt owns the persisted record. Before bind, a failed
2331
+ // beginResumeAttempt leaves in-memory state "pending" while disk still
2332
+ // holds the settled run's terminal bytes; persisting it would displace
2333
+ // that settled execution.
2334
+ const owned = ownership.executionId !== undefined;
2207
2335
  if (ownedSignal.aborted || error instanceof WorkflowCancellationError) {
2208
- await markWorkflowCancelled(store);
2336
+ if (owned)
2337
+ await markWorkflowCancelled(store);
2209
2338
  throw new WorkflowCancellationError();
2210
2339
  }
2340
+ // A verified-repair store is never bound: this is the pre-mutex rejection
2341
+ // reached when that read failed, so report it the same way (-32602).
2342
+ if (!owned && store?.runState.workflowName === VERIFIED_REPAIR_WORKFLOW)
2343
+ throw error;
2211
2344
  // Persist a terminal "failed" record for post-admission setup failures.
2212
2345
  // Idempotent: won't clobber an engine-emitted terminal status.
2213
- if (store != null) {
2346
+ if (owned && store != null) {
2214
2347
  try {
2215
2348
  await markWorkflowFailed(store, runId, error);
2216
2349
  }
@@ -2236,6 +2369,7 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
2236
2369
  summary: failureSummary,
2237
2370
  debugBlock: debug && store != null ? buildErrorDebugBlock(runId, store) : undefined,
2238
2371
  });
2372
+ appendIdentityBlock(content, null, ownership.executionId);
2239
2373
  return { content, isError: true };
2240
2374
  }
2241
2375
  finally {
@@ -2336,9 +2470,18 @@ function parseLifecycleForStatus(value) {
2336
2470
  return undefined;
2337
2471
  }
2338
2472
  }
2339
- async function executeWorkflowRunStatus(_options, args) {
2473
+ function expectedExecutionId(args) {
2474
+ const expected = args["expected_execution_id"];
2475
+ if (expected === undefined)
2476
+ return undefined;
2477
+ if (typeof expected !== "string" || expected.length === 0 || expected.length > 128) {
2478
+ throw new McpError(ErrorCode.InvalidParams, "expected_execution_id must be a nonempty string of at most 128 characters");
2479
+ }
2480
+ return expected;
2481
+ }
2482
+ /** Shared by resume and pinned/unpinned status/wait, preserving their existing messages. */
2483
+ function runIdArg(args) {
2340
2484
  const runId = args["run_id"];
2341
- const includeResult = args["include_result"] === true;
2342
2485
  if (typeof runId !== "string" || runId === "") {
2343
2486
  throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid "run_id" parameter');
2344
2487
  }
@@ -2348,12 +2491,242 @@ async function executeWorkflowRunStatus(_options, args) {
2348
2491
  catch {
2349
2492
  throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
2350
2493
  }
2351
- const read = await readRunStateForStatus(runId);
2494
+ return runId;
2495
+ }
2496
+ function identityError(runId, reason) {
2497
+ return {
2498
+ isError: true,
2499
+ content: [{ type: "text", text: JSON.stringify({ run_id: runId, error: reason }) }],
2500
+ };
2501
+ }
2502
+ /**
2503
+ * Project status for one bound execution and fence it. A settled execution is
2504
+ * fenced on the digest of the exact state bytes the projection parsed (one
2505
+ * state.json read on the success path); an active one only on provenance and
2506
+ * directory incarnation, since it legitimately republishes state.
2507
+ */
2508
+ async function projectGuardedStatus(options, args, entry) {
2509
+ const wasSettled = entry.settled;
2510
+ const wasStamped = entry.stamped;
2511
+ const guard = {};
2512
+ let result;
2513
+ try {
2514
+ result = await projectWorkflowRunStatus(options, args, guard);
2515
+ }
2516
+ catch (error) {
2517
+ const reread = wasSettled ? (await inspectPublication(entry.runDir))?.fingerprint ?? null : undefined;
2518
+ const failure = await completions.check(entry, reread);
2519
+ if (failure !== undefined)
2520
+ return { failure };
2521
+ if (error instanceof ContainedFileChangedError) {
2522
+ return { failure: wasSettled ? "identity_unavailable" : "state_read_unstable" };
2523
+ }
2524
+ throw error;
2525
+ }
2526
+ const failure = await completions.check(entry, wasSettled ? guard.stateDigest ?? null : undefined);
2527
+ if (failure !== undefined)
2528
+ return { failure };
2529
+ // Another process sharing the state dir can resume the run in place, which
2530
+ // keeps the directory incarnation; only the persisted id shows the change.
2531
+ if (wasStamped && guard.executionId !== entry.executionId) {
2532
+ return { failure: typeof guard.executionId === "string" ? "identity_changed" : "identity_unavailable" };
2533
+ }
2534
+ return { result };
2535
+ }
2536
+ async function executeWorkflowRunStatus(options, args) {
2537
+ const expected = expectedExecutionId(args);
2538
+ if (expected === undefined)
2539
+ return projectWorkflowRunStatus(options, args);
2540
+ const runId = runIdArg(args);
2541
+ const selected = completions.select(getRunStateDir(runId), expected);
2542
+ if (selected === "identity_unavailable") {
2543
+ const persisted = await projectPersistedSettledStatus(options, args, runId, expected);
2544
+ return "failure" in persisted ? identityError(runId, persisted.failure) : persisted.result;
2545
+ }
2546
+ if (typeof selected === "string")
2547
+ return identityError(runId, selected);
2548
+ if (selected === undefined)
2549
+ return identityError(runId, "identity_unavailable");
2550
+ const release = completions.retain(selected);
2551
+ try {
2552
+ const before = await completions.check(selected);
2553
+ if (before !== undefined)
2554
+ return identityError(runId, before);
2555
+ const fenced = await projectGuardedStatus(options, args, selected);
2556
+ return "failure" in fenced ? identityError(runId, fenced.failure) : fenced.result;
2557
+ }
2558
+ finally {
2559
+ release();
2560
+ }
2561
+ }
2562
+ async function executeWorkflowRunWait(options, args, signal) {
2563
+ const started = Date.now();
2564
+ const runId = runIdArg(args);
2565
+ const expected = expectedExecutionId(args);
2566
+ const selected = completions.select(getRunStateDir(runId), expected);
2567
+ let outcome;
2568
+ let entry;
2569
+ let persistedExecutionId;
2570
+ const release = typeof selected === "object" ? completions.retain(selected) : undefined;
2571
+ const respond = (projection = { run_id: runId }) => {
2572
+ if (signal.aborted)
2573
+ throw new WorkflowCancellationError();
2574
+ return {
2575
+ content: [{
2576
+ type: "text",
2577
+ text: JSON.stringify({
2578
+ ...projection,
2579
+ wait: {
2580
+ outcome,
2581
+ ...(entry !== undefined ? { execution_id: entry.executionId } : {}),
2582
+ ...(persistedExecutionId !== undefined ? { execution_id: persistedExecutionId } : {}),
2583
+ budget_ms: WORKFLOW_WAIT_BUDGET_MS,
2584
+ elapsed_ms: Math.max(0, Date.now() - started),
2585
+ },
2586
+ }),
2587
+ }],
2588
+ };
2589
+ };
2590
+ try {
2591
+ if (selected === "identity_unavailable" && expected !== undefined) {
2592
+ // No local record: settled only if state.json proves this execution ended.
2593
+ const persisted = await projectPersistedSettledStatus(options, { run_id: runId }, runId, expected);
2594
+ if (signal.aborted)
2595
+ throw new WorkflowCancellationError();
2596
+ if ("failure" in persisted) {
2597
+ outcome = persisted.failure;
2598
+ return respond();
2599
+ }
2600
+ outcome = "settled";
2601
+ persistedExecutionId = expected;
2602
+ return respond(JSON.parse(persisted.result.content[0].text));
2603
+ }
2604
+ if (typeof selected === "string")
2605
+ outcome = selected;
2606
+ else if (selected === undefined)
2607
+ outcome = "not_owned";
2608
+ else {
2609
+ entry = selected;
2610
+ outcome = await completions.wait(entry, signal);
2611
+ const failure = await completions.check(entry);
2612
+ if (failure !== undefined)
2613
+ outcome = failure;
2614
+ }
2615
+ if (signal.aborted)
2616
+ throw new WorkflowCancellationError();
2617
+ // Never inspect a replacement or unreadable state after provenance fails.
2618
+ if (outcome === "identity_changed" || outcome === "identity_unavailable")
2619
+ return respond();
2620
+ // Metadata only: the result body is fetched separately through the guarded status tool.
2621
+ if (entry !== undefined) {
2622
+ const fenced = await projectGuardedStatus(options, { run_id: runId }, entry);
2623
+ if (signal.aborted)
2624
+ throw new WorkflowCancellationError();
2625
+ if ("failure" in fenced) {
2626
+ // An unstable active read is a normal re-arm, never a hard error.
2627
+ outcome = fenced.failure === "state_read_unstable" ? "budget_elapsed" : fenced.failure;
2628
+ return respond();
2629
+ }
2630
+ return respond(JSON.parse(fenced.result.content[0].text));
2631
+ }
2632
+ const status = await projectWorkflowRunStatus(options, { run_id: runId });
2633
+ const projection = JSON.parse(status.content[0].text);
2634
+ if (projection["status"] === "not_found")
2635
+ outcome = "not_found";
2636
+ else if (projection["status"] === "orphaned")
2637
+ outcome = "orphaned";
2638
+ if (signal.aborted)
2639
+ throw new WorkflowCancellationError();
2640
+ return respond(projection);
2641
+ }
2642
+ finally {
2643
+ release?.();
2644
+ }
2645
+ }
2646
+ /**
2647
+ * A pin with no local record (bound by an earlier process, or evicted here) is
2648
+ * resolved from the executionId persisted in state.json. The id, terminal
2649
+ * status and released run.lock are checked on the exact bytes the projection
2650
+ * then renders. After the projection a second contained read must return the
2651
+ * same bytes with the lock still released, the directory incarnation must be
2652
+ * unchanged, and no local execution may have bound the run dir; otherwise the
2653
+ * read fails closed, since a foreign resume keeps the generation.
2654
+ */
2655
+ async function projectPersistedSettledStatus(options, args, runId, expected) {
2656
+ const runDir = getRunStateDir(runId);
2657
+ const latest = completions.select(runDir);
2658
+ if (latest !== undefined && !latest.settled)
2659
+ return { failure: "identity_changed" };
2660
+ const before = await readSessionLogGeneration(runDir);
2661
+ if (before.kind !== "available")
2662
+ return { failure: "identity_unavailable" };
2663
+ const guard = { persistedExecutionId: expected };
2664
+ let result;
2665
+ let projectionError;
2666
+ try {
2667
+ result = await projectWorkflowRunStatus(options, args, guard);
2668
+ }
2669
+ catch (error) {
2670
+ // Unreadable, oversized, malformed or persistently changing state.
2671
+ if (guard.persistedIdentity === undefined)
2672
+ return { failure: "identity_unavailable" };
2673
+ projectionError = error;
2674
+ }
2675
+ if (guard.persistedIdentity !== "matched")
2676
+ return { failure: guard.persistedIdentity ?? "identity_unavailable" };
2677
+ let reread;
2678
+ try {
2679
+ reread = await readRunStateForStatus(runId, true);
2680
+ }
2681
+ catch {
2682
+ return { failure: "identity_unavailable" };
2683
+ }
2684
+ if (reread == null)
2685
+ return { failure: "identity_unavailable" };
2686
+ // Same rule as the first check: only a different stamped id is a change.
2687
+ const rereadId = reread.state.executionId;
2688
+ if (typeof rereadId === "string" && rereadId !== expected)
2689
+ return { failure: "identity_changed" };
2690
+ if (reread.stateDigest !== guard.stateDigest || reread.lock !== "stale_or_absent") {
2691
+ return { failure: "identity_unavailable" };
2692
+ }
2693
+ const after = await readSessionLogGeneration(runDir);
2694
+ // A new local bind supersedes the pin; mere eviction of `latest` does not.
2695
+ const current = completions.select(runDir);
2696
+ if (current !== undefined && current !== latest)
2697
+ return { failure: "identity_changed" };
2698
+ if (after.kind !== "available")
2699
+ return { failure: "identity_unavailable" };
2700
+ if (after.sessionLogGeneration !== before.sessionLogGeneration)
2701
+ return { failure: "identity_changed" };
2702
+ if (result !== undefined)
2703
+ return { result };
2704
+ // Same mapping as the in-memory settled path: a racing contained read fails closed.
2705
+ if (projectionError instanceof ContainedFileChangedError)
2706
+ return { failure: "identity_unavailable" };
2707
+ throw projectionError;
2708
+ }
2709
+ async function projectWorkflowRunStatus(options, args, guard) {
2710
+ const runId = runIdArg(args);
2711
+ const includeResult = args["include_result"] === true;
2712
+ const read = await readRunStateForStatus(runId, guard !== undefined);
2352
2713
  if (read == null) {
2714
+ if (guard?.persistedExecutionId !== undefined)
2715
+ guard.persistedIdentity = "identity_unavailable";
2353
2716
  return {
2354
2717
  content: [{ type: "text", text: JSON.stringify({ status: "not_found", run_id: runId }) }],
2355
2718
  };
2356
2719
  }
2720
+ if (guard !== undefined) {
2721
+ guard.stateDigest = read.stateDigest;
2722
+ guard.executionId = read.state.executionId;
2723
+ }
2724
+ if (guard?.persistedExecutionId !== undefined) {
2725
+ // Checked before any further I/O, on the bytes this projection renders.
2726
+ guard.persistedIdentity = persistedIdentityFailure(read.state, read.lock, runId, guard.persistedExecutionId) ?? "matched";
2727
+ if (guard.persistedIdentity !== "matched")
2728
+ return { content: [] };
2729
+ }
2357
2730
  const { state, lock } = read;
2358
2731
  const status = (state.status === "running" || state.status === "pending") && lock === "stale_or_absent"
2359
2732
  ? "orphaned"
@@ -2396,6 +2769,7 @@ async function executeWorkflowRunStatus(_options, args) {
2396
2769
  };
2397
2770
  // artifacts_dir is cheap (pure join, no I/O) and present for every found run.
2398
2771
  projection["artifacts_dir"] = join(runDir, "artifacts");
2772
+ projection["verification"] = await readWorkflowVerification(getRunsDir(), runId);
2399
2773
  // Read-only context-selection observability from already-persisted state.
2400
2774
  // No file I/O — only projects state.workflow_context.context_builder and
2401
2775
  // the state.context_builder_lifecycle snapshot.
@@ -2578,6 +2952,47 @@ async function executeWorkflowRunStatus(_options, args) {
2578
2952
  }
2579
2953
  }
2580
2954
  }
2955
+ if (state.workflowName === ARCHITECTURE_WORKFLOW) {
2956
+ const comparison = await inspectArchitectureComparisonRecord(runDir, runId, includeResult);
2957
+ const { result, ...status } = comparison;
2958
+ projection["comparison"] = status;
2959
+ projection["result_available"] = status["status"] === "complete" || status["status"] === "incomplete";
2960
+ if (result !== undefined)
2961
+ projection["result"] = result;
2962
+ else
2963
+ delete projection["result"];
2964
+ }
2965
+ if (state.workflowName === VERIFIED_REPAIR_WORKFLOW) {
2966
+ let inspected = await inspectVerifiedRepairRecord(runDir, runId, includeResult);
2967
+ if (inspected.availability === "unrecorded" && lock === "stale_or_absent" && !activeRuns.has(runId)) {
2968
+ try {
2969
+ const value = await options.verifiedRepairHost?.readInterruptedRun?.({ runId, runDir });
2970
+ if (value === undefined)
2971
+ throw new Error("interrupted capture unavailable");
2972
+ const record = RepairRecordSchema.parse(value);
2973
+ if (record.run_id !== runId || record.execution_status !== "interrupted")
2974
+ throw new Error("interrupted repair record identity mismatch");
2975
+ assertRepairBounded(record, MAX_REPAIR_RECORD_BYTES);
2976
+ const serialized = JSON.stringify(record);
2977
+ inspected = { availability: "available", result_available: true,
2978
+ result_bytes: Buffer.byteLength(serialized), owner_acceptance: "not_recorded",
2979
+ execution_status: "interrupted", repair_status: record.repair_status,
2980
+ ...(includeResult ? { result: serialized, record } : {}) };
2981
+ }
2982
+ catch {
2983
+ inspected = { availability: "unavailable", result_available: false, result_bytes: 0,
2984
+ owner_acceptance: "not_recorded", reason: "interrupted_capture_unavailable" };
2985
+ }
2986
+ }
2987
+ const { result, record: _record, result_available, result_bytes, ...repair } = inspected;
2988
+ projection["repair"] = { ...repair, scope: repair.execution_status === "interrupted" ? "inspected_at" : "recorded_at" };
2989
+ projection["result_available"] = result_available;
2990
+ projection["result_bytes"] = result_bytes;
2991
+ if (result !== undefined)
2992
+ projection["result"] = result;
2993
+ else
2994
+ delete projection["result"];
2995
+ }
2581
2996
  return {
2582
2997
  content: [{ type: "text", text: JSON.stringify(projection) }],
2583
2998
  };
@@ -2680,7 +3095,7 @@ function executeListWorkflows(options) {
2680
3095
  supports_members: runtimeMembers,
2681
3096
  supports_chairman: runtimeMembers,
2682
3097
  supports_context_preflight: contextStrategy.supports_context_preflight,
2683
- effective_context_default: effectiveContextDefault,
3098
+ effective_context_default: name === VERIFIED_REPAIR_WORKFLOW ? "disabled" : effectiveContextDefault,
2684
3099
  fails_on_empty_selection: contextStrategy.fails_on_empty_selection,
2685
3100
  context_anchor_variables: contextStrategy.context_anchor_variables,
2686
3101
  context_override_variables: contextStrategy.context_override_variables,
@@ -2894,7 +3309,7 @@ const RUN_WORKFLOW_SCHEMA = {
2894
3309
  async: {
2895
3310
  type: "boolean",
2896
3311
  default: false,
2897
- description: "When true, start the run detached and return {run_id, status:'running'} immediately; poll workflow_run_status for progress and result.",
3312
+ description: "When true, start detached and return {run_id, execution_id, status:'running'} immediately. Prefer workflow_run_wait for local completion, then workflow_run_status with expected_execution_id and include_result:true; status polling remains available for recovery. When false (blocking), the final content block carries execution_id once the run owned state.",
2898
3313
  },
2899
3314
  roster: {
2900
3315
  type: "string",
@@ -2987,6 +3402,12 @@ const LIST_WORKFLOWS_SCHEMA = {
2987
3402
  properties: {},
2988
3403
  additionalProperties: false,
2989
3404
  };
3405
+ const EXECUTION_ID_SCHEMA = {
3406
+ type: "string",
3407
+ minLength: 1,
3408
+ maxLength: 128,
3409
+ description: "Opaque execution identity (also persisted in run state) returned by run_workflow (async ack or trailing sync block), resume_workflow, or a bare wait. A fence, not a capability: it grants nothing and only makes reads fail closed (identity_changed/identity_unavailable) if they would observe a different execution.",
3410
+ };
2990
3411
  const WORKFLOW_RUN_STATUS_SCHEMA = {
2991
3412
  type: "object",
2992
3413
  properties: {
@@ -2994,6 +3415,7 @@ const WORKFLOW_RUN_STATUS_SCHEMA = {
2994
3415
  type: "string",
2995
3416
  pattern: "^[A-Za-z0-9_-]{1,64}$",
2996
3417
  },
3418
+ expected_execution_id: EXECUTION_ID_SCHEMA,
2997
3419
  include_result: {
2998
3420
  type: "boolean",
2999
3421
  default: false,
@@ -3003,6 +3425,15 @@ const WORKFLOW_RUN_STATUS_SCHEMA = {
3003
3425
  required: ["run_id"],
3004
3426
  additionalProperties: false,
3005
3427
  };
3428
+ const WORKFLOW_RUN_WAIT_SCHEMA = {
3429
+ type: "object",
3430
+ properties: {
3431
+ run_id: { type: "string", pattern: "^[A-Za-z0-9_-]{1,64}$" },
3432
+ expected_execution_id: EXECUTION_ID_SCHEMA,
3433
+ },
3434
+ required: ["run_id"],
3435
+ additionalProperties: false,
3436
+ };
3006
3437
  const CANCEL_WORKFLOW_SCHEMA = {
3007
3438
  type: "object",
3008
3439
  properties: {
@@ -3114,13 +3545,14 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
3114
3545
  return [
3115
3546
  {
3116
3547
  name: "run_workflow",
3117
- description: `Start a new multi-stage workflow execution.${workflowSuffix}\nAccepts optional \`members\` and \`chairman\` parameters for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover, council-plan-review) to override member/reviewer composition and chairman CLI adapter at runtime. Council runs include a \`resolved_roster\` of requested seat and delegate values (no provenance) on the async ack, a trailing sync content block, and workflow_run_status.`,
3548
+ description: `Start a new multi-stage workflow execution.${workflowSuffix}\nAccepts optional \`members\` and \`chairman\` parameters for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover, council-plan-review) to override member/reviewer composition and chairman CLI adapter at runtime. Council runs include a \`resolved_roster\` of requested seat and delegate values (no provenance) on the async ack, a trailing sync content block, and workflow_run_status. Blocking runs end with one trailing identity block {resolved_roster?, execution_id?}; execution_id is present once the run owned state, absent when rejected before that.`,
3118
3549
  inputSchema: runSchema,
3119
3550
  },
3120
3551
  {
3121
3552
  name: "resume_workflow",
3122
3553
  description: "Resume a previously failed or cancelled workflow from its last checkpoint. " +
3123
- "Requires the original run_id and matching child config.",
3554
+ "Requires the original run_id and matching child config. " +
3555
+ "Once the resume owns the run, the final content block is {execution_id} for this attempt; pin workflow_run_wait/workflow_run_status to it. A resume rejected before then returns no execution_id and leaves the previous execution observable.",
3124
3556
  inputSchema: RESUME_WORKFLOW_SCHEMA,
3125
3557
  },
3126
3558
  {
@@ -3131,10 +3563,16 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
3131
3563
  },
3132
3564
  {
3133
3565
  name: "workflow_run_status",
3134
- description: "Read-only status/progress of a workflow run. The full result is included only with include_result:true. Generic workflows preserve succeeded-final-call behavior; plan-review-receipts reports marker-driven availability and bytes independently of terminal status, with body-blind default polling. Every found run also carries a top-level `health` block {classification: \"ok\"|\"degraded\", reasons: string[]} computed read-time from persisted data — a run can be status:\"succeeded\" yet degraded when on_failure:continue swallowed failed calls or context preflight was partial/degraded (reasons: failed_calls_in_succeeded_run, context_partial, context_lifecycle_failed, coverage_degraded, delegate_fallback_used, context_external_gaps — the finalizer reported evidence gaps outside the delegate packet under external_gap_policy:\"degrade\", so conclusions depending on them are conditional). When a context-builder lifecycle snapshot is persisted, the `context` block additionally includes `lifecycle_status` and `coverage` {policy, status, required, selected, missing}. A parsed snapshot also projects `context.delegate.finalizer` {effective_enabled, execution_status, score?}; `score` is present only for a succeeded finalizer that recorded one, and the finalizer key is omitted when the lifecycle is missing, unparseable, or those fields are absent. While context preflight is still running (non-terminal lifecycle, no committed context yet) the `context` block is absent and a top-level `context_progress` {phase, status, updated_at, counts} reports scan progress instead. Found council runs also include a top-level `resolved_roster` of requested seat and delegate values (no provenance) when the run persisted one; older runs omit the key.",
3566
+ description: "Read-only status/progress of a workflow run. The full result is included only with include_result:true. Generic workflows preserve succeeded-final-call behavior; plan-review-receipts reports marker-driven availability and bytes independently of terminal status, with body-blind default polling. Every found run also carries a top-level `health` block {classification: \"ok\"|\"degraded\", reasons: string[]} computed read-time from persisted data — a run can be status:\"succeeded\" yet degraded when on_failure:continue swallowed failed calls or context preflight was partial/degraded (reasons: failed_calls_in_succeeded_run, context_partial, context_lifecycle_failed, coverage_degraded, delegate_fallback_used, context_external_gaps — the finalizer reported evidence gaps outside the delegate packet under external_gap_policy:\"degrade\", so conclusions depending on them are conditional). When a context-builder lifecycle snapshot is persisted, the `context` block additionally includes `lifecycle_status` and `coverage` {policy, status, required, selected, missing}. A parsed snapshot also projects `context.delegate.finalizer` {effective_enabled, execution_status, score?}; `score` is present only for a succeeded finalizer that recorded one, and the finalizer key is omitted when the lifecycle is missing, unparseable, or those fields are absent. While context preflight is still running (non-terminal lifecycle, no committed context yet) the `context` block is absent and a top-level `context_progress` {phase, status, updated_at, counts} reports scan progress instead. Found council runs also include a top-level `resolved_roster` of requested seat and delegate values (no provenance) when the run persisted one; older runs omit the key. Architecture comparisons additionally project comparison completeness and retained record availability independently of run execution status. Every found run has a separately versioned verification projection of historical inspected-at evidence; it establishes neither current source/runtime applicability nor owner acceptance. With expected_execution_id the response is fenced to that execution: a mismatch is isError {run_id, error:\"identity_changed\"|\"identity_unavailable\"} with no result, and an active execution whose state kept changing through every bounded reread is isError {run_id, error:\"state_read_unstable\"} — retryable; re-issue the same pinned call after a short backoff.",
3135
3567
  inputSchema: WORKFLOW_RUN_STATUS_SCHEMA,
3136
3568
  annotations: { readOnlyHint: true },
3137
3569
  },
3570
+ {
3571
+ name: "workflow_run_wait",
3572
+ description: "Observe one local workflow execution through its cleanup fence for at most 30 seconds. Returns metadata status plus wait.outcome and execution_id; wait.budget_ms is the configured budget, not the time waited (see elapsed_ms). Re-arm budget_elapsed with the same identity; a persistently unstable state read on an active execution also re-arms as budget_elapsed, without projection. not_owned, orphaned and not_found return immediately: no local execution is witnessable here yet or any longer (a request still admitting has not bound one). expected_execution_id is a fence, not a capability. The settled fingerprint covers state.json bytes only, not result files. Cancellation detaches observation only. Unpinned foreign/historical state is not witnessed settlement; a pin whose execution reached terminal state settles from persisted state (e.g. after a restart). Fetch final results via workflow_run_status with expected_execution_id (it may return the retryable error state_read_unstable for an active execution; re-issue the same pinned call).",
3573
+ inputSchema: WORKFLOW_RUN_WAIT_SCHEMA,
3574
+ annotations: { readOnlyHint: true },
3575
+ },
3138
3576
  {
3139
3577
  name: "cancel_workflow",
3140
3578
  description: "Cancel a workflow run by run_id. Aborts a live in-process async run (returns status:'cancelling'; poll workflow_run_status until it reaches terminal 'cancelled'). Reaps an orphaned/no-live-worker run to terminal 'cancelled' (reaped:true). Refuses a lock-held run owned by another live worker or a blocking run (cancellable:false). Idempotent; not_found and already_terminal are soft results.",
@@ -3149,6 +3587,7 @@ export const WORKFLOW_TOOL_NAMES = new Set([
3149
3587
  "resume_workflow",
3150
3588
  "list_workflows",
3151
3589
  "workflow_run_status",
3590
+ "workflow_run_wait",
3152
3591
  "cancel_workflow",
3153
3592
  ]);
3154
3593
  /**
@@ -3167,6 +3606,8 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
3167
3606
  return executeListWorkflows(options);
3168
3607
  case "workflow_run_status":
3169
3608
  return await executeWorkflowRunStatus(options, args);
3609
+ case "workflow_run_wait":
3610
+ return await executeWorkflowRunWait(options, args, signal);
3170
3611
  case "cancel_workflow":
3171
3612
  return await executeCancelWorkflow(options, args);
3172
3613
  default:
@@ -3180,8 +3621,8 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
3180
3621
  * Register workflow tools on an MCP Server instance.
3181
3622
  *
3182
3623
  * This is the reusable core extracted from createWorkflowServer().
3183
- * It registers five tools: run_workflow, resume_workflow, list_workflows,
3184
- * workflow_run_status, cancel_workflow.
3624
+ * It registers six tools: run_workflow, resume_workflow, list_workflows,
3625
+ * workflow_run_status, workflow_run_wait, cancel_workflow.
3185
3626
  *
3186
3627
  * The `options.createBackend` factory determines how clink calls are
3187
3628
  * dispatched — callers provide either a child-process or in-process backend.