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.
- package/CHANGELOG.md +60 -0
- package/README.md +97 -13
- package/clinkx-workflows/dist/allowed-roots.js +2 -0
- package/clinkx-workflows/dist/allowed-roots.js.map +1 -1
- package/clinkx-workflows/dist/architecture-comparison/admission.d.ts +5 -0
- package/clinkx-workflows/dist/architecture-comparison/admission.js +50 -0
- package/clinkx-workflows/dist/architecture-comparison/admission.js.map +1 -0
- package/clinkx-workflows/dist/architecture-comparison/contract.d.ts +995 -0
- package/clinkx-workflows/dist/architecture-comparison/contract.js +149 -0
- package/clinkx-workflows/dist/architecture-comparison/contract.js.map +1 -0
- package/clinkx-workflows/dist/architecture-comparison/runtime.d.ts +29 -0
- package/clinkx-workflows/dist/architecture-comparison/runtime.js +387 -0
- package/clinkx-workflows/dist/architecture-comparison/runtime.js.map +1 -0
- package/clinkx-workflows/dist/child-env.d.ts +3 -1
- package/clinkx-workflows/dist/child-env.js +6 -2
- package/clinkx-workflows/dist/child-env.js.map +1 -1
- package/clinkx-workflows/dist/config.d.ts +28 -5
- package/clinkx-workflows/dist/config.js +137 -20
- package/clinkx-workflows/dist/config.js.map +1 -1
- package/clinkx-workflows/dist/contained-file.js +10 -1
- package/clinkx-workflows/dist/contained-file.js.map +1 -1
- package/clinkx-workflows/dist/council-execution-policy.d.ts +4 -2
- package/clinkx-workflows/dist/council-execution-policy.js +5 -1
- package/clinkx-workflows/dist/council-execution-policy.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +72 -72
- package/clinkx-workflows/dist/dashboard/contracts.d.ts +1564 -122
- package/clinkx-workflows/dist/dashboard/contracts.js +4 -0
- package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +12 -12
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +8 -8
- package/clinkx-workflows/dist/dashboard/scanner.d.ts +3 -0
- package/clinkx-workflows/dist/dashboard/scanner.js +27 -0
- package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +48 -48
- package/clinkx-workflows/dist/dashboard/session-log-identity.js +2 -1
- package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +2 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js +4 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/verified-repair-contract.d.ts +191 -0
- package/clinkx-workflows/dist/dashboard/verified-repair-contract.js +28 -0
- package/clinkx-workflows/dist/dashboard/verified-repair-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/index-BdpmbPps.js +115 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/style-Im5i1iAs.css +1 -0
- package/clinkx-workflows/dist/dashboard-ui/index.html +2 -2
- package/clinkx-workflows/dist/engine.d.ts +6 -0
- package/clinkx-workflows/dist/engine.js +78 -7
- package/clinkx-workflows/dist/engine.js.map +1 -1
- package/clinkx-workflows/dist/index.d.ts +3 -0
- package/clinkx-workflows/dist/index.js +3 -2
- package/clinkx-workflows/dist/index.js.map +1 -1
- package/clinkx-workflows/dist/reducers/index.d.ts +9 -1
- package/clinkx-workflows/dist/reducers/index.js +22 -0
- package/clinkx-workflows/dist/reducers/index.js.map +1 -1
- package/clinkx-workflows/dist/root-path-validation.d.ts +12 -0
- package/clinkx-workflows/dist/root-path-validation.js +63 -0
- package/clinkx-workflows/dist/root-path-validation.js.map +1 -0
- package/clinkx-workflows/dist/rosters.d.ts +1 -0
- package/clinkx-workflows/dist/rosters.js +16 -6
- package/clinkx-workflows/dist/rosters.js.map +1 -1
- package/clinkx-workflows/dist/server.js +5 -2
- package/clinkx-workflows/dist/server.js.map +1 -1
- package/clinkx-workflows/dist/state-schema.d.ts +13 -0
- package/clinkx-workflows/dist/state.d.ts +7 -1
- package/clinkx-workflows/dist/state.js +90 -19
- package/clinkx-workflows/dist/state.js.map +1 -1
- package/clinkx-workflows/dist/validators/architecture-comparison.d.ts +2 -0
- package/clinkx-workflows/dist/validators/architecture-comparison.js +12 -0
- package/clinkx-workflows/dist/validators/architecture-comparison.js.map +1 -0
- package/clinkx-workflows/dist/validators/index.js +4 -0
- package/clinkx-workflows/dist/validators/index.js.map +1 -1
- package/clinkx-workflows/dist/validators/verified-repair.d.ts +2 -0
- package/clinkx-workflows/dist/validators/verified-repair.js +12 -0
- package/clinkx-workflows/dist/validators/verified-repair.js.map +1 -0
- package/clinkx-workflows/dist/verification-proof.d.ts +254 -0
- package/clinkx-workflows/dist/verification-proof.js +195 -0
- package/clinkx-workflows/dist/verification-proof.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/admission.d.ts +72 -0
- package/clinkx-workflows/dist/verified-repair/admission.js +63 -0
- package/clinkx-workflows/dist/verified-repair/admission.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/contract.d.ts +1910 -0
- package/clinkx-workflows/dist/verified-repair/contract.js +127 -0
- package/clinkx-workflows/dist/verified-repair/contract.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/host.d.ts +32 -0
- package/clinkx-workflows/dist/verified-repair/host.js +2 -0
- package/clinkx-workflows/dist/verified-repair/host.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/prompts.d.ts +12 -0
- package/clinkx-workflows/dist/verified-repair/prompts.js +58 -0
- package/clinkx-workflows/dist/verified-repair/prompts.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/result.d.ts +16 -0
- package/clinkx-workflows/dist/verified-repair/result.js +102 -0
- package/clinkx-workflows/dist/verified-repair/result.js.map +1 -0
- package/clinkx-workflows/dist/verified-repair/runtime.d.ts +28 -0
- package/clinkx-workflows/dist/verified-repair/runtime.js +186 -0
- package/clinkx-workflows/dist/verified-repair/runtime.js.map +1 -0
- package/clinkx-workflows/dist/workflow-completion.d.ts +83 -0
- package/clinkx-workflows/dist/workflow-completion.js +250 -0
- package/clinkx-workflows/dist/workflow-completion.js.map +1 -0
- package/clinkx-workflows/dist/workflow-tools.d.ts +7 -3
- package/clinkx-workflows/dist/workflow-tools.js +487 -46
- package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
- package/clinkx-workflows/dist/workflow-verification-contract.d.ts +539 -0
- package/clinkx-workflows/dist/workflow-verification-contract.js +52 -0
- package/clinkx-workflows/dist/workflow-verification-contract.js.map +1 -0
- package/clinkx-workflows/dist/workflow-verification.d.ts +15 -0
- package/clinkx-workflows/dist/workflow-verification.js +218 -0
- package/clinkx-workflows/dist/workflow-verification.js.map +1 -0
- package/clinkx-workflows/templates/architecture-comparison/candidate.txt +29 -0
- package/clinkx-workflows/templates/architecture-comparison/judge.txt +31 -0
- package/clinkx-workflows/templates/verified-repair/investigate.txt +20 -0
- package/clinkx-workflows/templates/verified-repair/repair.txt +11 -0
- package/clinkx-workflows/templates/verified-repair/review.txt +18 -0
- package/clinkx-workflows/workflows/architecture-comparison.yaml +133 -0
- package/clinkx-workflows/workflows/verified-repair.yaml +104 -0
- package/dist/allowed-roots.d.ts +1 -1
- package/dist/allowed-roots.js +128 -15
- package/dist/allowed-roots.js.map +1 -1
- package/dist/codex/app-server-client.d.ts +1 -0
- package/dist/codex/app-server-client.js +3 -2
- package/dist/codex/app-server-client.js.map +1 -1
- package/dist/context-builder/config.d.ts +1 -0
- package/dist/context-builder/config.js +21 -9
- package/dist/context-builder/config.js.map +1 -1
- package/dist/context-builder/diagnostics.d.ts +1 -0
- package/dist/context-builder/diagnostics.js +1 -1
- package/dist/context-builder/diagnostics.js.map +1 -1
- package/dist/context-builder/discovery.d.ts +18 -1
- package/dist/context-builder/discovery.js +69 -1
- package/dist/context-builder/discovery.js.map +1 -1
- package/dist/context-builder/handler.js +8 -1
- package/dist/context-builder/handler.js.map +1 -1
- package/dist/context-builder/lifecycle-contract.d.ts +24 -24
- package/dist/context-builder/mutation-lock.d.ts +4 -0
- package/dist/context-builder/mutation-lock.js +7 -1
- package/dist/context-builder/mutation-lock.js.map +1 -1
- package/dist/context-builder/refine-handler.js +16 -1
- package/dist/context-builder/refine-handler.js.map +1 -1
- package/dist/context-builder/secrets.d.ts +11 -0
- package/dist/context-builder/secrets.js +75 -0
- package/dist/context-builder/secrets.js.map +1 -1
- package/dist/context-builder/storage.d.ts +10 -2
- package/dist/context-builder/storage.js +213 -16
- package/dist/context-builder/storage.js.map +1 -1
- package/dist/context-builder/versions.d.ts +5 -2
- package/dist/context-builder/versions.js +5 -2
- package/dist/context-builder/versions.js.map +1 -1
- package/dist/dashboard-cli.d.ts +6 -0
- package/dist/dashboard-cli.js +18 -32
- package/dist/dashboard-cli.js.map +1 -1
- package/dist/dashboard-gateway/main.js +8 -3
- package/dist/dashboard-gateway/main.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/project-verification/contract.d.ts +537 -0
- package/dist/project-verification/contract.js +40 -0
- package/dist/project-verification/contract.js.map +1 -0
- package/dist/project-verification/files.d.ts +23 -0
- package/dist/project-verification/files.js +80 -0
- package/dist/project-verification/files.js.map +1 -0
- package/dist/project-verification/process.d.ts +7 -0
- package/dist/project-verification/process.js +115 -0
- package/dist/project-verification/process.js.map +1 -0
- package/dist/project-verification/runner.d.ts +35 -0
- package/dist/project-verification/runner.js +185 -0
- package/dist/project-verification/runner.js.map +1 -0
- package/dist/project-verification/service.d.ts +37 -0
- package/dist/project-verification/service.js +151 -0
- package/dist/project-verification/service.js.map +1 -0
- package/dist/provider-doctor.d.ts +22 -0
- package/dist/provider-doctor.js +22 -0
- package/dist/provider-doctor.js.map +1 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +5 -1
- package/dist/registry.js.map +1 -1
- package/dist/server.js +41 -6
- package/dist/server.js.map +1 -1
- package/dist/verification-tools.d.ts +90 -0
- package/dist/verification-tools.js +120 -0
- package/dist/verification-tools.js.map +1 -0
- package/dist/verified-repair/files.d.ts +22 -0
- package/dist/verified-repair/files.js +114 -0
- package/dist/verified-repair/files.js.map +1 -0
- package/dist/verified-repair/host.d.ts +14 -0
- package/dist/verified-repair/host.js +612 -0
- package/dist/verified-repair/host.js.map +1 -0
- package/docs/configuration.md +224 -12
- package/docs/engineering-recipes.md +415 -0
- package/docs/security.md +3 -0
- package/docs/verification/README.md +59 -0
- package/docs/verification/dashboard-real-disk.md +436 -0
- package/docs/verification/project-checks.example.json +14 -0
- package/docs/verification/project-checks.md +444 -0
- package/docs/verification/workflow-handoff.md +113 -0
- package/package.json +10 -2
- package/scripts/provider-doctor-core.mjs +380 -0
- package/scripts/provider-doctor-probe.mjs +34 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +0 -106
- 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
|
|
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
|
|
1120
|
-
* roster
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
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
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|