clinkx 0.3.0 → 1.1.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 +119 -0
- package/LICENSE +10 -0
- package/README.md +154 -26
- package/clinkx-workflows/dist/allowed-roots.d.ts +1 -0
- package/clinkx-workflows/dist/allowed-roots.js +5 -0
- package/clinkx-workflows/dist/allowed-roots.js.map +1 -0
- package/clinkx-workflows/dist/artifacts.d.ts +20 -1
- package/clinkx-workflows/dist/artifacts.js +38 -9
- package/clinkx-workflows/dist/artifacts.js.map +1 -1
- package/clinkx-workflows/dist/backend.d.ts +4 -0
- package/clinkx-workflows/dist/chairman-knobs.d.ts +24 -0
- package/clinkx-workflows/dist/chairman-knobs.js +27 -0
- package/clinkx-workflows/dist/chairman-knobs.js.map +1 -0
- package/clinkx-workflows/dist/citation-check.d.ts +40 -0
- package/clinkx-workflows/dist/citation-check.js +348 -0
- package/clinkx-workflows/dist/citation-check.js.map +1 -0
- package/clinkx-workflows/dist/clink-client.d.ts +12 -1
- package/clinkx-workflows/dist/clink-client.js +56 -5
- package/clinkx-workflows/dist/clink-client.js.map +1 -1
- package/clinkx-workflows/dist/config.d.ts +173 -16
- package/clinkx-workflows/dist/config.js +417 -26
- package/clinkx-workflows/dist/config.js.map +1 -1
- package/clinkx-workflows/dist/contained-file.d.ts +172 -0
- package/clinkx-workflows/dist/contained-file.js +483 -0
- package/clinkx-workflows/dist/contained-file.js.map +1 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.d.ts +416 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.js +328 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-contract.js.map +1 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.d.ts +24 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.js +39 -0
- package/clinkx-workflows/dist/context-builder-lifecycle-router.js.map +1 -0
- package/clinkx-workflows/dist/context-cache-key.d.ts +53 -0
- package/clinkx-workflows/dist/context-cache-key.js +91 -0
- package/clinkx-workflows/dist/context-cache-key.js.map +1 -0
- package/clinkx-workflows/dist/context-mode-resolver.d.ts +27 -0
- package/clinkx-workflows/dist/context-mode-resolver.js +126 -0
- package/clinkx-workflows/dist/context-mode-resolver.js.map +1 -0
- package/clinkx-workflows/dist/context-preflight.d.ts +189 -0
- package/clinkx-workflows/dist/context-preflight.js +91 -0
- package/clinkx-workflows/dist/context-preflight.js.map +1 -0
- package/clinkx-workflows/dist/council-execution-policy.d.ts +68 -0
- package/clinkx-workflows/dist/council-execution-policy.js +110 -0
- package/clinkx-workflows/dist/council-execution-policy.js.map +1 -0
- package/clinkx-workflows/dist/council-expander.d.ts +10 -9
- package/clinkx-workflows/dist/council-expander.js +20 -10
- package/clinkx-workflows/dist/council-expander.js.map +1 -1
- package/clinkx-workflows/dist/dashboard/activity-contract.d.ts +5718 -0
- package/clinkx-workflows/dist/dashboard/activity-contract.js +157 -0
- package/clinkx-workflows/dist/dashboard/activity-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.d.ts +25 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.js +34 -0
- package/clinkx-workflows/dist/dashboard/activity-lease.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.d.ts +90 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.js +493 -0
- package/clinkx-workflows/dist/dashboard/activity-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/api-routes.d.ts +40 -0
- package/clinkx-workflows/dist/dashboard/api-routes.js +1155 -0
- package/clinkx-workflows/dist/dashboard/api-routes.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.d.ts +37 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.js +145 -0
- package/clinkx-workflows/dist/dashboard/artifact-index.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.d.ts +31 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.js +65 -0
- package/clinkx-workflows/dist/dashboard/auth/challenge-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.d.ts +26 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.js +596 -0
- package/clinkx-workflows/dist/dashboard/auth/controller.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.d.ts +60 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.js +776 -0
- package/clinkx-workflows/dist/dashboard/auth/credential-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.d.ts +9 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js +44 -0
- package/clinkx-workflows/dist/dashboard/auth/enrollment-gate.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.d.ts +17 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.js +77 -0
- package/clinkx-workflows/dist/dashboard/auth/session-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.d.ts +25 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js +71 -0
- package/clinkx-workflows/dist/dashboard/auth/webauthn-service.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.d.ts +3865 -0
- package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js +69 -0
- package/clinkx-workflows/dist/dashboard/context-builder-invocation-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.d.ts +37 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.js +49 -0
- package/clinkx-workflows/dist/dashboard/context-run-liveness.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.d.ts +256 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.js +1301 -0
- package/clinkx-workflows/dist/dashboard/context-run-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/contracts.d.ts +28483 -0
- package/clinkx-workflows/dist/dashboard/contracts.js +1284 -0
- package/clinkx-workflows/dist/dashboard/contracts.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/generation.d.ts +14 -0
- package/clinkx-workflows/dist/dashboard/generation.js +41 -0
- package/clinkx-workflows/dist/dashboard/generation.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/index.d.ts +32 -0
- package/clinkx-workflows/dist/dashboard/index.js +20 -0
- package/clinkx-workflows/dist/dashboard/index.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/invocation-contract.d.ts +786 -0
- package/clinkx-workflows/dist/dashboard/invocation-contract.js +201 -0
- package/clinkx-workflows/dist/dashboard/invocation-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.d.ts +114 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.js +799 -0
- package/clinkx-workflows/dist/dashboard/lazy-content.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/contract.d.ts +90 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/contract.js +31 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/projection.d.ts +12 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/projection.js +286 -0
- package/clinkx-workflows/dist/dashboard/native-transcript/projection.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.d.ts +446 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js +221 -0
- package/clinkx-workflows/dist/dashboard/progress-report-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.d.ts +50 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js +529 -0
- package/clinkx-workflows/dist/dashboard/progress-report-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.d.ts +89 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js +71 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-overlay.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.d.ts +29 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js +111 -0
- package/clinkx-workflows/dist/dashboard/progress-report-status-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.d.ts +194 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js +89 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.d.ts +26 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js +102 -0
- package/clinkx-workflows/dist/dashboard/prompt-builder-library-store.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/read-model.d.ts +86 -0
- package/clinkx-workflows/dist/dashboard/read-model.js +440 -0
- package/clinkx-workflows/dist/dashboard/read-model.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/request-admission.d.ts +31 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js +130 -0
- package/clinkx-workflows/dist/dashboard/request-admission.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/scanner.d.ts +78 -0
- package/clinkx-workflows/dist/dashboard/scanner.js +1154 -0
- package/clinkx-workflows/dist/dashboard/scanner.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/server-factory.d.ts +63 -0
- package/clinkx-workflows/dist/dashboard/server-factory.js +483 -0
- package/clinkx-workflows/dist/dashboard/server-factory.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.d.ts +2778 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.js +506 -0
- package/clinkx-workflows/dist/dashboard/session-log-contract.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.d.ts +21 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.js +101 -0
- package/clinkx-workflows/dist/dashboard/session-log-identity.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.d.ts +43 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.js +529 -0
- package/clinkx-workflows/dist/dashboard/session-log-journal.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.d.ts +61 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.js +378 -0
- package/clinkx-workflows/dist/dashboard/session-log-reader.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.d.ts +14 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js +1101 -0
- package/clinkx-workflows/dist/dashboard/session-log-redaction.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.d.ts +23 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.js +212 -0
- package/clinkx-workflows/dist/dashboard/session-log-retention.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/static-responses.d.ts +10 -0
- package/clinkx-workflows/dist/dashboard/static-responses.js +94 -0
- package/clinkx-workflows/dist/dashboard/static-responses.js.map +1 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.d.ts +28 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.js +241 -0
- package/clinkx-workflows/dist/dashboard/worker-lock-observer.js.map +1 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/index-JPHQVtn9.js +106 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/clinkx-workflows/dist/dashboard-ui/assets/style-DOMHk-j6.css +1 -0
- package/clinkx-workflows/dist/dashboard-ui/contracts/council-plan-review-input-v1.json +158 -0
- package/clinkx-workflows/dist/dashboard-ui/index.html +15 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Mono-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/IBM-Plex-Sans-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/JetBrains-Mono-OFL.txt +93 -0
- package/clinkx-workflows/dist/dashboard-ui/licenses/NOTICE.md +32 -0
- package/clinkx-workflows/dist/dashboard-ui/theme-init.js +8 -0
- package/clinkx-workflows/dist/definition-normalizer.d.ts +19 -0
- package/clinkx-workflows/dist/definition-normalizer.js.map +1 -1
- package/clinkx-workflows/dist/delegate-finalizer-policy.d.ts +26 -0
- package/clinkx-workflows/dist/delegate-finalizer-policy.js +72 -0
- package/clinkx-workflows/dist/delegate-finalizer-policy.js.map +1 -0
- package/clinkx-workflows/dist/delegate-signature.d.ts +14 -0
- package/clinkx-workflows/dist/delegate-signature.js +14 -0
- package/clinkx-workflows/dist/delegate-signature.js.map +1 -0
- package/clinkx-workflows/dist/delegate-validate.d.ts +108 -0
- package/clinkx-workflows/dist/delegate-validate.js +148 -0
- package/clinkx-workflows/dist/delegate-validate.js.map +1 -0
- package/clinkx-workflows/dist/engine.d.ts +251 -0
- package/clinkx-workflows/dist/engine.js +1342 -92
- package/clinkx-workflows/dist/engine.js.map +1 -1
- package/clinkx-workflows/dist/errors.d.ts +24 -2
- package/clinkx-workflows/dist/errors.js +30 -2
- package/clinkx-workflows/dist/errors.js.map +1 -1
- package/clinkx-workflows/dist/fidelity.d.ts +47 -1
- package/clinkx-workflows/dist/fidelity.js +54 -1
- package/clinkx-workflows/dist/fidelity.js.map +1 -1
- package/clinkx-workflows/dist/fingerprint.d.ts +25 -7
- package/clinkx-workflows/dist/fingerprint.js +33 -10
- package/clinkx-workflows/dist/fingerprint.js.map +1 -1
- package/clinkx-workflows/dist/index.d.ts +19 -0
- package/clinkx-workflows/dist/index.js +68 -1
- package/clinkx-workflows/dist/index.js.map +1 -1
- package/clinkx-workflows/dist/loader.d.ts +9 -3
- package/clinkx-workflows/dist/loader.js +92 -21
- package/clinkx-workflows/dist/loader.js.map +1 -1
- package/clinkx-workflows/dist/logger.d.ts +1 -1
- package/clinkx-workflows/dist/logger.js +17 -3
- package/clinkx-workflows/dist/logger.js.map +1 -1
- package/clinkx-workflows/dist/monotonic-clock.d.ts +19 -0
- package/clinkx-workflows/dist/monotonic-clock.js +22 -0
- package/clinkx-workflows/dist/monotonic-clock.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/admission.d.ts +160 -0
- package/clinkx-workflows/dist/plan-review/admission.js +290 -0
- package/clinkx-workflows/dist/plan-review/admission.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/context-policy.d.ts +55 -0
- package/clinkx-workflows/dist/plan-review/context-policy.js +146 -0
- package/clinkx-workflows/dist/plan-review/context-policy.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/contract.d.ts +763 -0
- package/clinkx-workflows/dist/plan-review/contract.js +289 -0
- package/clinkx-workflows/dist/plan-review/contract.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/digest.d.ts +68 -0
- package/clinkx-workflows/dist/plan-review/digest.js +241 -0
- package/clinkx-workflows/dist/plan-review/digest.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/discovery.d.ts +45 -0
- package/clinkx-workflows/dist/plan-review/discovery.js +36 -0
- package/clinkx-workflows/dist/plan-review/discovery.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.d.ts +298 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.js +194 -0
- package/clinkx-workflows/dist/plan-review/execution-contract.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/expander.d.ts +48 -0
- package/clinkx-workflows/dist/plan-review/expander.js +243 -0
- package/clinkx-workflows/dist/plan-review/expander.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/projector.d.ts +126 -0
- package/clinkx-workflows/dist/plan-review/projector.js +437 -0
- package/clinkx-workflows/dist/plan-review/projector.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/publication.d.ts +95 -0
- package/clinkx-workflows/dist/plan-review/publication.js +182 -0
- package/clinkx-workflows/dist/plan-review/publication.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.d.ts +34 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.js +139 -0
- package/clinkx-workflows/dist/plan-review/ranking-reducer.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/resume.d.ts +38 -0
- package/clinkx-workflows/dist/plan-review/resume.js +122 -0
- package/clinkx-workflows/dist/plan-review/resume.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/rubric.d.ts +7 -0
- package/clinkx-workflows/dist/plan-review/rubric.js +24 -0
- package/clinkx-workflows/dist/plan-review/rubric.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/run-state.d.ts +398 -0
- package/clinkx-workflows/dist/plan-review/run-state.js +183 -0
- package/clinkx-workflows/dist/plan-review/run-state.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.d.ts +74 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.js +186 -0
- package/clinkx-workflows/dist/plan-review/runtime-policy.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/snapshot.d.ts +81 -0
- package/clinkx-workflows/dist/plan-review/snapshot.js +170 -0
- package/clinkx-workflows/dist/plan-review/snapshot.js.map +1 -0
- package/clinkx-workflows/dist/plan-review/stage-error.d.ts +19 -0
- package/clinkx-workflows/dist/plan-review/stage-error.js +20 -0
- package/clinkx-workflows/dist/plan-review/stage-error.js.map +1 -0
- package/clinkx-workflows/dist/post-processors/index.d.ts +20 -0
- package/clinkx-workflows/dist/post-processors/index.js +21 -0
- package/clinkx-workflows/dist/post-processors/index.js.map +1 -0
- package/clinkx-workflows/dist/prompt-budget.d.ts +7 -0
- package/clinkx-workflows/dist/prompt-budget.js +7 -1
- package/clinkx-workflows/dist/prompt-budget.js.map +1 -1
- package/clinkx-workflows/dist/reducers/index.js +2 -0
- package/clinkx-workflows/dist/reducers/index.js.map +1 -1
- package/clinkx-workflows/dist/roster-contract.d.ts +889 -0
- package/clinkx-workflows/dist/roster-contract.js +180 -0
- package/clinkx-workflows/dist/roster-contract.js.map +1 -0
- package/clinkx-workflows/dist/rosters.d.ts +14 -0
- package/clinkx-workflows/dist/rosters.js +169 -0
- package/clinkx-workflows/dist/rosters.js.map +1 -0
- package/clinkx-workflows/dist/run-health.d.ts +7 -0
- package/clinkx-workflows/dist/run-health.js +25 -0
- package/clinkx-workflows/dist/run-health.js.map +1 -0
- package/clinkx-workflows/dist/run-lock-heartbeat.d.ts +2 -0
- package/clinkx-workflows/dist/run-lock-heartbeat.js +3 -0
- package/clinkx-workflows/dist/run-lock-heartbeat.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.d.ts +34 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js +74 -0
- package/clinkx-workflows/dist/run-summary/cards/council-plan-review.js.map +1 -0
- package/clinkx-workflows/dist/run-summary/cards/index.js +5 -0
- package/clinkx-workflows/dist/run-summary/cards/index.js.map +1 -1
- package/clinkx-workflows/dist/run-summary.d.ts +13 -0
- package/clinkx-workflows/dist/run-summary.js +29 -1
- package/clinkx-workflows/dist/run-summary.js.map +1 -1
- package/clinkx-workflows/dist/schema.d.ts +140 -21
- package/clinkx-workflows/dist/schema.js +34 -1
- package/clinkx-workflows/dist/schema.js.map +1 -1
- package/clinkx-workflows/dist/seed-terms.d.ts +30 -0
- package/clinkx-workflows/dist/seed-terms.js +176 -0
- package/clinkx-workflows/dist/seed-terms.js.map +1 -0
- package/clinkx-workflows/dist/server.d.ts +5 -2
- package/clinkx-workflows/dist/server.js +84 -52
- package/clinkx-workflows/dist/server.js.map +1 -1
- package/clinkx-workflows/dist/shutdown.d.ts +29 -18
- package/clinkx-workflows/dist/shutdown.js +90 -64
- package/clinkx-workflows/dist/shutdown.js.map +1 -1
- package/clinkx-workflows/dist/state-schema.d.ts +61 -0
- package/clinkx-workflows/dist/state.d.ts +175 -3
- package/clinkx-workflows/dist/state.js +1398 -44
- package/clinkx-workflows/dist/state.js.map +1 -1
- package/clinkx-workflows/dist/stdio-watchdog.d.ts +52 -0
- package/clinkx-workflows/dist/stdio-watchdog.js +212 -0
- package/clinkx-workflows/dist/stdio-watchdog.js.map +1 -0
- package/clinkx-workflows/dist/transport.d.ts +15 -0
- package/clinkx-workflows/dist/transport.js +19 -10
- package/clinkx-workflows/dist/transport.js.map +1 -1
- package/clinkx-workflows/dist/types.d.ts +20 -2
- package/clinkx-workflows/dist/validators/council.d.ts +334 -101
- package/clinkx-workflows/dist/validators/council.js +52 -13
- package/clinkx-workflows/dist/validators/council.js.map +1 -1
- package/clinkx-workflows/dist/validators/index.d.ts +1 -0
- package/clinkx-workflows/dist/validators/index.js +3 -0
- package/clinkx-workflows/dist/validators/index.js.map +1 -1
- package/clinkx-workflows/dist/validators/plan-review.d.ts +398 -0
- package/clinkx-workflows/dist/validators/plan-review.js +316 -0
- package/clinkx-workflows/dist/validators/plan-review.js.map +1 -0
- package/clinkx-workflows/dist/workflow-receipt.js +188 -4
- package/clinkx-workflows/dist/workflow-receipt.js.map +1 -1
- package/clinkx-workflows/dist/workflow-tools.d.ts +118 -3
- package/clinkx-workflows/dist/workflow-tools.js +2110 -175
- package/clinkx-workflows/dist/workflow-tools.js.map +1 -1
- package/clinkx-workflows/dist/workspace-signature.d.ts +16 -0
- package/clinkx-workflows/dist/workspace-signature.js +388 -0
- package/clinkx-workflows/dist/workspace-signature.js.map +1 -0
- package/clinkx-workflows/templates/council/answer-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/answer-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/answer-specialist.txt +5 -1
- package/clinkx-workflows/templates/council/chairman.txt +3 -0
- package/clinkx-workflows/templates/council/code-review-chairman.txt +16 -0
- package/clinkx-workflows/templates/council/code-review-cross-reviewer.txt +7 -1
- package/clinkx-workflows/templates/council/code-review-reviewer.txt +22 -3
- package/clinkx-workflows/templates/council/debug-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/debug-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/debug-debugger.txt +5 -1
- package/clinkx-workflows/templates/council/discover-analyst.txt +5 -1
- package/clinkx-workflows/templates/council/discover-chairman.txt +3 -0
- package/clinkx-workflows/templates/council/discover-cross-reviewer.txt +3 -0
- package/clinkx-workflows/templates/council/member.txt +5 -1
- package/clinkx-workflows/templates/council/plan-review-chairman.txt +71 -0
- package/clinkx-workflows/templates/council/plan-review-cross-reviewer.txt +54 -0
- package/clinkx-workflows/templates/council/plan-review-reviewer.txt +63 -0
- package/clinkx-workflows/templates/council/reviewer.txt +3 -0
- package/clinkx-workflows/templates/plan-review/chairman.txt +48 -0
- package/clinkx-workflows/templates/plan-review/cross-review.txt +52 -0
- package/clinkx-workflows/templates/plan-review/ranking.txt +16 -0
- package/clinkx-workflows/templates/plan-review/reviewer.txt +57 -0
- package/clinkx-workflows/workflows/council-answer.yaml +100 -8
- package/clinkx-workflows/workflows/council-code-review.yaml +106 -8
- package/clinkx-workflows/workflows/council-debug.yaml +100 -8
- package/clinkx-workflows/workflows/council-default.yaml +104 -8
- package/clinkx-workflows/workflows/council-discover.yaml +100 -8
- package/clinkx-workflows/workflows/council-plan-review.yaml +253 -0
- package/clinkx-workflows/workflows/plan-review-receipts.yaml +230 -0
- package/conf/adapters/agy.json +56 -0
- package/conf/adapters/claude.json +12 -0
- package/conf/adapters/codex-app-server.json +44 -0
- package/conf/adapters/codex.json +14 -2
- package/conf/adapters/glm.json +24 -3
- package/conf/adapters/grok.json +67 -0
- package/conf/adapters/hapi/agy.json +59 -0
- package/conf/adapters/hapi/claude.json +12 -0
- package/conf/adapters/hapi/codex.json +14 -2
- package/conf/adapters/hapi/glm.json +25 -4
- package/conf/rosters/default-council.json +21 -0
- package/dist/activity-journal.d.ts +56 -0
- package/dist/activity-journal.js +706 -0
- package/dist/activity-journal.js.map +1 -0
- package/dist/allowed-roots.d.ts +41 -0
- package/dist/allowed-roots.js +125 -3
- package/dist/allowed-roots.js.map +1 -1
- package/dist/codex/activity-session-log.d.ts +14 -0
- package/dist/codex/activity-session-log.js +206 -0
- package/dist/codex/activity-session-log.js.map +1 -0
- package/dist/codex/agent-tools.d.ts +23 -0
- package/dist/codex/agent-tools.js +1306 -0
- package/dist/codex/agent-tools.js.map +1 -0
- package/dist/codex/app-server-client.d.ts +79 -0
- package/dist/codex/app-server-client.js +593 -0
- package/dist/codex/app-server-client.js.map +1 -0
- package/dist/codex/app-server-registry.d.ts +407 -0
- package/dist/codex/app-server-registry.js +1493 -0
- package/dist/codex/app-server-registry.js.map +1 -0
- package/dist/codex/client-types.d.ts +115 -0
- package/dist/codex/client-types.js +2 -0
- package/dist/codex/client-types.js.map +1 -0
- package/dist/codex/protocol.d.ts +5375 -0
- package/dist/codex/protocol.js +614 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/codex/session.d.ts +263 -0
- package/dist/codex/session.js +466 -0
- package/dist/codex/session.js.map +1 -0
- package/dist/config.d.ts +386 -23
- package/dist/config.js +184 -15
- package/dist/config.js.map +1 -1
- package/dist/context-builder/anchors.d.ts +1 -0
- package/dist/context-builder/anchors.js +3 -0
- package/dist/context-builder/anchors.js.map +1 -1
- package/dist/context-builder/bundle.d.ts +48 -2
- package/dist/context-builder/bundle.js +388 -2417
- package/dist/context-builder/bundle.js.map +1 -1
- package/dist/context-builder/codemap.d.ts +5 -0
- package/dist/context-builder/codemap.js +387 -13
- package/dist/context-builder/codemap.js.map +1 -1
- package/dist/context-builder/config.d.ts +1 -0
- package/dist/context-builder/config.js +12 -0
- package/dist/context-builder/config.js.map +1 -1
- package/dist/context-builder/context-run-catalog.d.ts +17 -0
- package/dist/context-builder/context-run-catalog.js +99 -0
- package/dist/context-builder/context-run-catalog.js.map +1 -0
- package/dist/context-builder/delegate-finalizer-effective.d.ts +33 -0
- package/dist/context-builder/delegate-finalizer-effective.js +82 -0
- package/dist/context-builder/delegate-finalizer-effective.js.map +1 -0
- package/dist/context-builder/delegate-packet.d.ts +55 -0
- package/dist/context-builder/delegate-packet.js +430 -74
- package/dist/context-builder/delegate-packet.js.map +1 -1
- package/dist/context-builder/delegate-reconstruction.d.ts +29 -0
- package/dist/context-builder/delegate-reconstruction.js +110 -41
- package/dist/context-builder/delegate-reconstruction.js.map +1 -1
- package/dist/context-builder/delegate-validation.d.ts +41 -1
- package/dist/context-builder/delegate-validation.js +237 -48
- package/dist/context-builder/delegate-validation.js.map +1 -1
- package/dist/context-builder/delegate.d.ts +11 -0
- package/dist/context-builder/delegate.js +210 -58
- package/dist/context-builder/delegate.js.map +1 -1
- package/dist/context-builder/diagnostics.d.ts +11 -2
- package/dist/context-builder/diagnostics.js +39 -6
- package/dist/context-builder/diagnostics.js.map +1 -1
- package/dist/context-builder/discovery-progress.d.ts +25 -0
- package/dist/context-builder/discovery-progress.js +64 -0
- package/dist/context-builder/discovery-progress.js.map +1 -0
- package/dist/context-builder/discovery-watchdog.d.ts +15 -0
- package/dist/context-builder/discovery-watchdog.js +39 -0
- package/dist/context-builder/discovery-watchdog.js.map +1 -0
- package/dist/context-builder/discovery.d.ts +36 -1
- package/dist/context-builder/discovery.js +584 -172
- package/dist/context-builder/discovery.js.map +1 -1
- package/dist/context-builder/external-gaps.d.ts +73 -0
- package/dist/context-builder/external-gaps.js +248 -0
- package/dist/context-builder/external-gaps.js.map +1 -0
- package/dist/context-builder/globs.d.ts +1 -0
- package/dist/context-builder/globs.js +25 -10
- package/dist/context-builder/globs.js.map +1 -1
- package/dist/context-builder/handler.d.ts +67 -2
- package/dist/context-builder/handler.js +1810 -2440
- package/dist/context-builder/handler.js.map +1 -1
- package/dist/context-builder/invocation-sidecar.d.ts +41 -0
- package/dist/context-builder/invocation-sidecar.js +189 -0
- package/dist/context-builder/invocation-sidecar.js.map +1 -0
- package/dist/context-builder/lifecycle-contract.d.ts +424 -0
- package/dist/context-builder/lifecycle-contract.js +523 -0
- package/dist/context-builder/lifecycle-contract.js.map +1 -0
- package/dist/context-builder/lifecycle-journal.d.ts +62 -0
- package/dist/context-builder/lifecycle-journal.js +88 -0
- package/dist/context-builder/lifecycle-journal.js.map +1 -0
- package/dist/context-builder/lifecycle-observer.d.ts +102 -0
- package/dist/context-builder/lifecycle-observer.js +258 -0
- package/dist/context-builder/lifecycle-observer.js.map +1 -0
- package/dist/context-builder/lifecycle-reconcile.d.ts +32 -0
- package/dist/context-builder/lifecycle-reconcile.js +191 -0
- package/dist/context-builder/lifecycle-reconcile.js.map +1 -0
- package/dist/context-builder/lifecycle-recovery.d.ts +2 -0
- package/dist/context-builder/lifecycle-recovery.js +14 -0
- package/dist/context-builder/lifecycle-recovery.js.map +1 -0
- package/dist/context-builder/mutation-lock.d.ts +32 -0
- package/dist/context-builder/mutation-lock.js +321 -0
- package/dist/context-builder/mutation-lock.js.map +1 -0
- package/dist/context-builder/path-signals.js +9 -5
- package/dist/context-builder/path-signals.js.map +1 -1
- package/dist/context-builder/progress.d.ts +3 -1
- package/dist/context-builder/progress.js +23 -0
- package/dist/context-builder/progress.js.map +1 -1
- package/dist/context-builder/receipt.d.ts +6 -4
- package/dist/context-builder/receipt.js +12 -1
- package/dist/context-builder/receipt.js.map +1 -1
- package/dist/context-builder/refine-handler.d.ts +14 -0
- package/dist/context-builder/refine-handler.js +790 -0
- package/dist/context-builder/refine-handler.js.map +1 -0
- package/dist/context-builder/review-coverage.d.ts +116 -0
- package/dist/context-builder/review-coverage.js +544 -0
- package/dist/context-builder/review-coverage.js.map +1 -0
- package/dist/context-builder/schema.d.ts +315 -2
- package/dist/context-builder/schema.js +133 -7
- package/dist/context-builder/schema.js.map +1 -1
- package/dist/context-builder/secrets.d.ts +59 -0
- package/dist/context-builder/secrets.js +2255 -44
- package/dist/context-builder/secrets.js.map +1 -1
- package/dist/context-builder/selection.d.ts +7 -3
- package/dist/context-builder/selection.js +200 -738
- package/dist/context-builder/selection.js.map +1 -1
- package/dist/context-builder/session-log-identity.d.ts +1 -0
- package/dist/context-builder/session-log-identity.js +66 -0
- package/dist/context-builder/session-log-identity.js.map +1 -0
- package/dist/context-builder/slice-range-math.d.ts +16 -0
- package/dist/context-builder/slice-range-math.js +137 -0
- package/dist/context-builder/slice-range-math.js.map +1 -0
- package/dist/context-builder/status.d.ts +23 -2
- package/dist/context-builder/status.js +60 -4
- package/dist/context-builder/status.js.map +1 -1
- package/dist/context-builder/storage.d.ts +59 -2
- package/dist/context-builder/storage.js +500 -168
- package/dist/context-builder/storage.js.map +1 -1
- package/dist/context-builder/token-estimate.d.ts +13 -2
- package/dist/context-builder/token-estimate.js +77 -4
- package/dist/context-builder/token-estimate.js.map +1 -1
- package/dist/context-builder/versions.d.ts +44 -0
- package/dist/context-builder/versions.js +45 -0
- package/dist/context-builder/versions.js.map +1 -0
- package/dist/context-builder/workflow-runner.d.ts +75 -0
- package/dist/context-builder/workflow-runner.js +1241 -0
- package/dist/context-builder/workflow-runner.js.map +1 -0
- package/dist/context-builder/write-watchdog.d.ts +14 -0
- package/dist/context-builder/write-watchdog.js +38 -0
- package/dist/context-builder/write-watchdog.js.map +1 -0
- package/dist/dashboard-adapter-catalog.d.ts +9 -0
- package/dist/dashboard-adapter-catalog.js +53 -0
- package/dist/dashboard-adapter-catalog.js.map +1 -0
- package/dist/dashboard-cli.d.ts +81 -0
- package/dist/dashboard-cli.js +623 -0
- package/dist/dashboard-cli.js.map +1 -0
- package/dist/dashboard-companion/client.d.ts +37 -0
- package/dist/dashboard-companion/client.js +229 -0
- package/dist/dashboard-companion/client.js.map +1 -0
- package/dist/dashboard-companion/config.d.ts +8 -0
- package/dist/dashboard-companion/config.js +52 -0
- package/dist/dashboard-companion/config.js.map +1 -0
- package/dist/dashboard-companion/http-proxy.d.ts +10 -0
- package/dist/dashboard-companion/http-proxy.js +234 -0
- package/dist/dashboard-companion/http-proxy.js.map +1 -0
- package/dist/dashboard-companion/protocol.d.ts +80 -0
- package/dist/dashboard-companion/protocol.js +458 -0
- package/dist/dashboard-companion/protocol.js.map +1 -0
- package/dist/dashboard-companion/standalone-assets.d.ts +12 -0
- package/dist/dashboard-companion/standalone-assets.js +72 -0
- package/dist/dashboard-companion/standalone-assets.js.map +1 -0
- package/dist/dashboard-companion/standalone-entry.d.ts +1 -0
- package/dist/dashboard-companion/standalone-entry.js +17 -0
- package/dist/dashboard-companion/standalone-entry.js.map +1 -0
- package/dist/dashboard-gateway/config.d.ts +9 -0
- package/dist/dashboard-gateway/config.js +44 -0
- package/dist/dashboard-gateway/config.js.map +1 -0
- package/dist/dashboard-gateway/index.d.ts +12 -0
- package/dist/dashboard-gateway/index.js +786 -0
- package/dist/dashboard-gateway/index.js.map +1 -0
- package/dist/dashboard-gateway/main.d.ts +1 -0
- package/dist/dashboard-gateway/main.js +25 -0
- package/dist/dashboard-gateway/main.js.map +1 -0
- package/dist/dashboard-pairing-token.d.ts +1 -0
- package/dist/dashboard-pairing-token.js +5 -0
- package/dist/dashboard-pairing-token.js.map +1 -0
- package/dist/elaris/client.d.ts +73 -0
- package/dist/elaris/client.js +825 -0
- package/dist/elaris/client.js.map +1 -0
- package/dist/elaris/config.d.ts +18 -0
- package/dist/elaris/config.js +75 -0
- package/dist/elaris/config.js.map +1 -0
- package/dist/elaris/errors.d.ts +41 -0
- package/dist/elaris/errors.js +205 -0
- package/dist/elaris/errors.js.map +1 -0
- package/dist/elaris/github-repo.d.ts +18 -0
- package/dist/elaris/github-repo.js +89 -0
- package/dist/elaris/github-repo.js.map +1 -0
- package/dist/elaris/index.d.ts +10 -0
- package/dist/elaris/index.js +11 -0
- package/dist/elaris/index.js.map +1 -0
- package/dist/elaris/redact.d.ts +89 -0
- package/dist/elaris/redact.js +423 -0
- package/dist/elaris/redact.js.map +1 -0
- package/dist/elaris/run-id.d.ts +5 -0
- package/dist/elaris/run-id.js +20 -0
- package/dist/elaris/run-id.js.map +1 -0
- package/dist/elaris/status.d.ts +45 -0
- package/dist/elaris/status.js +122 -0
- package/dist/elaris/status.js.map +1 -0
- package/dist/elaris/tool-schemas.d.ts +314 -0
- package/dist/elaris/tool-schemas.js +208 -0
- package/dist/elaris/tool-schemas.js.map +1 -0
- package/dist/elaris/tools.d.ts +73 -0
- package/dist/elaris/tools.js +1065 -0
- package/dist/elaris/tools.js.map +1 -0
- package/dist/elaris/types.d.ts +1541 -0
- package/dist/elaris/types.js +252 -0
- package/dist/elaris/types.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +19 -0
- package/dist/errors.js.map +1 -1
- package/dist/handler.d.ts +4 -0
- package/dist/handler.js +15 -2
- package/dist/handler.js.map +1 -1
- package/dist/index.d.ts +20 -2
- package/dist/index.js +154 -6
- package/dist/index.js.map +1 -1
- package/dist/local-clink-backend.d.ts +3 -0
- package/dist/local-clink-backend.js +4 -0
- package/dist/local-clink-backend.js.map +1 -1
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +18 -3
- package/dist/logger.js.map +1 -1
- package/dist/package-info.d.ts +2 -0
- package/dist/package-info.js +18 -0
- package/dist/package-info.js.map +1 -0
- package/dist/parsers/agy-stream-json.d.ts +14 -0
- package/dist/parsers/agy-stream-json.js +172 -0
- package/dist/parsers/agy-stream-json.js.map +1 -0
- package/dist/parsers/codex-jsonl.js +48 -3
- package/dist/parsers/codex-jsonl.js.map +1 -1
- package/dist/parsers/extract.d.ts +10 -1
- package/dist/parsers/extract.js +16 -4
- package/dist/parsers/extract.js.map +1 -1
- package/dist/parsers/observed-invocation.d.ts +20 -0
- package/dist/parsers/observed-invocation.js +42 -0
- package/dist/parsers/observed-invocation.js.map +1 -0
- package/dist/pipeline.d.ts +17 -0
- package/dist/pipeline.js +181 -10
- package/dist/pipeline.js.map +1 -1
- package/dist/progress-report/lock.d.ts +74 -0
- package/dist/progress-report/lock.js +343 -0
- package/dist/progress-report/lock.js.map +1 -0
- package/dist/progress-report/schema.d.ts +808 -0
- package/dist/progress-report/schema.js +119 -0
- package/dist/progress-report/schema.js.map +1 -0
- package/dist/progress-report/store.d.ts +66 -0
- package/dist/progress-report/store.js +766 -0
- package/dist/progress-report/store.js.map +1 -0
- package/dist/progress-report/tools.d.ts +20 -0
- package/dist/progress-report/tools.js +141 -0
- package/dist/progress-report/tools.js.map +1 -0
- package/dist/prompt-mode.d.ts +1 -1
- package/dist/prompt-mode.js +7 -2
- package/dist/prompt-mode.js.map +1 -1
- package/dist/run-dir.d.ts +24 -0
- package/dist/run-dir.js +201 -2
- package/dist/run-dir.js.map +1 -1
- package/dist/runner.d.ts +20 -0
- package/dist/runner.js +192 -16
- package/dist/runner.js.map +1 -1
- package/dist/safety.d.ts +54 -4
- package/dist/safety.js +186 -5
- package/dist/safety.js.map +1 -1
- package/dist/schema.d.ts +15 -7
- package/dist/schema.js +20 -0
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +7 -1
- package/dist/server.js +451 -23
- package/dist/server.js.map +1 -1
- package/dist/session-log-stream.d.ts +18 -0
- package/dist/session-log-stream.js +566 -0
- package/dist/session-log-stream.js.map +1 -0
- package/dist/stdio-activity.d.ts +28 -0
- package/dist/stdio-activity.js +72 -0
- package/dist/stdio-activity.js.map +1 -0
- package/package.json +47 -5
- package/conf/adapters/gemini.json +0 -44
- package/conf/adapters/hapi/gemini.json +0 -47
- package/dist/parsers/gemini-json.d.ts +0 -2
- package/dist/parsers/gemini-json.js +0 -72
- package/dist/parsers/gemini-json.js.map +0 -1
|
@@ -5,44 +5,252 @@
|
|
|
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 five 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
|
+
* - workflow_run_status: read workflow run progress/result
|
|
13
|
+
* - cancel_workflow: cancel a live or orphaned workflow run
|
|
12
14
|
*
|
|
13
15
|
* The caller provides a `createBackend` factory that determines how clink
|
|
14
16
|
* calls are dispatched — either via child process or in-process.
|
|
15
17
|
*/
|
|
16
18
|
import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
21
|
+
import { computeRunHealth } from "./run-health.js";
|
|
17
22
|
import { loadWorkflow, listWorkflowCatalog } from "./loader.js";
|
|
18
|
-
import { WorkflowEngine } from "./engine.js";
|
|
19
|
-
import { WorkflowStateStore } from "./state.js";
|
|
23
|
+
import { defaultWorkflowAllowedRoots, WorkflowEngine } from "./engine.js";
|
|
24
|
+
import { readRunStateForStatus, reapRunCancelled, WorkflowStateStore } from "./state.js";
|
|
25
|
+
import { isTerminalContextBuilderLifecycleStatus, parseContextBuilderLifecycleStateV1, } from "./context-builder-lifecycle-contract.js";
|
|
20
26
|
import { validateRunId } from "./run-id.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
27
|
+
import { validateDynamicPaths } from "./path-validation.js";
|
|
28
|
+
import { getMaxPromptBytes, getRunStateDir, getTemplatesPath, getWorkflowsContextDefault, getWorkflowsContextDefaultCurator, getWorkflowsMaxConcurrentAsync, resolveWorkspaceRoot, } from "./config.js";
|
|
29
|
+
import { WorkflowCancellationError, WorkflowValidationError, } from "./errors.js";
|
|
23
30
|
import { shutdownCoordinator } from "./shutdown.js";
|
|
24
|
-
import { basename } from "node:path";
|
|
25
31
|
import { logger } from "./logger.js";
|
|
26
32
|
import { createValidatorRegistry } from "./validators/index.js";
|
|
27
33
|
import { deriveDecisiveFailure } from "./run-summary-derived.js";
|
|
28
34
|
import { buildRunSummaryOrUndefined, buildFailureRunSummary } from "./run-summary.js";
|
|
29
35
|
import { extractCard } from "./run-summary/cards/index.js";
|
|
30
36
|
import { renderWorkflowReceipt } from "./workflow-receipt.js";
|
|
31
|
-
import { supportsRuntimeMembers, COUNCIL_RUNTIME_MEMBER_WORKFLOWS, expandCouncilComposition } from "./council-expander.js";
|
|
37
|
+
import { supportsRuntimeMembers, COUNCIL_RUNTIME_MEMBER_WORKFLOWS, COUNCIL_STAGE_COUNT, expandCouncilComposition } from "./council-expander.js";
|
|
38
|
+
import { resolveWorkflowContextMode, WorkflowContextModeResolverError, } from "./context-mode-resolver.js";
|
|
39
|
+
import { resolveEffectiveChairmanCall } from "./chairman-knobs.js";
|
|
40
|
+
import { disabledFinalizerOverrideRejection, effectiveFinalizerEnabled, } from "./delegate-finalizer-policy.js";
|
|
41
|
+
import { delegateFinalizerMinScoreExplicit, WorkflowDelegateValidationError, } from "./delegate-validate.js";
|
|
42
|
+
import { contextSearchTermsValidationError, contextSeedSourceValidationError, resolveSeedSource, } from "./seed-terms.js";
|
|
43
|
+
import { LEGACY_PLAN_REVIEW_RECEIPTS_WORKFLOW_NAME, PLAN_REVIEW_RECEIPTS_WORKFLOW, admitCouncilPlanReview, assertPlanReviewPromptBudget, isPlanReviewRunState, } from "./plan-review/admission.js";
|
|
44
|
+
import { PLAN_REVIEW_V1_CLI_NAMES } from "./plan-review/contract.js";
|
|
45
|
+
import { buildCouncilPlanReviewDiscovery } from "./plan-review/discovery.js";
|
|
46
|
+
import { expandCouncilPlanReview } from "./plan-review/expander.js";
|
|
47
|
+
import { buildPlanReviewContextPolicy, findRejectedPlanContextAnchors, } from "./plan-review/context-policy.js";
|
|
48
|
+
import { PLAN_SNAPSHOT_RELPATH, persistCanonicalPlanSnapshot, readCanonicalPlanSnapshot, writeExecutionContractSnapshot, } from "./plan-review/snapshot.js";
|
|
49
|
+
import { deriveSeatMappings, reconstructPlanReviewCallBindings, } from "./plan-review/run-state.js";
|
|
50
|
+
import { restorePlanReviewRun, } from "./plan-review/resume.js";
|
|
51
|
+
import { PlanReviewStageError } from "./plan-review/stage-error.js";
|
|
52
|
+
import { CouncilPlanReviewErrorSchema, createPlanReviewPublicationError, inspectPlanReviewPublication, } from "./plan-review/publication.js";
|
|
53
|
+
import { validatePlanReviewCallBindings, validatePlanReviewPreparedInputPolicy, } from "./plan-review/runtime-policy.js";
|
|
54
|
+
import { ContainedFilePolicyError, readContainedRelativeFile, } from "./contained-file.js";
|
|
55
|
+
import { createDashboardRunReader } from "./dashboard/read-model.js";
|
|
56
|
+
import { ROSTER_NAME_PATTERN, ResolvedRosterSchema, } from "./roster-contract.js";
|
|
57
|
+
async function inspectLegacyWorkflowSessionLogGeneration(runDir, runId) {
|
|
58
|
+
try {
|
|
59
|
+
const result = await createDashboardRunReader().inspectGeneration?.(dirname(runDir), runId);
|
|
60
|
+
if (result?.kind === "present")
|
|
61
|
+
return result.runGeneration;
|
|
62
|
+
logger.debug({
|
|
63
|
+
run_id: runId,
|
|
64
|
+
reason: result === undefined ? "unsupported" : result.kind,
|
|
65
|
+
}, "workflow session-log generation unavailable; capture disabled");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
logger.debug({ run_id: runId, reason: "inspection_failed" }, "workflow session-log generation unavailable; capture disabled");
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Allowed roots for reading the caller's `plan_file`.
|
|
74
|
+
*
|
|
75
|
+
* Deliberately the workspace root ONLY. The state/artifact dirs are
|
|
76
|
+
* artifact-only roots — they are where runtime-owned output goes, never a
|
|
77
|
+
* source of reviewable plan text.
|
|
78
|
+
*/
|
|
79
|
+
function planFileAllowedRoots() {
|
|
80
|
+
return [resolveWorkspaceRoot()];
|
|
81
|
+
}
|
|
82
|
+
function createPreflightOnlyBackend(advertisedCliNames) {
|
|
83
|
+
return {
|
|
84
|
+
cliNames: [...(advertisedCliNames ?? [])],
|
|
85
|
+
roleNames: [],
|
|
86
|
+
isTerminated: false,
|
|
87
|
+
hasChildDied: false,
|
|
88
|
+
connect: async () => { },
|
|
89
|
+
callTool: async () => {
|
|
90
|
+
throw new Error("preflight-only backend cannot dispatch provider calls");
|
|
91
|
+
},
|
|
92
|
+
terminate: async () => { },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
32
95
|
// ---------------------------------------------------------------------------
|
|
33
96
|
// Admission control
|
|
34
97
|
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
98
|
+
const activeRuns = new Map();
|
|
99
|
+
/**
|
|
100
|
+
* Number of workflow tool handlers currently executing in the standalone
|
|
101
|
+
* registration path. This includes non-run tools such as status/list/cancel;
|
|
102
|
+
* it is kept separate from activeRuns because an admitted run can outlive its
|
|
103
|
+
* request handler (async mode), while a handler can be busy before admission.
|
|
104
|
+
*/
|
|
105
|
+
let activeWorkflowHandlers = 0;
|
|
106
|
+
let workflowActivityObserver;
|
|
107
|
+
/** Internal standalone-lifecycle seam; not part of the public package API. */
|
|
108
|
+
export function getActiveWorkflowHandlerCount() {
|
|
109
|
+
return activeWorkflowHandlers;
|
|
110
|
+
}
|
|
111
|
+
/** Internal shared lifecycle seam used by the root server's compiled import. */
|
|
112
|
+
export function getActiveWorkflowRunCount() {
|
|
113
|
+
return activeRuns.size;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Install the standalone server's activity sink. The observer is optional and
|
|
117
|
+
* process-local; library callers leave it unset, so importing this module or
|
|
118
|
+
* using the in-process registration path has no timer/lifecycle side effects.
|
|
119
|
+
*/
|
|
120
|
+
export function setWorkflowActivityObserver(observer) {
|
|
121
|
+
workflowActivityObserver = observer;
|
|
122
|
+
}
|
|
123
|
+
function notifyWorkflowActivity() {
|
|
124
|
+
try {
|
|
125
|
+
workflowActivityObserver?.();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
logger.warn({ err: error instanceof Error ? error.message : String(error) }, "workflow activity observer failed");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* In-process registry of AbortControllers for live async runs, keyed by run_id.
|
|
133
|
+
* cancel_workflow aborts the controller to drive the run's own cancellation
|
|
134
|
+
* unwind. Registered at async admission (foreground) and deleted in the detached
|
|
135
|
+
* run's finally (a delete on an absent key is a harmless no-op, so the
|
|
136
|
+
* synchronous run path is unaffected).
|
|
137
|
+
*/
|
|
138
|
+
const cancelRegistry = new Map();
|
|
139
|
+
export function resolveWorkflowAsyncAdmissionCap(options) {
|
|
140
|
+
const injected = options?.asyncAdmissionCap;
|
|
141
|
+
if (typeof injected === "number" &&
|
|
142
|
+
Number.isSafeInteger(injected) &&
|
|
143
|
+
injected >= 1) {
|
|
144
|
+
return injected;
|
|
145
|
+
}
|
|
146
|
+
return getWorkflowsMaxConcurrentAsync();
|
|
147
|
+
}
|
|
148
|
+
function acquireMutex(runId, options) {
|
|
149
|
+
if (shutdownCoordinator.isShuttingDown()) {
|
|
150
|
+
throw new McpError(ErrorCode.InvalidRequest, "workflow server is shutting down");
|
|
151
|
+
}
|
|
152
|
+
if (activeRuns.has(runId)) {
|
|
153
|
+
throw new McpError(ErrorCode.InvalidParams, `run_id ${runId} already running`);
|
|
154
|
+
}
|
|
155
|
+
const maxConcurrent = resolveWorkflowAsyncAdmissionCap(options);
|
|
156
|
+
if (activeRuns.size >= maxConcurrent) {
|
|
157
|
+
throw new McpError(ErrorCode.InvalidRequest, `max ${maxConcurrent} concurrent workflow(s); retry later`);
|
|
158
|
+
}
|
|
159
|
+
activeRuns.set(runId, { startedAt: new Date().toISOString() });
|
|
160
|
+
}
|
|
161
|
+
function releaseMutex(runId) {
|
|
162
|
+
activeRuns.delete(runId);
|
|
163
|
+
notifyWorkflowActivity();
|
|
164
|
+
}
|
|
165
|
+
/** Own setup before its first await, and keep that ownership through cleanup. */
|
|
166
|
+
function acquireWorkflowRun(runId, options, requestSignal) {
|
|
167
|
+
acquireMutex(runId, options);
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
const signal = requestSignal === undefined
|
|
170
|
+
? controller.signal
|
|
171
|
+
: AbortSignal.any([requestSignal, controller.signal]);
|
|
172
|
+
let backend;
|
|
173
|
+
let termination;
|
|
174
|
+
const terminate = () => {
|
|
175
|
+
if (backend === undefined || backend.isTerminated)
|
|
176
|
+
return termination ?? Promise.resolve();
|
|
177
|
+
termination ??= Promise.resolve().then(() => backend.terminate());
|
|
178
|
+
return termination;
|
|
179
|
+
};
|
|
180
|
+
let complete;
|
|
181
|
+
const completion = new Promise((resolve) => { complete = resolve; });
|
|
182
|
+
let handle;
|
|
183
|
+
try {
|
|
184
|
+
// The run owns persistence and close; shutdown only interrupts the lazy
|
|
185
|
+
// backend and waits until the run's own finally releases all resources.
|
|
186
|
+
handle = shutdownCoordinator.register({ terminate }, controller, async () => { }, completion);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
releaseMutex(runId);
|
|
190
|
+
complete();
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
if (requestSignal === undefined)
|
|
194
|
+
cancelRegistry.set(runId, controller);
|
|
195
|
+
return {
|
|
196
|
+
signal,
|
|
197
|
+
throwIfCancelled: () => {
|
|
198
|
+
if (signal.aborted)
|
|
199
|
+
throw new WorkflowCancellationError();
|
|
200
|
+
},
|
|
201
|
+
setBackend: (value) => { backend = value; },
|
|
202
|
+
finish: async (store) => {
|
|
203
|
+
try {
|
|
204
|
+
try {
|
|
205
|
+
await terminate();
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
logger.warn({ err }, "backend.terminate() failed during cleanup");
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
await store?.close();
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
logger.warn({ err }, "store.close() failed during cleanup");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
cancelRegistry.delete(runId);
|
|
219
|
+
releaseMutex(runId);
|
|
220
|
+
shutdownCoordinator.unregister(handle);
|
|
221
|
+
complete();
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function markWorkflowCancelled(store) {
|
|
227
|
+
try {
|
|
228
|
+
await store?.markCancelled();
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
// Preserve cancellation/no-response even if best-effort persistence fails.
|
|
232
|
+
logger.warn({ err }, "markCancelled during cancellation handling failed");
|
|
41
233
|
}
|
|
42
|
-
activeMutex = { runId, startedAt: new Date().toISOString() };
|
|
43
234
|
}
|
|
44
|
-
|
|
45
|
-
|
|
235
|
+
// Keep in sync with schema.ts model regex and ModelSchema (delegate-validate.ts).
|
|
236
|
+
const MODEL_OVERRIDE_PATTERN = /^[^\s\u0000-][^\s\u0000]*$/;
|
|
237
|
+
// Keep in sync with schema.ts effort regex and EffortSchema (delegate-validate.ts).
|
|
238
|
+
const EFFORT_OVERRIDE_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
239
|
+
function parseInvocationOverride(raw, label, maxLength, pattern) {
|
|
240
|
+
if (typeof raw !== "string") {
|
|
241
|
+
throw new McpError(ErrorCode.InvalidParams, `${label} must be a string`);
|
|
242
|
+
}
|
|
243
|
+
const value = raw.trim();
|
|
244
|
+
if (value.length === 0) {
|
|
245
|
+
throw new McpError(ErrorCode.InvalidParams, `${label} must not be empty`);
|
|
246
|
+
}
|
|
247
|
+
if (value.length > maxLength) {
|
|
248
|
+
throw new McpError(ErrorCode.InvalidParams, `${label} must be at most ${maxLength} characters`);
|
|
249
|
+
}
|
|
250
|
+
if (!pattern.test(value)) {
|
|
251
|
+
throw new McpError(ErrorCode.InvalidParams, `${label} has an invalid format`);
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
46
254
|
}
|
|
47
255
|
export function parseMembersOverride(raw, knownCliNames) {
|
|
48
256
|
if (raw == null)
|
|
@@ -63,7 +271,7 @@ export function parseMembersOverride(raw, knownCliNames) {
|
|
|
63
271
|
throw new McpError(ErrorCode.InvalidParams, `members[${i}] must be a non-null object`);
|
|
64
272
|
}
|
|
65
273
|
const keys = Object.keys(item);
|
|
66
|
-
const allowedKeys = new Set(["cli_name", "stance"]);
|
|
274
|
+
const allowedKeys = new Set(["cli_name", "stance", "model", "effort"]);
|
|
67
275
|
for (const key of keys) {
|
|
68
276
|
if (!allowedKeys.has(key)) {
|
|
69
277
|
throw new McpError(ErrorCode.InvalidParams, `members[${i}] has unexpected key "${key}"`);
|
|
@@ -102,6 +310,12 @@ export function parseMembersOverride(raw, knownCliNames) {
|
|
|
102
310
|
else if (i >= 3) {
|
|
103
311
|
throw new McpError(ErrorCode.InvalidParams, "stance is required for members at index 3 and above");
|
|
104
312
|
}
|
|
313
|
+
if (obj["model"] !== undefined) {
|
|
314
|
+
entry.model = parseInvocationOverride(obj["model"], `members[${i}].model`, 128, MODEL_OVERRIDE_PATTERN);
|
|
315
|
+
}
|
|
316
|
+
if (obj["effort"] !== undefined) {
|
|
317
|
+
entry.effort = parseInvocationOverride(obj["effort"], `members[${i}].effort`, 32, EFFORT_OVERRIDE_PATTERN);
|
|
318
|
+
}
|
|
105
319
|
result.push(entry);
|
|
106
320
|
}
|
|
107
321
|
return result;
|
|
@@ -125,6 +339,131 @@ export function parseChairmanOverride(raw, knownCliNames) {
|
|
|
125
339
|
}
|
|
126
340
|
return trimmed;
|
|
127
341
|
}
|
|
342
|
+
function parseChairmanModelOverride(raw) {
|
|
343
|
+
if (raw === undefined)
|
|
344
|
+
return undefined;
|
|
345
|
+
return parseInvocationOverride(raw, "chairman_model", 128, MODEL_OVERRIDE_PATTERN);
|
|
346
|
+
}
|
|
347
|
+
function parseChairmanEffortOverride(raw) {
|
|
348
|
+
if (raw === undefined)
|
|
349
|
+
return undefined;
|
|
350
|
+
return parseInvocationOverride(raw, "chairman_effort", 32, EFFORT_OVERRIDE_PATTERN);
|
|
351
|
+
}
|
|
352
|
+
function invocationOverrideKnobs(value) {
|
|
353
|
+
if (value.model === undefined && value.effort === undefined) {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
...(value.model === undefined ? {} : { model: value.model }),
|
|
358
|
+
...(value.effort === undefined ? {} : { effort: value.effort }),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function validateInvocationOverridesAtAdmission(workflow, definition, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, validate, callerDelegateRaw) {
|
|
362
|
+
// Structural E5: independent of the optional adapter-capability hook so the
|
|
363
|
+
// standalone server rejects a disabled-finalizer override too. No-op when
|
|
364
|
+
// the policy has no enabled delegate (including the no-runner `off` default).
|
|
365
|
+
const admissionDelegate = initialContextPolicy.effectiveDelegate;
|
|
366
|
+
if (admissionDelegate?.enabled === true) {
|
|
367
|
+
const rejection = disabledFinalizerOverrideRejection(admissionDelegate, resolvedVariables, delegateFinalizerMinScoreExplicit(callerDelegateRaw));
|
|
368
|
+
if (rejection !== undefined) {
|
|
369
|
+
throw new McpError(ErrorCode.InvalidParams, rejection);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (validate == null)
|
|
373
|
+
return;
|
|
374
|
+
const councilWorkflow = COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow);
|
|
375
|
+
if (councilWorkflow && definition.stages.length !== COUNCIL_STAGE_COUNT) {
|
|
376
|
+
throw new McpError(ErrorCode.InvalidParams, `Council workflow "${workflow}" expects ${COUNCIL_STAGE_COUNT} stages, found ${definition.stages.length}`);
|
|
377
|
+
}
|
|
378
|
+
const staticChairman = councilWorkflow
|
|
379
|
+
? definition.stages[3]?.calls[0]
|
|
380
|
+
: undefined;
|
|
381
|
+
for (const [stageIndex, stage] of definition.stages.entries()) {
|
|
382
|
+
for (const call of stage.calls) {
|
|
383
|
+
if (call === staticChairman) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (parsedMembers !== undefined && stageIndex < 2) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const knobs = invocationOverrideKnobs(call);
|
|
390
|
+
if (knobs !== undefined) {
|
|
391
|
+
validate(call.cli_name, knobs);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (parsedMembers !== undefined) {
|
|
396
|
+
for (const member of parsedMembers) {
|
|
397
|
+
const knobs = invocationOverrideKnobs(member);
|
|
398
|
+
if (knobs !== undefined) {
|
|
399
|
+
validate(member.cli_name, knobs);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (councilWorkflow && staticChairman !== undefined) {
|
|
404
|
+
const effectiveChairman = resolveEffectiveChairmanCall({
|
|
405
|
+
staticCall: staticChairman,
|
|
406
|
+
chairmanCliName: parsedChairman,
|
|
407
|
+
chairmanModel: parsedChairmanModel,
|
|
408
|
+
chairmanEffort: parsedChairmanEffort,
|
|
409
|
+
});
|
|
410
|
+
const knobs = invocationOverrideKnobs(effectiveChairman);
|
|
411
|
+
if (knobs !== undefined) {
|
|
412
|
+
validate(effectiveChairman.cli_name, knobs);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const delegate = initialContextPolicy.effectiveDelegate;
|
|
416
|
+
if (delegate?.enabled === true) {
|
|
417
|
+
const curatorKnobs = invocationOverrideKnobs(delegate.curator);
|
|
418
|
+
if (curatorKnobs !== undefined) {
|
|
419
|
+
validate(delegate.curator.cli_name, curatorKnobs);
|
|
420
|
+
}
|
|
421
|
+
if (effectiveFinalizerEnabled(delegate, resolvedVariables)) {
|
|
422
|
+
const finalizerKnobs = invocationOverrideKnobs(delegate.finalizer);
|
|
423
|
+
if (finalizerKnobs !== undefined) {
|
|
424
|
+
validate(delegate.finalizer.cli_name, finalizerKnobs);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Point a generic-council caller at the input shape the workflow actually takes.
|
|
431
|
+
*
|
|
432
|
+
* Appended by the boundary decorator wrapping the whole pre-mutex admission
|
|
433
|
+
* block, so it rides on ANY `InvalidParams` rejection of a generic-shaped
|
|
434
|
+
* `plan-review-receipts` request — not only the `members`/`chairman` roster
|
|
435
|
+
* gates. A generic `user_request`-shaped caller can trip several independent
|
|
436
|
+
* pre-state checks (the roster-override parsers themselves, the two roster
|
|
437
|
+
* gates, unknown `user_request`/`cb_delegate` variables, context-mode and root
|
|
438
|
+
* validation, the conditional `cb_task` seed, task-secret validation, and the
|
|
439
|
+
* missing typed `plan_review`); whichever fires first now names the
|
|
440
|
+
* destination, instead of the caller re-submitting and being told something
|
|
441
|
+
* different each round trip.
|
|
442
|
+
*
|
|
443
|
+
* The decorator is gated on a RAW-`args` shape predicate, not on parsed
|
|
444
|
+
* values, specifically because a parser throw means no parsed value exists
|
|
445
|
+
* yet: `parseMembersOverride` rejects `[]` and unknown `cli_name`s before
|
|
446
|
+
* `parsedMembers` is ever bound, and that is exactly a case the hint must
|
|
447
|
+
* cover.
|
|
448
|
+
*
|
|
449
|
+
* Deliberately a HINT appended to the existing message, not a reordered check.
|
|
450
|
+
* Hoisting the `plan_review` requirement above the roster gate would mask
|
|
451
|
+
* malformed roster overrides behind a missing-contract error, and split the
|
|
452
|
+
* requirement from its authoritative mirror in `admitCouncilPlanReview`. The
|
|
453
|
+
* decorator changes no ordering, so that reasoning still holds.
|
|
454
|
+
*/
|
|
455
|
+
function planReviewShapeHint() {
|
|
456
|
+
return (`. ${PLAN_REVIEW_RECEIPTS_WORKFLOW} takes a typed "plan_review" contract instead:` +
|
|
457
|
+
` the roster is plan_review.seats[] and the chairman is plan_review.chairman_seat` +
|
|
458
|
+
` (a seat_id, not an adapter name). It declares neither a "user_request" nor a` +
|
|
459
|
+
` "cb_delegate" variable, and its "cb_task" seed is conditionally required:` +
|
|
460
|
+
` trimmed-nonblank whenever context preflight is available (the default` +
|
|
461
|
+
` deployment), and genuinely optional only on a server where preflight is` +
|
|
462
|
+
` unavailable. It is driven by the plan-review-loop skill, not a generic` +
|
|
463
|
+
` council template. For a generic members/chairman/user_request plan review` +
|
|
464
|
+
` without the typed contract, use the "council-plan-review" workflow; the` +
|
|
465
|
+
` typed plan_review contract belongs only to "plan-review-receipts".`);
|
|
466
|
+
}
|
|
128
467
|
// ---------------------------------------------------------------------------
|
|
129
468
|
// Variable validation (6-6)
|
|
130
469
|
// ---------------------------------------------------------------------------
|
|
@@ -136,7 +475,8 @@ function formatExpectedVariables(schema) {
|
|
|
136
475
|
}
|
|
137
476
|
return parts.length > 0 ? `Expected variables: ${parts.join(", ")}` : "";
|
|
138
477
|
}
|
|
139
|
-
|
|
478
|
+
/** @internal Shared with the compiled producer-fixture admission check. */
|
|
479
|
+
export function validateVariables(supplied, schema) {
|
|
140
480
|
const resolved = {};
|
|
141
481
|
const errors = [];
|
|
142
482
|
for (const [name, def] of Object.entries(schema)) {
|
|
@@ -174,6 +514,153 @@ function validateVariables(supplied, schema) {
|
|
|
174
514
|
}
|
|
175
515
|
return resolved;
|
|
176
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Reject caller-supplied dynamic path variables that fall outside the workflow
|
|
519
|
+
* engine's allowed roots, at ADMISSION — before the concurrency mutex, before
|
|
520
|
+
* any run state exists, and before context preflight spends a delegate
|
|
521
|
+
* round-trip. The engine already rejects these paths, but only at
|
|
522
|
+
* `validateInputPathsExist()`, which runs AFTER preflight: by then the caller
|
|
523
|
+
* has paid for a curator/finalizer subprocess and is left with a `failed` run
|
|
524
|
+
* directory to inspect and prune.
|
|
525
|
+
*
|
|
526
|
+
* This is a pre-check, NOT a replacement. The engine's post-preflight check
|
|
527
|
+
* must stay: it is the only thing that covers preflight-produced bundle copies
|
|
528
|
+
* appended to `context_bundle_files`, which do not exist yet at admission.
|
|
529
|
+
*
|
|
530
|
+
* Three deliberate narrowings, each of which exists to avoid rejecting a run
|
|
531
|
+
* that succeeds today:
|
|
532
|
+
*
|
|
533
|
+
* (i) Only variable names the DEFINITION actually wires through
|
|
534
|
+
* `absolute_file_paths_from` / `images_from` are validated. A caller may
|
|
535
|
+
* legitimately pass any other declared variable that happens to hold
|
|
536
|
+
* path-like strings; the engine never resolves those as paths, so
|
|
537
|
+
* validating them here would reject working runs.
|
|
538
|
+
* (ii) `plan-review-receipts` is skipped entirely.
|
|
539
|
+
* `enforcePlanReviewContextExclusions` strips force-excluded plan paths
|
|
540
|
+
* BEFORE the engine's check, so a path destined for stripping is never
|
|
541
|
+
* validated today and must not start being rejected at admission.
|
|
542
|
+
* (iii) The resume path does not call this. Resume prefers persisted inputs and
|
|
543
|
+
* skips completed calls, so re-validating current variables could reject a
|
|
544
|
+
* resume whose original admission passed.
|
|
545
|
+
*
|
|
546
|
+
* Non-array values and non-string entries are deliberately NOT rejected here —
|
|
547
|
+
* they are left to the engine so its existing error text is unchanged.
|
|
548
|
+
*/
|
|
549
|
+
function assertCallerDynamicPathsAllowed(normalized, variables, workflowName, runId) {
|
|
550
|
+
// Caveat (ii).
|
|
551
|
+
if (workflowName === PLAN_REVIEW_RECEIPTS_WORKFLOW) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
// Caveat (i): the set of variable names this definition wires as paths.
|
|
555
|
+
const wiredNames = new Set();
|
|
556
|
+
const collectWiredNames = (declared) => {
|
|
557
|
+
if (typeof declared === "string") {
|
|
558
|
+
wiredNames.add(declared);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (declared === undefined) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
for (const name of declared) {
|
|
565
|
+
wiredNames.add(name);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
for (const stage of normalized.stages) {
|
|
569
|
+
for (const call of stage.calls) {
|
|
570
|
+
collectWiredNames(call.absolute_file_paths_from);
|
|
571
|
+
collectWiredNames(call.images_from);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (wiredNames.size === 0) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
// The engine derives its own roots and never consults CLINKX_ALLOWED_ROOTS;
|
|
578
|
+
// mirror that exact set so admission cannot be stricter or looser than
|
|
579
|
+
// dispatch.
|
|
580
|
+
const allowedRoots = defaultWorkflowAllowedRoots(getRunStateDir(runId));
|
|
581
|
+
const workspaceRoot = resolveWorkspaceRoot();
|
|
582
|
+
for (const name of wiredNames) {
|
|
583
|
+
const rawValue = variables[name];
|
|
584
|
+
if (!Array.isArray(rawValue)) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
// Mirrors resolveDynamicPathVariable (engine.ts) exactly.
|
|
588
|
+
const values = rawValue
|
|
589
|
+
.filter((entry) => typeof entry === "string")
|
|
590
|
+
.map((entry) => (isAbsolute(entry) ? entry : resolve(workspaceRoot, entry)));
|
|
591
|
+
if (values.length === 0) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
validateDynamicPaths(values, allowedRoots, `${workflowName}.${name}`);
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
if (err instanceof WorkflowValidationError) {
|
|
599
|
+
throw new McpError(ErrorCode.InvalidParams, err.message);
|
|
600
|
+
}
|
|
601
|
+
throw err;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function validateContextBuilderAnchorSufficiency(variables, runner, contextDefault, defaultCurator) {
|
|
606
|
+
if (runner == null) {
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
let policy;
|
|
610
|
+
try {
|
|
611
|
+
// Throws on an invalid cb_context_mode / cb_delegate combination (surfaced
|
|
612
|
+
// below as -32602). The resolved policy is also returned for admission-time
|
|
613
|
+
// checks that must stay inert under soft off.
|
|
614
|
+
policy = resolveWorkflowContextMode(variables["cb_context_mode"], contextDefault, true, variables["cb_delegate"], defaultCurator);
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
if (error instanceof WorkflowContextModeResolverError ||
|
|
618
|
+
error instanceof WorkflowDelegateValidationError) {
|
|
619
|
+
throw new McpError(ErrorCode.InvalidParams, error.message);
|
|
620
|
+
}
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
const cbRoots = readStringArrayForAnchorCheck(variables["cb_roots"]);
|
|
624
|
+
// Eager allowlist validation: explicit cb_roots are validated whenever
|
|
625
|
+
// provided, including soft off. Out-of-allowlist roots are rejected at the
|
|
626
|
+
// boundary as -32602. The disabled hard kill never reaches this validator.
|
|
627
|
+
if (cbRoots.length > 0) {
|
|
628
|
+
try {
|
|
629
|
+
runner.assertRootsAllowed(cbRoots);
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
throw new McpError(ErrorCode.InvalidParams, err instanceof Error ? err.message : String(err));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// policy.preflightDisabled is unreachable here: this validator early-returns
|
|
636
|
+
// when runner == null and always resolves with preflightEnabled:true, so the
|
|
637
|
+
// resolver never returns a disabled policy on this path.
|
|
638
|
+
return policy;
|
|
639
|
+
}
|
|
640
|
+
function validateContextSeedSourceAtAdmission(variables, policy) {
|
|
641
|
+
if (policy == null) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const cbTask = typeof variables["cb_task"] === "string"
|
|
645
|
+
? variables["cb_task"]
|
|
646
|
+
: undefined;
|
|
647
|
+
const userRequest = typeof variables["user_request"] === "string"
|
|
648
|
+
? variables["user_request"]
|
|
649
|
+
: undefined;
|
|
650
|
+
const seedSource = resolveSeedSource(cbTask, userRequest);
|
|
651
|
+
const seedSourceError = contextSeedSourceValidationError(seedSource);
|
|
652
|
+
if (seedSourceError != null) {
|
|
653
|
+
throw new McpError(ErrorCode.InvalidParams, seedSourceError);
|
|
654
|
+
}
|
|
655
|
+
const searchTerms = readStringArrayForAnchorCheck(variables["cb_search_terms"]);
|
|
656
|
+
const searchTermsError = contextSearchTermsValidationError(searchTerms);
|
|
657
|
+
if (searchTermsError != null) {
|
|
658
|
+
throw new McpError(ErrorCode.InvalidParams, searchTermsError);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function readStringArrayForAnchorCheck(value) {
|
|
662
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : [];
|
|
663
|
+
}
|
|
177
664
|
function validateVariableType(name, value, expectedType) {
|
|
178
665
|
switch (expectedType) {
|
|
179
666
|
case "string":
|
|
@@ -597,6 +1084,52 @@ function finalizeWorkflowPresentation(options) {
|
|
|
597
1084
|
}
|
|
598
1085
|
return content;
|
|
599
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Wire projection of the warn-only duplicate-replay signal recorded at
|
|
1089
|
+
* `WorkflowStateStore.create()` (failure investigation F6). Additive and
|
|
1090
|
+
* advisory: identical snapshot hashes do NOT imply an identical outcome —
|
|
1091
|
+
* workspace content is not covered by the hashes and providers are
|
|
1092
|
+
* nondeterministic — so this field never gates admission or execution.
|
|
1093
|
+
* Defensive optional access: partial store mocks may omit the property.
|
|
1094
|
+
*/
|
|
1095
|
+
function buildDuplicateOfFailedRunField(store) {
|
|
1096
|
+
const signal = store?.duplicateOfFailedRun;
|
|
1097
|
+
if (signal === undefined || signal === null)
|
|
1098
|
+
return undefined;
|
|
1099
|
+
return {
|
|
1100
|
+
run_id: signal.runId,
|
|
1101
|
+
...(signal.completedAt !== undefined ? { completed_at: signal.completedAt } : {}),
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Appends the duplicate-replay signal as a trailing additive content block on
|
|
1106
|
+
* sync run_workflow results (success and error). No block is added when no
|
|
1107
|
+
* signal was detected, so existing content ordering is untouched.
|
|
1108
|
+
*/
|
|
1109
|
+
function appendDuplicateReplayBlock(content, store) {
|
|
1110
|
+
const duplicateOfFailedRun = buildDuplicateOfFailedRunField(store);
|
|
1111
|
+
if (duplicateOfFailedRun === undefined)
|
|
1112
|
+
return;
|
|
1113
|
+
content.push({
|
|
1114
|
+
type: "text",
|
|
1115
|
+
text: JSON.stringify({ duplicate_of_failed_run: duplicateOfFailedRun }),
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
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.
|
|
1123
|
+
*/
|
|
1124
|
+
function appendResolvedRosterBlock(content, store) {
|
|
1125
|
+
const resolvedRoster = store?.runState?.resolvedRoster;
|
|
1126
|
+
if (resolvedRoster === undefined)
|
|
1127
|
+
return;
|
|
1128
|
+
content.push({
|
|
1129
|
+
type: "text",
|
|
1130
|
+
text: JSON.stringify({ resolved_roster: resolvedRoster }),
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
600
1133
|
// ---------------------------------------------------------------------------
|
|
601
1134
|
// Actionable error formatting (5-6)
|
|
602
1135
|
// ---------------------------------------------------------------------------
|
|
@@ -655,15 +1188,77 @@ function formatWorkflowError(runId, error, store, workflowSource) {
|
|
|
655
1188
|
let nextAction = "";
|
|
656
1189
|
if (store != null) {
|
|
657
1190
|
const isResumable = state?.resumable === true;
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1191
|
+
const isTerminalPlanReview = state != null &&
|
|
1192
|
+
isPlanReviewRunState(state) &&
|
|
1193
|
+
(state.status === "failed" ||
|
|
1194
|
+
state.status === "cancelled" ||
|
|
1195
|
+
state.status === "succeeded");
|
|
1196
|
+
nextAction = isTerminalPlanReview
|
|
1197
|
+
? `Start a new run with run_workflow and a new run_id; terminal plan-review-receipts runs are immutable.`
|
|
1198
|
+
: isResumable
|
|
1199
|
+
? `Use resume_workflow with run_id "${runId}" to retry from the last checkpoint.`
|
|
1200
|
+
: `Start a new run with run_workflow (this run is not resumable — set CLINKX_WORKFLOWS_CHILD_CONFIG_PATH for resumable runs).`;
|
|
661
1201
|
}
|
|
662
1202
|
return (`Workflow failed [run_id: ${runId}]${sourceLabel}${location}` +
|
|
663
1203
|
(retryCount > 0 ? ` (after ${String(retryCount)} retries)` : "") +
|
|
664
1204
|
`: ${message}${metaBlock}` +
|
|
665
1205
|
(nextAction !== "" ? `\n\n${nextAction}` : ""));
|
|
666
1206
|
}
|
|
1207
|
+
function toCouncilPlanReviewError(runId, error) {
|
|
1208
|
+
if (!(error instanceof PlanReviewStageError)) {
|
|
1209
|
+
return undefined;
|
|
1210
|
+
}
|
|
1211
|
+
return CouncilPlanReviewErrorSchema.parse({
|
|
1212
|
+
schema_version: "council-plan-review-error-v1",
|
|
1213
|
+
run_id: runId,
|
|
1214
|
+
execution_status: "failed",
|
|
1215
|
+
stage: error.stage,
|
|
1216
|
+
error: {
|
|
1217
|
+
reason: error.reason,
|
|
1218
|
+
detail: error.message.slice(0, 2_048),
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Read a well-formed `failureClass` the error already carries. Structural on
|
|
1224
|
+
* purpose: errors crossing the engine boundary cannot be narrowed by
|
|
1225
|
+
* `instanceof` here, and this must never GUESS a class — an error without one
|
|
1226
|
+
* yields undefined, which `markFailed` treats as "leave the existing class
|
|
1227
|
+
* alone".
|
|
1228
|
+
*/
|
|
1229
|
+
function carriedFailureClass(error) {
|
|
1230
|
+
if (error == null || typeof error !== "object") {
|
|
1231
|
+
return undefined;
|
|
1232
|
+
}
|
|
1233
|
+
const value = error.failureClass;
|
|
1234
|
+
return value === "transient" || value === "permanent" ? value : undefined;
|
|
1235
|
+
}
|
|
1236
|
+
async function markWorkflowFailed(store, runId, error) {
|
|
1237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1238
|
+
// Forward a class the error already carries. Load-bearing on the RESUME path:
|
|
1239
|
+
// `beginResumeAttempt` clears `failureClass`, and the resume preflight engine
|
|
1240
|
+
// never runs `execute()`, so its catch never stamps one — without this a
|
|
1241
|
+
// bounded-preflight timeout persists `failed` with no class at all, while the
|
|
1242
|
+
// identical first-run failure keeps `transient`. Safe for the other call
|
|
1243
|
+
// sites: `markFailed` assigns only when defined, so an error carrying no
|
|
1244
|
+
// class still cannot clobber a class the engine already emitted.
|
|
1245
|
+
const planReviewError = toCouncilPlanReviewError(runId, error);
|
|
1246
|
+
if (planReviewError !== undefined) {
|
|
1247
|
+
// Plan-review terminal intent is first-wins. Forwarding a class here would
|
|
1248
|
+
// flip `replaceFailureClass` and overwrite a recorded intent, so this call
|
|
1249
|
+
// stays exactly as it was.
|
|
1250
|
+
await store.markFailed(message, undefined, planReviewError);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const failureClass = carriedFailureClass(error);
|
|
1254
|
+
if (failureClass === undefined) {
|
|
1255
|
+
// Nothing to add: keep the original single-argument call so callers that
|
|
1256
|
+
// rely on `markFailed`'s existing behavior are byte-identical.
|
|
1257
|
+
await store.markFailed(message);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
await store.markFailed(message, failureClass);
|
|
1261
|
+
}
|
|
667
1262
|
function getCallEntryByLocation(stages, location) {
|
|
668
1263
|
const dotIndex = location.indexOf(".");
|
|
669
1264
|
if (dotIndex === -1) {
|
|
@@ -675,6 +1270,54 @@ function getCallEntryByLocation(stages, location) {
|
|
|
675
1270
|
return stage?.calls.find((call) => call.callId === callId);
|
|
676
1271
|
}
|
|
677
1272
|
// ---------------------------------------------------------------------------
|
|
1273
|
+
// Roster preset admission (pre-mutex, pre-state)
|
|
1274
|
+
// ---------------------------------------------------------------------------
|
|
1275
|
+
function applyRosterPresetAtAdmission(workflow, args, variables, rosterLoader) {
|
|
1276
|
+
const raw = args["roster"];
|
|
1277
|
+
if (typeof raw !== "string") {
|
|
1278
|
+
throw new McpError(ErrorCode.InvalidParams, "roster must be a string");
|
|
1279
|
+
}
|
|
1280
|
+
if (!COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
|
|
1281
|
+
throw new McpError(ErrorCode.InvalidParams, `roster parameter is not supported for workflow "${workflow}"`);
|
|
1282
|
+
}
|
|
1283
|
+
const conflicts = [];
|
|
1284
|
+
if (args["members"] != null)
|
|
1285
|
+
conflicts.push("members");
|
|
1286
|
+
if (args["chairman"] != null)
|
|
1287
|
+
conflicts.push("chairman");
|
|
1288
|
+
if (args["chairman_model"] != null)
|
|
1289
|
+
conflicts.push("chairman_model");
|
|
1290
|
+
if (args["chairman_effort"] != null)
|
|
1291
|
+
conflicts.push("chairman_effort");
|
|
1292
|
+
const cbDelegate = variables?.["cb_delegate"];
|
|
1293
|
+
if (typeof cbDelegate === "string" && cbDelegate !== "") {
|
|
1294
|
+
conflicts.push("cb_delegate");
|
|
1295
|
+
}
|
|
1296
|
+
if (conflicts.length > 0) {
|
|
1297
|
+
throw new McpError(ErrorCode.InvalidParams, `roster is exclusive and cannot be combined with ${conflicts.join(", ")}`);
|
|
1298
|
+
}
|
|
1299
|
+
if (rosterLoader == null) {
|
|
1300
|
+
throw new McpError(ErrorCode.InvalidParams, "rosters are not available on this server");
|
|
1301
|
+
}
|
|
1302
|
+
const preset = rosterLoader.load(raw);
|
|
1303
|
+
if (preset === undefined) {
|
|
1304
|
+
const available = rosterLoader.list().map((entry) => entry.name).join(", ");
|
|
1305
|
+
throw new McpError(ErrorCode.InvalidParams, `roster "${raw}" is not a known roster. Available: ${available}`);
|
|
1306
|
+
}
|
|
1307
|
+
const chairman = preset.chairman;
|
|
1308
|
+
const nextVariables = preset.delegate !== undefined
|
|
1309
|
+
? { ...(variables ?? {}), cb_delegate: JSON.stringify(preset.delegate) }
|
|
1310
|
+
: variables;
|
|
1311
|
+
return {
|
|
1312
|
+
rosterName: raw,
|
|
1313
|
+
membersRaw: preset.members,
|
|
1314
|
+
chairmanRaw: chairman?.cli_name,
|
|
1315
|
+
chairmanModelRaw: chairman?.model,
|
|
1316
|
+
chairmanEffortRaw: chairman?.effort,
|
|
1317
|
+
variables: nextVariables,
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
// ---------------------------------------------------------------------------
|
|
678
1321
|
// Tool execution helpers
|
|
679
1322
|
// ---------------------------------------------------------------------------
|
|
680
1323
|
async function executeRunWorkflow(server, options, args, signal, progressToken) {
|
|
@@ -704,108 +1347,589 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
|
|
|
704
1347
|
const knownCliNames = options.availableCliNames != null
|
|
705
1348
|
? new Set(options.availableCliNames)
|
|
706
1349
|
: undefined;
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1350
|
+
// Read RAW `args`, not parsed values: `parseMembersOverride` throws on `[]`
|
|
1351
|
+
// and on an unknown `cli_name`, so in exactly the cases the hint exists to
|
|
1352
|
+
// cover, `parsedMembers` is never bound.
|
|
1353
|
+
const genericShaped = workflow === PLAN_REVIEW_RECEIPTS_WORKFLOW &&
|
|
1354
|
+
(args["plan_review"] == null ||
|
|
1355
|
+
args["members"] != null ||
|
|
1356
|
+
args["chairman"] != null ||
|
|
1357
|
+
args["chairman_model"] != null ||
|
|
1358
|
+
args["chairman_effort"] != null ||
|
|
1359
|
+
args["roster"] != null);
|
|
1360
|
+
// Declarations hoisted out of the decorator's try so the bindings stay in
|
|
1361
|
+
// scope for the post-admission code below; the assignments and their order
|
|
1362
|
+
// are unchanged.
|
|
1363
|
+
let parsedMembers;
|
|
1364
|
+
let parsedChairman;
|
|
1365
|
+
let parsedChairmanModel;
|
|
1366
|
+
let parsedChairmanEffort;
|
|
1367
|
+
let resolvedVariables;
|
|
1368
|
+
let initialContextPolicy;
|
|
1369
|
+
let planReviewAdmission;
|
|
1370
|
+
let admittedRosterName;
|
|
720
1371
|
try {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1372
|
+
let membersRaw = args["members"];
|
|
1373
|
+
let chairmanRaw = args["chairman"];
|
|
1374
|
+
let chairmanModelRaw = args["chairman_model"];
|
|
1375
|
+
let chairmanEffortRaw = args["chairman_effort"];
|
|
1376
|
+
let suppliedVariables = variables;
|
|
1377
|
+
if (args["roster"] !== undefined) {
|
|
1378
|
+
const applied = applyRosterPresetAtAdmission(workflow, args, variables, options.rosterLoader);
|
|
1379
|
+
admittedRosterName = applied.rosterName;
|
|
1380
|
+
membersRaw = applied.membersRaw;
|
|
1381
|
+
chairmanRaw = applied.chairmanRaw;
|
|
1382
|
+
chairmanModelRaw = applied.chairmanModelRaw;
|
|
1383
|
+
chairmanEffortRaw = applied.chairmanEffortRaw;
|
|
1384
|
+
suppliedVariables = applied.variables;
|
|
1385
|
+
}
|
|
1386
|
+
parsedMembers = parseMembersOverride(membersRaw, knownCliNames);
|
|
1387
|
+
parsedChairman = parseChairmanOverride(chairmanRaw, knownCliNames);
|
|
1388
|
+
parsedChairmanModel = parseChairmanModelOverride(chairmanModelRaw);
|
|
1389
|
+
parsedChairmanEffort = parseChairmanEffortOverride(chairmanEffortRaw);
|
|
1390
|
+
if (parsedMembers != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
|
|
1391
|
+
throw new McpError(ErrorCode.InvalidParams, `members parameter is not supported for workflow "${workflow}"`);
|
|
1392
|
+
}
|
|
1393
|
+
if (parsedChairman != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
|
|
1394
|
+
throw new McpError(ErrorCode.InvalidParams, `chairman parameter is not supported for workflow "${workflow}"`);
|
|
738
1395
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
controller.abort(new WorkflowCancellationError());
|
|
742
|
-
}, { once: true });
|
|
1396
|
+
if (parsedChairmanModel != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
|
|
1397
|
+
throw new McpError(ErrorCode.InvalidParams, `chairman_model parameter is not supported for workflow "${workflow}"`);
|
|
743
1398
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1399
|
+
if (parsedChairmanEffort != null && !COUNCIL_RUNTIME_MEMBER_WORKFLOWS.has(workflow)) {
|
|
1400
|
+
throw new McpError(ErrorCode.InvalidParams, `chairman_effort parameter is not supported for workflow "${workflow}"`);
|
|
1401
|
+
}
|
|
1402
|
+
resolvedVariables = validateVariables(suppliedVariables, loaded.definition.variables);
|
|
1403
|
+
// When the runner is absent (preflight disabled / hard kill), the boundary
|
|
1404
|
+
// validator is inert. Skip it WITHOUT evaluating the context-default env
|
|
1405
|
+
// parsers — getWorkflowsContextDefaultCurator() throws on empty env, and a
|
|
1406
|
+
// runner-less run_workflow must never trip context env parsing.
|
|
1407
|
+
let contextPolicy;
|
|
1408
|
+
if (options.contextBuilderRunner != null) {
|
|
1409
|
+
contextPolicy = validateContextBuilderAnchorSufficiency(resolvedVariables, options.contextBuilderRunner, options.contextDefault ?? getWorkflowsContextDefault(), options.defaultCurator ?? getWorkflowsContextDefaultCurator());
|
|
1410
|
+
}
|
|
1411
|
+
validateContextSeedSourceAtAdmission(resolvedVariables, contextPolicy);
|
|
1412
|
+
// Pre-mutex, pre-state, pre-preflight: an out-of-roots caller path costs
|
|
1413
|
+
// zero delegate round-trips and leaves no run directory behind.
|
|
1414
|
+
assertCallerDynamicPathsAllowed(loaded.normalized, resolvedVariables, workflow, runId);
|
|
1415
|
+
initialContextPolicy = contextPolicy ?? resolveWorkflowContextMode(undefined, options.contextDefault ?? "off", false, undefined, options.defaultCurator);
|
|
1416
|
+
if (options.contextBuilderRunner != null) {
|
|
1417
|
+
try {
|
|
1418
|
+
options.contextBuilderRunner.assertTaskSecretsAllowed(resolvedVariables);
|
|
747
1419
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
reporter = progressToken != null
|
|
752
|
-
? new WorkflowProgressReporter(server, progressToken, runId, effectiveDefinition)
|
|
753
|
-
: undefined;
|
|
754
|
-
const rpt = reporter; // local const for closure narrowing
|
|
755
|
-
const hooks = {
|
|
756
|
-
...storeRef.hooks,
|
|
757
|
-
...(rpt != null
|
|
758
|
-
? {
|
|
759
|
-
onProgress: async (event) => {
|
|
760
|
-
await storeRef.hooks.onProgress?.(event);
|
|
761
|
-
await rpt.onProgress(event);
|
|
762
|
-
},
|
|
763
|
-
onRetryScheduled: async (event) => {
|
|
764
|
-
await storeRef.hooks.onRetryScheduled?.(event);
|
|
765
|
-
await rpt.onRetryScheduled(event);
|
|
766
|
-
},
|
|
1420
|
+
catch (err) {
|
|
1421
|
+
if (err instanceof McpError) {
|
|
1422
|
+
throw err;
|
|
767
1423
|
}
|
|
768
|
-
:
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1424
|
+
throw new McpError(ErrorCode.InvalidParams, err instanceof Error ? err.message : String(err));
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
const planReviewCliNames = options.planReviewAvailableCliNames ?? options.availableCliNames;
|
|
1428
|
+
planReviewAdmission = admitCouncilPlanReview({
|
|
1429
|
+
workflow,
|
|
1430
|
+
planReview: args["plan_review"],
|
|
773
1431
|
variables: resolvedVariables,
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
signal: controller.signal,
|
|
778
|
-
hooks,
|
|
1432
|
+
...(planReviewCliNames !== undefined
|
|
1433
|
+
? { availableCliNames: planReviewCliNames }
|
|
1434
|
+
: {}),
|
|
779
1435
|
});
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
1436
|
+
}
|
|
1437
|
+
catch (err) {
|
|
1438
|
+
// Append the hint by MUTATING the caught error and rethrowing that same
|
|
1439
|
+
// instance. Do NOT re-wrap it in `new McpError(err.code, err.message + …)`:
|
|
1440
|
+
// the SDK constructor prefixes its argument with `MCP error <code>: `, so
|
|
1441
|
+
// `err.message` already carries that prefix and re-constructing emits it
|
|
1442
|
+
// twice on the wire (`MCP error -32602: MCP error -32602: …`). Mutating in
|
|
1443
|
+
// place also preserves `code`, `data`, and the original stack.
|
|
1444
|
+
if (genericShaped && err instanceof McpError && err.code === ErrorCode.InvalidParams) {
|
|
1445
|
+
err.message = `${err.message}${planReviewShapeHint()}`;
|
|
1446
|
+
}
|
|
1447
|
+
throw err;
|
|
1448
|
+
}
|
|
1449
|
+
validateInvocationOverridesAtAdmission(workflow, loaded.normalized, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, options.validateInvocationOverrides,
|
|
1450
|
+
// Caller-supplied cb_delegate only, never the roster-materialized one: a
|
|
1451
|
+
// roster preset is Zod-parsed at load, which defaults min_score and erases
|
|
1452
|
+
// whether the preset file actually requested it.
|
|
1453
|
+
admittedRosterName === undefined ? variables?.["cb_delegate"] : undefined);
|
|
1454
|
+
// A-7: reject caller context anchors that resolve to the bound plan or any
|
|
1455
|
+
// plan-workspace PLAN.md. Pre-mutex, so a rejected request creates no state:
|
|
1456
|
+
// the caller must not be able to hand actors a MUTABLE plan alongside the
|
|
1457
|
+
// immutable snapshot.
|
|
1458
|
+
let planReviewPlan;
|
|
1459
|
+
if (planReviewAdmission !== undefined) {
|
|
1460
|
+
const contextPolicy = buildPlanReviewContextPolicy(planReviewAdmission.input.plan_file);
|
|
1461
|
+
const rejected = findRejectedPlanContextAnchors(contextPolicy, resolvedVariables, resolveWorkspaceRoot());
|
|
1462
|
+
if (rejected.length > 0) {
|
|
1463
|
+
throw new McpError(ErrorCode.InvalidParams, `plan-review-receipts forbids plan context anchors; reviewers receive only the immutable snapshot. Offending entries: ${rejected
|
|
1464
|
+
.map((entry) => `${entry.variable}: ${entry.path}`)
|
|
1465
|
+
.join(", ")}`);
|
|
1466
|
+
}
|
|
1467
|
+
planReviewPlan = {
|
|
1468
|
+
admission: planReviewAdmission,
|
|
1469
|
+
contextPolicy,
|
|
1470
|
+
// Deterministic, runtime-owned: the definition must name the snapshot
|
|
1471
|
+
// before `create()` makes the run dir, so the path is derived rather than
|
|
1472
|
+
// discovered. A post-create guard asserts the store agrees.
|
|
1473
|
+
snapshotAbsolutePath: join(getRunStateDir(runId), PLAN_SNAPSHOT_RELPATH),
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
const isAsync = args["async"] === true;
|
|
1477
|
+
const ownership = acquireWorkflowRun(runId, options, isAsync ? undefined : signal);
|
|
1478
|
+
if (isAsync) {
|
|
1479
|
+
// Everything between acquireMutex and the detached spawn must release the
|
|
1480
|
+
// mutex on ANY throw, or the run slot leaks permanently (bricks the server
|
|
1481
|
+
// at the default cap of 1).
|
|
1482
|
+
let store;
|
|
1483
|
+
let effectiveDefinition;
|
|
1484
|
+
let initializedPlanReview;
|
|
1485
|
+
let preparedSnapshot;
|
|
1486
|
+
try {
|
|
1487
|
+
ownership.throwIfCancelled();
|
|
1488
|
+
const runDir = getRunStateDir(runId);
|
|
1489
|
+
if (existsSync(runDir)) {
|
|
1490
|
+
throw new McpError(ErrorCode.InvalidParams, `run_id ${runId} already has persisted state at ${runDir}`);
|
|
1491
|
+
}
|
|
1492
|
+
// Read + digest-verify the plan BEFORE composing: the expander inlines
|
|
1493
|
+
// the snapshot text, so it must exist by expansion time (D9).
|
|
1494
|
+
preparedSnapshot = planReviewPlan === undefined
|
|
1495
|
+
? undefined
|
|
1496
|
+
: await preparePlanReviewSnapshot(planReviewPlan);
|
|
1497
|
+
ownership.throwIfCancelled();
|
|
1498
|
+
// Expand composition ONCE in the foreground; the detached run reuses it.
|
|
1499
|
+
effectiveDefinition = expandRunWorkflowDefinition(loaded.normalized, workflow, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, planReviewPlan, preparedSnapshot);
|
|
1500
|
+
const resolvedRoster = resolvedRosterForAdmission(workflow, effectiveDefinition, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, admittedRosterName);
|
|
1501
|
+
// Create the durable, pollable state record in the FOREGROUND so a failure
|
|
1502
|
+
// here surfaces synchronously (a real error, never a false "running").
|
|
785
1503
|
try {
|
|
786
|
-
|
|
787
|
-
|
|
1504
|
+
store = await WorkflowStateStore.create({
|
|
1505
|
+
runId,
|
|
1506
|
+
definition: effectiveDefinition,
|
|
1507
|
+
variables: resolvedVariables,
|
|
1508
|
+
debugKeepArtifacts: debug,
|
|
1509
|
+
async: true,
|
|
1510
|
+
initialContextPolicy,
|
|
1511
|
+
...(resolvedRoster !== undefined ? { resolvedRoster } : {}),
|
|
1512
|
+
...(admittedRosterName !== undefined ? { rosterName: admittedRosterName } : {}),
|
|
1513
|
+
});
|
|
788
1514
|
}
|
|
789
1515
|
catch (err) {
|
|
790
|
-
|
|
1516
|
+
throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1517
|
+
}
|
|
1518
|
+
ownership.throwIfCancelled();
|
|
1519
|
+
// A-10: the async acknowledgement is a durability boundary. A caller
|
|
1520
|
+
// cannot observe `running` until every plan-review snapshot and the
|
|
1521
|
+
// strict typed state subtree have committed successfully.
|
|
1522
|
+
if (planReviewPlan !== undefined) {
|
|
1523
|
+
initializedPlanReview = await initializePlanReviewRunState(store, planReviewPlan, effectiveDefinition, preparedSnapshot);
|
|
791
1524
|
}
|
|
1525
|
+
ownership.throwIfCancelled();
|
|
792
1526
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1527
|
+
catch (err) {
|
|
1528
|
+
try {
|
|
1529
|
+
if (ownership.signal.aborted || err instanceof WorkflowCancellationError) {
|
|
1530
|
+
await markWorkflowCancelled(store);
|
|
1531
|
+
throw new WorkflowCancellationError();
|
|
1532
|
+
}
|
|
1533
|
+
if (store !== undefined && planReviewPlan !== undefined) {
|
|
1534
|
+
try {
|
|
1535
|
+
await markWorkflowFailed(store, runId, err);
|
|
1536
|
+
}
|
|
1537
|
+
catch (markFailedError) {
|
|
1538
|
+
logger.warn({ err: markFailedError }, "markFailed during async initialization error handling failed");
|
|
1539
|
+
}
|
|
1540
|
+
const errorText = formatWorkflowError(runId, err, store, {
|
|
1541
|
+
source: loaded.source,
|
|
1542
|
+
sourcePath: loaded.sourcePath,
|
|
1543
|
+
});
|
|
1544
|
+
let failureSummary;
|
|
1545
|
+
if (receipt) {
|
|
1546
|
+
try {
|
|
1547
|
+
failureSummary = buildFailureRunSummary(store);
|
|
1548
|
+
}
|
|
1549
|
+
catch (summaryError) {
|
|
1550
|
+
logger.warn({ err: summaryError }, "failure summary construction failed, omitting receipt");
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const content = finalizeWorkflowPresentation({
|
|
1554
|
+
outputText: errorText,
|
|
1555
|
+
receipt,
|
|
1556
|
+
debug,
|
|
1557
|
+
summary: failureSummary,
|
|
1558
|
+
debugBlock: debug ? buildErrorDebugBlock(runId, store) : undefined,
|
|
1559
|
+
});
|
|
1560
|
+
return { content, isError: true };
|
|
1561
|
+
}
|
|
1562
|
+
throw err;
|
|
1563
|
+
}
|
|
1564
|
+
finally {
|
|
1565
|
+
await ownership.finish(store);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
if (store === undefined || effectiveDefinition === undefined) {
|
|
1569
|
+
await ownership.finish(store);
|
|
1570
|
+
throw new McpError(ErrorCode.InternalError, `Failed to initialize async run ${runId}`);
|
|
1571
|
+
}
|
|
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) => {
|
|
1573
|
+
if (err instanceof WorkflowCancellationError) {
|
|
1574
|
+
logger.info({ runId }, "workflow run cancelled");
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
logger.error({ err, runId }, "detached async run failed");
|
|
1578
|
+
});
|
|
1579
|
+
// Additive-only ack shape: base {run_id, status:"running"} is unchanged;
|
|
1580
|
+
// the duplicate-replay field appears only when detected (warn-only, F6).
|
|
1581
|
+
// resolved_roster is the admission-time requested-value projection (E2).
|
|
1582
|
+
const duplicateOfFailedRun = buildDuplicateOfFailedRunField(store);
|
|
1583
|
+
const resolvedRoster = store.runState.resolvedRoster;
|
|
1584
|
+
return {
|
|
1585
|
+
content: [{
|
|
1586
|
+
type: "text",
|
|
1587
|
+
text: JSON.stringify({
|
|
1588
|
+
run_id: runId,
|
|
1589
|
+
status: "running",
|
|
1590
|
+
...(duplicateOfFailedRun !== undefined
|
|
1591
|
+
? { duplicate_of_failed_run: duplicateOfFailedRun }
|
|
1592
|
+
: {}),
|
|
1593
|
+
...(resolvedRoster !== undefined
|
|
1594
|
+
? { resolved_roster: resolvedRoster }
|
|
1595
|
+
: {}),
|
|
1596
|
+
}),
|
|
1597
|
+
}],
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
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);
|
|
1601
|
+
}
|
|
1602
|
+
async function preparePlanReviewSnapshot(plan) {
|
|
1603
|
+
try {
|
|
1604
|
+
const canonical = await readCanonicalPlanSnapshot({
|
|
1605
|
+
planFile: plan.admission.input.plan_file,
|
|
1606
|
+
allowedRoots: planFileAllowedRoots(),
|
|
1607
|
+
claimedDigest: plan.admission.input.digest,
|
|
1608
|
+
});
|
|
1609
|
+
// Fail fast on a roster that cannot fit its own prompts. This is caller
|
|
1610
|
+
// sizing, not a TOCTOU: it is deterministic and pre-state.
|
|
1611
|
+
//
|
|
1612
|
+
// It raises `McpError(-32602)`, which the re-throw below deliberately lets
|
|
1613
|
+
// escape rather than carrying as `{ok:false}` — but the CODE only reaches
|
|
1614
|
+
// the caller on the ASYNC path. `executeWorkflowRun` wraps the sync path in
|
|
1615
|
+
// its generic failure handler, so the same error surfaces there as an
|
|
1616
|
+
// `isError` TEXT result carrying the message but NOT the code. Do not
|
|
1617
|
+
// describe this as "-32602" unqualified; the invariant that holds on BOTH
|
|
1618
|
+
// paths is: no run directory, no persisted state, no provider work.
|
|
1619
|
+
assertPlanReviewPromptBudget({
|
|
1620
|
+
seatCount: plan.admission.execution_contract.seats.length,
|
|
1621
|
+
snapshotBytes: canonical.canonicalBytes.byteLength,
|
|
1622
|
+
maxPromptBytes: getMaxPromptBytes(),
|
|
1623
|
+
});
|
|
1624
|
+
return { ok: true, canonical };
|
|
1625
|
+
}
|
|
1626
|
+
catch (err) {
|
|
1627
|
+
if (err instanceof McpError)
|
|
1628
|
+
throw err;
|
|
1629
|
+
return { ok: false, error: err };
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* A-5/A-6: materialize the immutable snapshot + persisted execution contract
|
|
1634
|
+
* into runtime-owned storage and commit the typed plan-review subtree, all
|
|
1635
|
+
* BEFORE any seat is dispatched.
|
|
1636
|
+
*
|
|
1637
|
+
* A failure here is a post-state PRE-SEAT failure: the run is marked `failed`
|
|
1638
|
+
* with the offending `stage`, and no result sidecar is ever attempted.
|
|
1639
|
+
*/
|
|
1640
|
+
async function initializePlanReviewRunState(store, plan, definition, prepared) {
|
|
1641
|
+
// Raise a carried read/verify failure now that state exists, so a swapped or
|
|
1642
|
+
// unreadable plan is a `failed` run rather than a pre-state rejection.
|
|
1643
|
+
if (!prepared.ok)
|
|
1644
|
+
throw prepared.error;
|
|
1645
|
+
const contract = plan.admission.execution_contract;
|
|
1646
|
+
const snapshot = await persistCanonicalPlanSnapshot(store.runDir, prepared.canonical);
|
|
1647
|
+
if (snapshot.absolutePath !== plan.snapshotAbsolutePath) {
|
|
1648
|
+
throw new PlanReviewStageError("snapshot", `Snapshot path ${snapshot.absolutePath} does not match the runtime-owned path derived for this run (${plan.snapshotAbsolutePath})`);
|
|
1649
|
+
}
|
|
1650
|
+
const persistedContract = await writeExecutionContractSnapshot(store.runDir, contract);
|
|
1651
|
+
const state = {
|
|
1652
|
+
schema_version: "council-plan-review-state-v1",
|
|
1653
|
+
execution_contract_path: persistedContract.relPath,
|
|
1654
|
+
execution_contract_sha256: persistedContract.sha256,
|
|
1655
|
+
execution_contract_digest: plan.admission.execution_contract_digest,
|
|
1656
|
+
snapshot_path: snapshot.relPath,
|
|
1657
|
+
snapshot_sha256: snapshot.sha256,
|
|
1658
|
+
plan_digest: snapshot.planDigest,
|
|
1659
|
+
seat_mappings: deriveSeatMappings(contract),
|
|
1660
|
+
call_bindings: reconstructPlanReviewCallBindings(contract, resolvePlanReviewChairmanCallId(definition)),
|
|
1661
|
+
context_policy: plan.contextPolicy,
|
|
1662
|
+
};
|
|
1663
|
+
validatePlanReviewCallBindings(definition, state.call_bindings);
|
|
1664
|
+
await store.commitPlanReviewState(state);
|
|
1665
|
+
return state;
|
|
1666
|
+
}
|
|
1667
|
+
/** The chairman stage's call id, read from the normalized definition. */
|
|
1668
|
+
function resolvePlanReviewChairmanCallId(definition) {
|
|
1669
|
+
const stage = definition.stages.find((entry) => entry.id === "chairman");
|
|
1670
|
+
const callId = stage?.calls[0]?.id;
|
|
1671
|
+
if (callId === undefined) {
|
|
1672
|
+
throw new PlanReviewStageError("execution_contract", "Plan-review definition has no chairman call");
|
|
1673
|
+
}
|
|
1674
|
+
return callId;
|
|
1675
|
+
}
|
|
1676
|
+
function projectResolvedRosterSeat(call, index, stancePresent) {
|
|
1677
|
+
return {
|
|
1678
|
+
index,
|
|
1679
|
+
cli_name: call.cli_name,
|
|
1680
|
+
...(call.model !== undefined ? { model: call.model } : {}),
|
|
1681
|
+
...(call.effort !== undefined ? { effort: call.effort } : {}),
|
|
1682
|
+
stance_present: stancePresent,
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
function projectResolvedRosterDelegate(policy, resolvedVariables) {
|
|
1686
|
+
const delegate = policy.effectiveDelegate;
|
|
1687
|
+
if (delegate?.enabled !== true) {
|
|
1688
|
+
return { enabled: false };
|
|
1689
|
+
}
|
|
1690
|
+
const finalizerEnabled = effectiveFinalizerEnabled(delegate, resolvedVariables);
|
|
1691
|
+
return {
|
|
1692
|
+
enabled: true,
|
|
1693
|
+
curator: {
|
|
1694
|
+
cli_name: delegate.curator.cli_name,
|
|
1695
|
+
role: delegate.curator.role,
|
|
1696
|
+
...(delegate.curator.model !== undefined ? { model: delegate.curator.model } : {}),
|
|
1697
|
+
...(delegate.curator.effort !== undefined ? { effort: delegate.curator.effort } : {}),
|
|
1698
|
+
},
|
|
1699
|
+
finalizer: {
|
|
1700
|
+
cli_name: delegate.finalizer.cli_name,
|
|
1701
|
+
role: delegate.finalizer.role,
|
|
1702
|
+
...(delegate.finalizer.model !== undefined ? { model: delegate.finalizer.model } : {}),
|
|
1703
|
+
...(delegate.finalizer.effort !== undefined ? { effort: delegate.finalizer.effort } : {}),
|
|
1704
|
+
effective_enabled: finalizerEnabled,
|
|
1705
|
+
},
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Requested-value roster projection for council workflows. Call AFTER
|
|
1710
|
+
* `expandCouncilComposition` / `expandRunWorkflowDefinition`, AFTER
|
|
1711
|
+
* `resolveEffectiveChairmanCall` values are on the definition (or passed as
|
|
1712
|
+
* parsed chairman knobs), and AFTER `effectiveFinalizerEnabled` can be
|
|
1713
|
+
* evaluated from `initialContextPolicy` — and BEFORE `WorkflowStateStore.create()`.
|
|
1714
|
+
* `plan-review-receipts` and any non-council workflow return undefined.
|
|
1715
|
+
*/
|
|
1716
|
+
export function buildResolvedRoster(input) {
|
|
1717
|
+
if (!supportsRuntimeMembers(input.workflow)) {
|
|
1718
|
+
return undefined;
|
|
1719
|
+
}
|
|
1720
|
+
const stages = input.definition.stages;
|
|
1721
|
+
if (stages.length !== COUNCIL_STAGE_COUNT) {
|
|
1722
|
+
return undefined;
|
|
1723
|
+
}
|
|
1724
|
+
const memberCalls = stages[0]?.calls;
|
|
1725
|
+
const reviewerCalls = stages[1]?.calls;
|
|
1726
|
+
const chairmanCall = stages[3]?.calls[0];
|
|
1727
|
+
if (memberCalls === undefined || reviewerCalls === undefined || chairmanCall === undefined) {
|
|
1728
|
+
return undefined;
|
|
1729
|
+
}
|
|
1730
|
+
const members = memberCalls.map((call, index) => projectResolvedRosterSeat(call, index, input.parsedMembers?.[index]?.stance !== undefined));
|
|
1731
|
+
const reviewers = reviewerCalls.map((call, index) => projectResolvedRosterSeat(call, index, false));
|
|
1732
|
+
const effectiveChairman = resolveEffectiveChairmanCall({
|
|
1733
|
+
staticCall: chairmanCall,
|
|
1734
|
+
chairmanCliName: input.parsedChairman,
|
|
1735
|
+
chairmanModel: input.parsedChairmanModel,
|
|
1736
|
+
chairmanEffort: input.parsedChairmanEffort,
|
|
1737
|
+
});
|
|
1738
|
+
const delegate = projectResolvedRosterDelegate(input.initialContextPolicy, input.resolvedVariables);
|
|
1739
|
+
const preflightCalls = delegate.enabled === true
|
|
1740
|
+
? 1 + (delegate.finalizer.effective_enabled ? 1 : 0)
|
|
1741
|
+
: 0;
|
|
1742
|
+
return ResolvedRosterSchema.parse({
|
|
1743
|
+
...(input.rosterName !== undefined ? { roster: input.rosterName } : {}),
|
|
1744
|
+
members,
|
|
1745
|
+
reviewers,
|
|
1746
|
+
chairman: {
|
|
1747
|
+
cli_name: effectiveChairman.cli_name,
|
|
1748
|
+
...(effectiveChairman.model !== undefined ? { model: effectiveChairman.model } : {}),
|
|
1749
|
+
...(effectiveChairman.effort !== undefined ? { effort: effectiveChairman.effort } : {}),
|
|
1750
|
+
},
|
|
1751
|
+
delegate,
|
|
1752
|
+
provider_spawns: members.length + reviewers.length + 1,
|
|
1753
|
+
preflight_calls: preflightCalls,
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
function resolvedRosterForAdmission(workflow, definition, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, rosterName) {
|
|
1757
|
+
return buildResolvedRoster({
|
|
1758
|
+
workflow,
|
|
1759
|
+
definition,
|
|
1760
|
+
...(parsedMembers !== undefined ? { parsedMembers } : {}),
|
|
1761
|
+
...(parsedChairman !== undefined ? { parsedChairman } : {}),
|
|
1762
|
+
...(parsedChairmanModel !== undefined ? { parsedChairmanModel } : {}),
|
|
1763
|
+
...(parsedChairmanEffort !== undefined ? { parsedChairmanEffort } : {}),
|
|
1764
|
+
initialContextPolicy,
|
|
1765
|
+
resolvedVariables,
|
|
1766
|
+
...(rosterName !== undefined ? { rosterName } : {}),
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
function expandRunWorkflowDefinition(definition, workflow, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, planReviewPlan,
|
|
1770
|
+
/**
|
|
1771
|
+
* The already-verified snapshot. This function must NEVER read `plan_file`:
|
|
1772
|
+
* the text it inlines is the digest-verified canonical copy or nothing.
|
|
1773
|
+
*/
|
|
1774
|
+
preparedSnapshot) {
|
|
1775
|
+
if (planReviewPlan !== undefined) {
|
|
1776
|
+
return expandCouncilPlanReview(definition, planReviewPlan.admission.execution_contract,
|
|
1777
|
+
// On a carried read failure the run is already doomed; expand without a
|
|
1778
|
+
// snapshot so the store can be created and mark the run `failed`.
|
|
1779
|
+
//
|
|
1780
|
+
// The verified snapshot is passed WHOLE rather than flattened to `.text`:
|
|
1781
|
+
// the expander's guard re-checks the digest-bearing shape, which a bare
|
|
1782
|
+
// string cannot carry. This is the single read — never re-read `plan_file`.
|
|
1783
|
+
preparedSnapshot?.ok === true ? preparedSnapshot.canonical : undefined);
|
|
1784
|
+
}
|
|
1785
|
+
return parsedMembers != null ||
|
|
1786
|
+
parsedChairman != null ||
|
|
1787
|
+
parsedChairmanModel !== undefined ||
|
|
1788
|
+
parsedChairmanEffort !== undefined
|
|
1789
|
+
? expandCouncilComposition(definition, parsedMembers, workflow, parsedChairman, parsedChairmanModel, parsedChairmanEffort)
|
|
1790
|
+
: definition;
|
|
1791
|
+
}
|
|
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) {
|
|
1793
|
+
let store = preCreatedStore ?? null;
|
|
1794
|
+
let backend = null;
|
|
1795
|
+
let reporter;
|
|
1796
|
+
let releaseLifecycleBinding;
|
|
1797
|
+
let initializedPlanReview = preInitializedPlanReview;
|
|
1798
|
+
const signal = ownership.signal;
|
|
1799
|
+
try {
|
|
1800
|
+
ownership.throwIfCancelled();
|
|
1801
|
+
// The async admission path already expanded the composition (and read the
|
|
1802
|
+
// snapshot) in the foreground; reuse both to avoid redundant work and a
|
|
1803
|
+
// second `plan_file` read.
|
|
1804
|
+
const preparedSnapshot = preExpandedDefinition !== undefined ||
|
|
1805
|
+
planReviewPlan === undefined
|
|
1806
|
+
? undefined
|
|
1807
|
+
: await preparePlanReviewSnapshot(planReviewPlan);
|
|
1808
|
+
ownership.throwIfCancelled();
|
|
1809
|
+
const effectiveDefinition = preExpandedDefinition ??
|
|
1810
|
+
expandRunWorkflowDefinition(loaded.normalized, workflow, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, planReviewPlan, preparedSnapshot);
|
|
1811
|
+
// Create state store (resumable if child config path is set) unless the
|
|
1812
|
+
// async admission path already created it in the foreground.
|
|
1813
|
+
if (store == null) {
|
|
1814
|
+
const resolvedRoster = resolvedRosterForAdmission(workflow, effectiveDefinition, parsedMembers, parsedChairman, parsedChairmanModel, parsedChairmanEffort, initialContextPolicy, resolvedVariables, rosterName);
|
|
1815
|
+
store = await WorkflowStateStore.create({
|
|
1816
|
+
runId,
|
|
1817
|
+
definition: effectiveDefinition,
|
|
1818
|
+
variables: resolvedVariables,
|
|
1819
|
+
debugKeepArtifacts: debug,
|
|
1820
|
+
async: isAsync,
|
|
1821
|
+
initialContextPolicy,
|
|
1822
|
+
...(resolvedRoster !== undefined ? { resolvedRoster } : {}),
|
|
1823
|
+
...(rosterName !== undefined ? { rosterName } : {}),
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
ownership.throwIfCancelled();
|
|
1827
|
+
releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
|
|
1828
|
+
// A-5/A-6: snapshot + contract + typed subtree land here — after state
|
|
1829
|
+
// exists, before the backend is even created, so a snapshot/digest failure
|
|
1830
|
+
// is a `failed` RunState with zero seats dispatched (never a result
|
|
1831
|
+
// sidecar, never a pre-state -32602).
|
|
1832
|
+
if (planReviewPlan !== undefined && preparedSnapshot !== undefined) {
|
|
1833
|
+
initializedPlanReview = await initializePlanReviewRunState(store, planReviewPlan, effectiveDefinition, preparedSnapshot);
|
|
1834
|
+
}
|
|
1835
|
+
ownership.throwIfCancelled();
|
|
1836
|
+
const sessionLogGeneration = store.sessionLogGeneration ??
|
|
1837
|
+
await inspectLegacyWorkflowSessionLogGeneration(store.runDir, runId);
|
|
1838
|
+
ownership.throwIfCancelled();
|
|
1839
|
+
backend = options.createBackend(runId);
|
|
1840
|
+
ownership.setBackend(backend);
|
|
1841
|
+
ownership.throwIfCancelled();
|
|
1842
|
+
await backend.connect();
|
|
1843
|
+
ownership.throwIfCancelled();
|
|
1844
|
+
// Build engine hooks — capture store in a const for closure narrowing
|
|
1845
|
+
const storeRef = store;
|
|
1846
|
+
reporter = progressToken != null
|
|
1847
|
+
? new WorkflowProgressReporter(server, progressToken, runId, effectiveDefinition)
|
|
1848
|
+
: undefined;
|
|
1849
|
+
const rpt = reporter; // local const for closure narrowing
|
|
1850
|
+
const hooks = {
|
|
1851
|
+
...storeRef.hooks,
|
|
1852
|
+
...(rpt != null
|
|
1853
|
+
? {
|
|
1854
|
+
onProgress: async (event) => {
|
|
1855
|
+
await storeRef.hooks.onProgress?.(event);
|
|
1856
|
+
await rpt.onProgress(event);
|
|
1857
|
+
},
|
|
1858
|
+
onRetryScheduled: async (event) => {
|
|
1859
|
+
await storeRef.hooks.onRetryScheduled?.(event);
|
|
1860
|
+
await rpt.onRetryScheduled(event);
|
|
1861
|
+
},
|
|
1862
|
+
}
|
|
1863
|
+
: {}),
|
|
1864
|
+
};
|
|
1865
|
+
const engine = new WorkflowEngine({
|
|
1866
|
+
definition: effectiveDefinition,
|
|
1867
|
+
session: backend,
|
|
1868
|
+
variables: resolvedVariables,
|
|
1869
|
+
validatorRegistry,
|
|
1870
|
+
runId,
|
|
1871
|
+
sessionLogGeneration,
|
|
1872
|
+
runDir: storeRef.runDir,
|
|
1873
|
+
signal,
|
|
1874
|
+
hooks,
|
|
1875
|
+
contextBuilderRunner: options.contextBuilderRunner,
|
|
1876
|
+
contextDefault: options.contextDefault,
|
|
1877
|
+
defaultCurator: options.defaultCurator,
|
|
1878
|
+
commitPreflightContext: storeRef.commitPreflightContext?.bind(storeRef),
|
|
1879
|
+
initialContextPolicy,
|
|
1880
|
+
...(options.deriveInvocationAudit === undefined
|
|
1881
|
+
? {}
|
|
1882
|
+
: { deriveInvocationAudit: options.deriveInvocationAudit }),
|
|
1883
|
+
...(initializedPlanReview !== undefined
|
|
1884
|
+
? {
|
|
1885
|
+
planReviewContextPolicy: initializedPlanReview.context_policy,
|
|
1886
|
+
planReviewCallBindings: initializedPlanReview.call_bindings,
|
|
1887
|
+
commitPlanReviewContextProvenance: storeRef.commitPlanReviewContextProvenance?.bind(storeRef),
|
|
1888
|
+
terminalResultPolicy: {
|
|
1889
|
+
kind: "plan_review_artifacts",
|
|
1890
|
+
project: storeRef.projectPlanReviewTerminalResult.bind(storeRef),
|
|
1891
|
+
},
|
|
1892
|
+
}
|
|
1893
|
+
: {}),
|
|
1894
|
+
});
|
|
1895
|
+
const result = await engine.execute();
|
|
1896
|
+
// Build summary (fail-open)
|
|
1897
|
+
let summary;
|
|
1898
|
+
if (receipt) {
|
|
1899
|
+
try {
|
|
1900
|
+
const cardExtractor = createCardExtractor(effectiveDefinition);
|
|
1901
|
+
summary = buildRunSummaryOrUndefined(result, store, cardExtractor);
|
|
1902
|
+
}
|
|
1903
|
+
catch (err) {
|
|
1904
|
+
logger.warn({ err }, "run summary construction failed, omitting receipt");
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
const content = finalizeWorkflowPresentation({
|
|
1908
|
+
outputText: result.outputText,
|
|
1909
|
+
receipt,
|
|
1910
|
+
debug,
|
|
797
1911
|
summary,
|
|
798
1912
|
debugBlock: debug ? buildDebugBlock(runId, result, store) : undefined,
|
|
799
1913
|
});
|
|
1914
|
+
appendDuplicateReplayBlock(content, store);
|
|
1915
|
+
appendResolvedRosterBlock(content, store);
|
|
800
1916
|
return { content };
|
|
801
1917
|
}
|
|
802
1918
|
catch (error) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1919
|
+
if (signal.aborted || error instanceof WorkflowCancellationError) {
|
|
1920
|
+
await markWorkflowCancelled(store);
|
|
1921
|
+
throw new WorkflowCancellationError();
|
|
1922
|
+
}
|
|
1923
|
+
// Persist a terminal "failed" record for post-admission setup failures
|
|
1924
|
+
// (e.g. backend.connect() / engine construction). Idempotent: the engine
|
|
1925
|
+
// already persists "failed" for in-engine failures, so this won't clobber.
|
|
1926
|
+
if (store != null) {
|
|
1927
|
+
try {
|
|
1928
|
+
await markWorkflowFailed(store, runId, error);
|
|
1929
|
+
}
|
|
1930
|
+
catch (err) {
|
|
1931
|
+
logger.warn({ err }, "markFailed during error handling failed");
|
|
807
1932
|
}
|
|
808
|
-
throw error;
|
|
809
1933
|
}
|
|
810
1934
|
const errorText = formatWorkflowError(runId, error, store, {
|
|
811
1935
|
source: loaded.source,
|
|
@@ -828,27 +1952,18 @@ async function executeRunWorkflow(server, options, args, signal, progressToken)
|
|
|
828
1952
|
summary: failureSummary,
|
|
829
1953
|
debugBlock: debug && store != null ? buildErrorDebugBlock(runId, store) : undefined,
|
|
830
1954
|
});
|
|
1955
|
+
appendDuplicateReplayBlock(content, store);
|
|
1956
|
+
appendResolvedRosterBlock(content, store);
|
|
831
1957
|
return { content, isError: true };
|
|
832
1958
|
}
|
|
833
1959
|
finally {
|
|
834
|
-
reporter?.stop();
|
|
835
|
-
try {
|
|
836
|
-
if (backend != null && !backend.isTerminated) {
|
|
837
|
-
await backend.terminate();
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
catch (cleanupErr) {
|
|
841
|
-
logger.warn({ err: cleanupErr }, "backend.terminate() failed during cleanup");
|
|
842
|
-
}
|
|
843
1960
|
try {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
1961
|
+
releaseLifecycleBinding?.();
|
|
1962
|
+
reporter?.stop();
|
|
847
1963
|
}
|
|
848
|
-
|
|
849
|
-
|
|
1964
|
+
finally {
|
|
1965
|
+
await ownership.finish(store);
|
|
850
1966
|
}
|
|
851
|
-
releaseMutex();
|
|
852
1967
|
}
|
|
853
1968
|
}
|
|
854
1969
|
async function executeResumeWorkflow(server, options, args, signal, progressToken) {
|
|
@@ -864,33 +1979,148 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
|
|
|
864
1979
|
catch {
|
|
865
1980
|
throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
|
|
866
1981
|
}
|
|
867
|
-
|
|
1982
|
+
// Plan-review terminal state is authoritative even while the detached run's
|
|
1983
|
+
// finally still owns the in-process mutex. Read it before duplicate-run
|
|
1984
|
+
// arbitration so terminal-before-cleanup never leaks as "already running".
|
|
1985
|
+
// Any read/validation error is retried by WorkflowStateStore.resume under the
|
|
1986
|
+
// run lock, preserving the existing error surface for corrupt generic state.
|
|
1987
|
+
let persistedBeforeMutex;
|
|
1988
|
+
try {
|
|
1989
|
+
persistedBeforeMutex = await readRunStateForStatus(runId);
|
|
1990
|
+
}
|
|
1991
|
+
catch {
|
|
1992
|
+
persistedBeforeMutex = null;
|
|
1993
|
+
}
|
|
1994
|
+
const persistedState = persistedBeforeMutex?.state;
|
|
1995
|
+
if (persistedState != null &&
|
|
1996
|
+
isPlanReviewRunState(persistedState) &&
|
|
1997
|
+
(persistedState.status === "failed" ||
|
|
1998
|
+
persistedState.status === "cancelled" ||
|
|
1999
|
+
persistedState.status === "succeeded")) {
|
|
2000
|
+
const error = new Error(`Run ${runId} is terminal (${persistedState.status}) and cannot be resumed; start a new run instead.`);
|
|
2001
|
+
return {
|
|
2002
|
+
content: finalizeWorkflowPresentation({
|
|
2003
|
+
outputText: formatWorkflowError(runId, error, null),
|
|
2004
|
+
receipt,
|
|
2005
|
+
debug,
|
|
2006
|
+
summary: undefined,
|
|
2007
|
+
debugBlock: undefined,
|
|
2008
|
+
}),
|
|
2009
|
+
isError: true,
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
const ownership = acquireWorkflowRun(runId, options, signal);
|
|
2013
|
+
const ownedSignal = ownership.signal;
|
|
868
2014
|
let store = null;
|
|
869
2015
|
let backend = null;
|
|
870
2016
|
let reporter;
|
|
2017
|
+
let releaseLifecycleBinding;
|
|
871
2018
|
try {
|
|
2019
|
+
ownership.throwIfCancelled();
|
|
872
2020
|
const resumeOptions = {
|
|
873
2021
|
runId,
|
|
874
2022
|
debugKeepArtifacts: debug,
|
|
875
2023
|
};
|
|
876
2024
|
store = await WorkflowStateStore.resume(resumeOptions);
|
|
2025
|
+
// Open this attempt before classifying an abort during reconstruction;
|
|
2026
|
+
// otherwise a prior terminal record would suppress markCancelled.
|
|
2027
|
+
await store.beginResumeAttempt();
|
|
2028
|
+
ownership.throwIfCancelled();
|
|
2029
|
+
releaseLifecycleBinding = options.contextBuilderLifecycleRouter?.bind(runId, store);
|
|
877
2030
|
const validatorRegistry = createValidatorRegistry();
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
2031
|
+
const initialContextPolicy = store.runState.initialContextPolicy ??
|
|
2032
|
+
reconstructLegacyInitialContextPolicy(store.runState, options.defaultCurator);
|
|
2033
|
+
// A-5 resume fidelity: the persisted contract + snapshot are the SOLE
|
|
2034
|
+
// inputs — `plan_file` is never re-read and caller input is never
|
|
2035
|
+
// re-parsed. Decode + hash-verify + RECOMPUTE before the engine exists, so
|
|
2036
|
+
// a corrupt contract fails post-state/pre-seat rather than dispatching
|
|
2037
|
+
// seats under unverified (or absent) per-call bindings.
|
|
2038
|
+
// Typed detection is by PERSISTED plan-review state presence, not the
|
|
2039
|
+
// definition name alone: legacy runs persisted under the pre-rename name
|
|
2040
|
+
// "council-plan-review" (which now names the GENERIC plan-review council)
|
|
2041
|
+
// must keep routing through the typed restore path, while a new generic
|
|
2042
|
+
// council-plan-review run (no plan_review_state) must never enter it. A
|
|
2043
|
+
// tampered generic run that smuggles in a plan_review_state subtree fails
|
|
2044
|
+
// closed downstream: restorePlanReviewRun / validatePlanReviewCallBindings
|
|
2045
|
+
// reject any definition whose stages/calls do not match the contract.
|
|
2046
|
+
const persistedPlanReview = store.runState.plan_review_state;
|
|
2047
|
+
let restoredPlanReview;
|
|
2048
|
+
if (store.definition.name === PLAN_REVIEW_RECEIPTS_WORKFLOW &&
|
|
2049
|
+
persistedPlanReview === undefined) {
|
|
2050
|
+
throw new PlanReviewStageError("execution_contract", "Persisted plan-review-receipts run is missing plan_review_state");
|
|
885
2051
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
2052
|
+
if (persistedPlanReview !== undefined &&
|
|
2053
|
+
store.definition.name !== PLAN_REVIEW_RECEIPTS_WORKFLOW &&
|
|
2054
|
+
store.definition.name !== LEGACY_PLAN_REVIEW_RECEIPTS_WORKFLOW_NAME) {
|
|
2055
|
+
throw new PlanReviewStageError("execution_contract", `Persisted workflow "${store.definition.name}" cannot carry plan_review_state`);
|
|
890
2056
|
}
|
|
891
|
-
|
|
892
|
-
await store.
|
|
893
|
-
|
|
2057
|
+
if (persistedPlanReview !== undefined) {
|
|
2058
|
+
restoredPlanReview = await restorePlanReviewRun(store.runDir, persistedPlanReview, resolvePlanReviewChairmanCallId(store.definition));
|
|
2059
|
+
const bindingsByCall = validatePlanReviewCallBindings(store.definition, restoredPlanReview.call_bindings);
|
|
2060
|
+
/**
|
|
2061
|
+
* DELIBERATELY the FIRST of two identical checks; `WorkflowEngine`'s
|
|
2062
|
+
* constructor runs the same one. This is not drift.
|
|
2063
|
+
*
|
|
2064
|
+
* The engine's copy is the load-bearing seam: it is the one no caller can
|
|
2065
|
+
* bypass, and it is what a future construction path would inherit. This
|
|
2066
|
+
* copy buys ORDERING — it rejects tampered state before `createBackend` /
|
|
2067
|
+
* `connect`, so a doomed resume never spawns a provider process. The
|
|
2068
|
+
* function is pure and cheap over a handful of prepared inputs, so paying
|
|
2069
|
+
* twice costs nothing measurable and removing EITHER one loses a real
|
|
2070
|
+
* property. If you are here to delete a redundant call: don't.
|
|
2071
|
+
*/
|
|
2072
|
+
validatePlanReviewPreparedInputPolicy(store.resume.preparedInputs ?? new Map(), bindingsByCall, {
|
|
2073
|
+
// From the SAME read that re-verified the snapshot above (D9).
|
|
2074
|
+
snapshotText: restoredPlanReview.snapshot.text,
|
|
2075
|
+
contextPolicy: restoredPlanReview.state.context_policy,
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
ownership.throwIfCancelled();
|
|
2079
|
+
const sessionLogGeneration = store.sessionLogGeneration ??
|
|
2080
|
+
await inspectLegacyWorkflowSessionLogGeneration(store.runDir, runId);
|
|
2081
|
+
ownership.throwIfCancelled();
|
|
2082
|
+
if (store.runState.workflow_context === undefined) {
|
|
2083
|
+
const preflightEngine = new WorkflowEngine({
|
|
2084
|
+
definition: store.definition,
|
|
2085
|
+
session: createPreflightOnlyBackend(options.availableCliNames),
|
|
2086
|
+
variables: store.variables,
|
|
2087
|
+
validatorRegistry,
|
|
2088
|
+
runId,
|
|
2089
|
+
runDir: store.runDir,
|
|
2090
|
+
signal: ownedSignal,
|
|
2091
|
+
resume: store.resume,
|
|
2092
|
+
contextBuilderRunner: options.contextBuilderRunner,
|
|
2093
|
+
contextDefault: options.contextDefault,
|
|
2094
|
+
defaultCurator: options.defaultCurator,
|
|
2095
|
+
commitPreflightContext: store.commitPreflightContext.bind(store),
|
|
2096
|
+
requireContextPreflight: true,
|
|
2097
|
+
initialContextPolicy,
|
|
2098
|
+
...(options.deriveInvocationAudit === undefined
|
|
2099
|
+
? {}
|
|
2100
|
+
: { deriveInvocationAudit: options.deriveInvocationAudit }),
|
|
2101
|
+
...(restoredPlanReview === undefined
|
|
2102
|
+
? {}
|
|
2103
|
+
: {
|
|
2104
|
+
planReviewContextPolicy: restoredPlanReview.state.context_policy,
|
|
2105
|
+
planReviewCallBindings: restoredPlanReview.call_bindings,
|
|
2106
|
+
planReviewSnapshotText: restoredPlanReview.snapshot.text,
|
|
2107
|
+
commitPlanReviewContextProvenance: store.commitPlanReviewContextProvenance.bind(store),
|
|
2108
|
+
}),
|
|
2109
|
+
});
|
|
2110
|
+
// D1: bound the resume-path preflight by the same fresh `timeout_seconds`
|
|
2111
|
+
// budget a resumed run receives. `execute()` is never called on this engine,
|
|
2112
|
+
// so without this the curator/finalizer delegate runs unbounded. The engine is
|
|
2113
|
+
// constructed with `signal: ownedSignal`, which createDeadlineController chains,
|
|
2114
|
+
// so cancellation ownership is unchanged.
|
|
2115
|
+
await preflightEngine.prepareContextPreflightWithDeadline();
|
|
2116
|
+
}
|
|
2117
|
+
// Create backend only after the owned resume preflight has completed.
|
|
2118
|
+
ownership.throwIfCancelled();
|
|
2119
|
+
backend = options.createBackend(runId);
|
|
2120
|
+
ownership.setBackend(backend);
|
|
2121
|
+
ownership.throwIfCancelled();
|
|
2122
|
+
await backend.connect();
|
|
2123
|
+
ownership.throwIfCancelled();
|
|
894
2124
|
// Build engine hooks with progress reporter
|
|
895
2125
|
reporter = progressToken != null
|
|
896
2126
|
? new WorkflowProgressReporter(server, progressToken, runId, store.definition)
|
|
@@ -922,13 +2152,37 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
|
|
|
922
2152
|
variables: store.variables,
|
|
923
2153
|
validatorRegistry,
|
|
924
2154
|
runId,
|
|
2155
|
+
sessionLogGeneration,
|
|
925
2156
|
runDir: store.runDir,
|
|
926
|
-
signal:
|
|
2157
|
+
signal: ownedSignal,
|
|
927
2158
|
hooks: rpt != null ? hooks : store.hooks,
|
|
928
2159
|
resume: store.resume,
|
|
2160
|
+
contextBuilderRunner: options.contextBuilderRunner,
|
|
2161
|
+
contextDefault: options.contextDefault,
|
|
2162
|
+
defaultCurator: options.defaultCurator,
|
|
2163
|
+
commitPreflightContext: store.commitPreflightContext?.bind(store),
|
|
2164
|
+
restoredWorkflowContext: store.runState.workflow_context,
|
|
2165
|
+
requireContextPreflight: false,
|
|
2166
|
+
initialContextPolicy,
|
|
2167
|
+
...(options.deriveInvocationAudit === undefined
|
|
2168
|
+
? {}
|
|
2169
|
+
: { deriveInvocationAudit: options.deriveInvocationAudit }),
|
|
2170
|
+
// A-7: replay the SAME enforced exclusion on resume, reconstructed from
|
|
2171
|
+
// persisted state rather than re-derived from caller variables.
|
|
2172
|
+
...(restoredPlanReview !== undefined
|
|
2173
|
+
? {
|
|
2174
|
+
planReviewContextPolicy: restoredPlanReview.state.context_policy,
|
|
2175
|
+
planReviewCallBindings: restoredPlanReview.call_bindings,
|
|
2176
|
+
planReviewSnapshotText: restoredPlanReview.snapshot.text,
|
|
2177
|
+
commitPlanReviewContextProvenance: store.commitPlanReviewContextProvenance?.bind(store),
|
|
2178
|
+
terminalResultPolicy: {
|
|
2179
|
+
kind: "plan_review_artifacts",
|
|
2180
|
+
project: store.projectPlanReviewTerminalResult.bind(store),
|
|
2181
|
+
},
|
|
2182
|
+
}
|
|
2183
|
+
: {}),
|
|
929
2184
|
});
|
|
930
2185
|
const result = await engine.execute();
|
|
931
|
-
shutdownCoordinator.unregister();
|
|
932
2186
|
// Build summary (fail-open)
|
|
933
2187
|
let summary;
|
|
934
2188
|
if (receipt) {
|
|
@@ -950,12 +2204,19 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
|
|
|
950
2204
|
return { content };
|
|
951
2205
|
}
|
|
952
2206
|
catch (error) {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
2207
|
+
if (ownedSignal.aborted || error instanceof WorkflowCancellationError) {
|
|
2208
|
+
await markWorkflowCancelled(store);
|
|
2209
|
+
throw new WorkflowCancellationError();
|
|
2210
|
+
}
|
|
2211
|
+
// Persist a terminal "failed" record for post-admission setup failures.
|
|
2212
|
+
// Idempotent: won't clobber an engine-emitted terminal status.
|
|
2213
|
+
if (store != null) {
|
|
2214
|
+
try {
|
|
2215
|
+
await markWorkflowFailed(store, runId, error);
|
|
2216
|
+
}
|
|
2217
|
+
catch (err) {
|
|
2218
|
+
logger.warn({ err }, "markFailed during error handling failed");
|
|
957
2219
|
}
|
|
958
|
-
throw error;
|
|
959
2220
|
}
|
|
960
2221
|
const errorText = formatWorkflowError(runId, error, store);
|
|
961
2222
|
// Build failure summary (fail-open)
|
|
@@ -978,45 +2239,468 @@ async function executeResumeWorkflow(server, options, args, signal, progressToke
|
|
|
978
2239
|
return { content, isError: true };
|
|
979
2240
|
}
|
|
980
2241
|
finally {
|
|
981
|
-
reporter?.stop();
|
|
982
2242
|
try {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
2243
|
+
releaseLifecycleBinding?.();
|
|
2244
|
+
reporter?.stop();
|
|
986
2245
|
}
|
|
987
|
-
|
|
988
|
-
|
|
2246
|
+
finally {
|
|
2247
|
+
await ownership.finish(store);
|
|
989
2248
|
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
// ---------------------------------------------------------------------------
|
|
2252
|
+
// Sidecar access (atomic, fd-based, cheap-poll-safe)
|
|
2253
|
+
// ---------------------------------------------------------------------------
|
|
2254
|
+
/**
|
|
2255
|
+
* Thrown for genuine containment-policy violations (absolute outputPath or an
|
|
2256
|
+
* escape past the run directory). These MUST reject even on a cheap default
|
|
2257
|
+
* poll. Everything else (missing file, fs race, permission) is swallowed on the
|
|
2258
|
+
* cheap path so liveness polling never throws for a recoverable condition.
|
|
2259
|
+
*
|
|
2260
|
+
* Alias of the shared reader's policy error so the existing `instanceof`
|
|
2261
|
+
* cheap-poll discrimination below is unchanged.
|
|
2262
|
+
*/
|
|
2263
|
+
const SidecarPolicyError = ContainedFilePolicyError;
|
|
2264
|
+
/**
|
|
2265
|
+
* Read a run sidecar through the shared containment-safe, no-follow reader.
|
|
2266
|
+
*
|
|
2267
|
+
* The reader is shared (not re-implemented) with the plan-review snapshot and
|
|
2268
|
+
* contract paths: one audited realpath + `O_NOFOLLOW` + single-handle
|
|
2269
|
+
* implementation, so a fix to the TOCTOU handling lands everywhere at once.
|
|
2270
|
+
*/
|
|
2271
|
+
async function readContainedSidecar(runDir, outputPath, wantContent) {
|
|
2272
|
+
const read = await readContainedRelativeFile(runDir, outputPath, {
|
|
2273
|
+
wantContent,
|
|
2274
|
+
// Preserve this path's established error wording byte-for-byte.
|
|
2275
|
+
containerLabel: "run directory",
|
|
2276
|
+
pathLabel: "outputPath",
|
|
2277
|
+
});
|
|
2278
|
+
return read.content !== undefined
|
|
2279
|
+
? { size: read.size, content: read.content }
|
|
2280
|
+
: { size: read.size };
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Legacy state-v1 runs did not persist their admission-time default. Rebuild
|
|
2284
|
+
* only caller-explicit choices; otherwise require deterministic preflight so a
|
|
2285
|
+
* changed server default cannot silently introduce or switch a delegate.
|
|
2286
|
+
*/
|
|
2287
|
+
export function reconstructLegacyInitialContextPolicy(state, defaultCurator) {
|
|
2288
|
+
const persistedMode = state.workflow_context?.context_builder.context_mode;
|
|
2289
|
+
if (persistedMode === "disabled") {
|
|
2290
|
+
return {
|
|
2291
|
+
requestedMode: "off",
|
|
2292
|
+
effectiveMode: "disabled",
|
|
2293
|
+
preflightDisabled: true,
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
const legacyVariables = state.variables ?? {};
|
|
2297
|
+
const callerMode = legacyVariables["cb_context_mode"];
|
|
2298
|
+
const normalizedMode = typeof callerMode === "string" ? callerMode.trim() : "";
|
|
2299
|
+
if (normalizedMode === "off" || normalizedMode === "deterministic") {
|
|
2300
|
+
return {
|
|
2301
|
+
requestedMode: normalizedMode,
|
|
2302
|
+
effectiveMode: normalizedMode,
|
|
2303
|
+
preflightDisabled: false,
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
if (normalizedMode === "full" && legacyVariables["cb_delegate"] != null) {
|
|
990
2307
|
try {
|
|
991
|
-
|
|
992
|
-
|
|
2308
|
+
return resolveWorkflowContextMode("full", "deterministic", true, legacyVariables["cb_delegate"], defaultCurator);
|
|
2309
|
+
}
|
|
2310
|
+
catch {
|
|
2311
|
+
// Legacy hard-disabled runs may contain a delegate value that was never
|
|
2312
|
+
// validated. Do not turn it into a new resume-time fatal or provider.
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return {
|
|
2316
|
+
requestedMode: normalizedMode === "full" || persistedMode === "full"
|
|
2317
|
+
? "full"
|
|
2318
|
+
: "deterministic",
|
|
2319
|
+
effectiveMode: "deterministic",
|
|
2320
|
+
preflightDisabled: false,
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Best-effort read-time parse of the persisted context-builder lifecycle
|
|
2325
|
+
* snapshot. Status projection is observability-only and must never throw for
|
|
2326
|
+
* a malformed or absent snapshot — invalid input yields undefined and the
|
|
2327
|
+
* dependent health/context fields are simply omitted.
|
|
2328
|
+
*/
|
|
2329
|
+
function parseLifecycleForStatus(value) {
|
|
2330
|
+
if (value == null)
|
|
2331
|
+
return undefined;
|
|
2332
|
+
try {
|
|
2333
|
+
return parseContextBuilderLifecycleStateV1(value);
|
|
2334
|
+
}
|
|
2335
|
+
catch {
|
|
2336
|
+
return undefined;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
async function executeWorkflowRunStatus(_options, args) {
|
|
2340
|
+
const runId = args["run_id"];
|
|
2341
|
+
const includeResult = args["include_result"] === true;
|
|
2342
|
+
if (typeof runId !== "string" || runId === "") {
|
|
2343
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid "run_id" parameter');
|
|
2344
|
+
}
|
|
2345
|
+
try {
|
|
2346
|
+
validateRunId(runId);
|
|
2347
|
+
}
|
|
2348
|
+
catch {
|
|
2349
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
|
|
2350
|
+
}
|
|
2351
|
+
const read = await readRunStateForStatus(runId);
|
|
2352
|
+
if (read == null) {
|
|
2353
|
+
return {
|
|
2354
|
+
content: [{ type: "text", text: JSON.stringify({ status: "not_found", run_id: runId }) }],
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
const { state, lock } = read;
|
|
2358
|
+
const status = (state.status === "running" || state.status === "pending") && lock === "stale_or_absent"
|
|
2359
|
+
? "orphaned"
|
|
2360
|
+
: state.status;
|
|
2361
|
+
const runDir = getRunStateDir(runId);
|
|
2362
|
+
const stages = state.stages.map((stage) => {
|
|
2363
|
+
const completedCalls = stage.calls.filter((call) => call.state === "succeeded").length;
|
|
2364
|
+
const runningCalls = stage.calls.filter((call) => call.state === "running").length;
|
|
2365
|
+
const failedCalls = stage.calls.filter((call) => call.state === "failed").length;
|
|
2366
|
+
return {
|
|
2367
|
+
stage_id: stage.stageId,
|
|
2368
|
+
state: stage.state,
|
|
2369
|
+
total_calls: stage.calls.length,
|
|
2370
|
+
completed_calls: completedCalls,
|
|
2371
|
+
running_calls: runningCalls,
|
|
2372
|
+
failed_calls: failedCalls,
|
|
2373
|
+
calls: stage.calls.map((call) => ({
|
|
2374
|
+
call_id: call.callId,
|
|
2375
|
+
state: call.state,
|
|
2376
|
+
retry_count: call.retryCount,
|
|
2377
|
+
...(call.startedAt != null ? { started_at: call.startedAt } : {}),
|
|
2378
|
+
...(call.completedAt != null ? { completed_at: call.completedAt } : {}),
|
|
2379
|
+
...(call.durationMs != null ? { duration_ms: call.durationMs } : {}),
|
|
2380
|
+
...(call.errorMessage != null ? { error: call.errorMessage } : {}),
|
|
2381
|
+
})),
|
|
2382
|
+
};
|
|
2383
|
+
});
|
|
2384
|
+
const projection = {
|
|
2385
|
+
run_id: state.runId,
|
|
2386
|
+
status,
|
|
2387
|
+
async: state.async ?? false,
|
|
2388
|
+
workflow: {
|
|
2389
|
+
name: state.workflowName,
|
|
2390
|
+
version: state.workflowVersion,
|
|
2391
|
+
},
|
|
2392
|
+
...(state.startedAt != null ? { started_at: state.startedAt } : {}),
|
|
2393
|
+
...(state.completedAt != null ? { completed_at: state.completedAt } : {}),
|
|
2394
|
+
...(lock === "unknown" ? { lock_ownership: "unknown" } : {}),
|
|
2395
|
+
stages,
|
|
2396
|
+
};
|
|
2397
|
+
// artifacts_dir is cheap (pure join, no I/O) and present for every found run.
|
|
2398
|
+
projection["artifacts_dir"] = join(runDir, "artifacts");
|
|
2399
|
+
// Read-only context-selection observability from already-persisted state.
|
|
2400
|
+
// No file I/O — only projects state.workflow_context.context_builder and
|
|
2401
|
+
// the state.context_builder_lifecycle snapshot.
|
|
2402
|
+
const lifecycle = parseLifecycleForStatus(state.context_builder_lifecycle);
|
|
2403
|
+
const cb = state.workflow_context?.context_builder;
|
|
2404
|
+
if (cb != null) {
|
|
2405
|
+
const context = {
|
|
2406
|
+
...(cb.context_mode != null ? { context_mode: cb.context_mode } : {}),
|
|
2407
|
+
status: cb.status,
|
|
2408
|
+
files_selected: cb.metrics.files_selected,
|
|
2409
|
+
};
|
|
2410
|
+
if (cb.delegate != null) {
|
|
2411
|
+
context["delegate"] = {
|
|
2412
|
+
status: cb.delegate.status,
|
|
2413
|
+
fallback_used: cb.delegate.fallback_used,
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
if (lifecycle != null) {
|
|
2417
|
+
// Additive lifecycle observability. `coverage.missing` is a bounded
|
|
2418
|
+
// integer count in the persisted schema (never a list), so it is
|
|
2419
|
+
// projected as-is with no truncation concern.
|
|
2420
|
+
context["lifecycle_status"] = lifecycle.status;
|
|
2421
|
+
context["coverage"] = {
|
|
2422
|
+
policy: lifecycle.coverage.policy,
|
|
2423
|
+
status: lifecycle.coverage.status,
|
|
2424
|
+
required: lifecycle.coverage.required,
|
|
2425
|
+
selected: lifecycle.coverage.selected,
|
|
2426
|
+
missing: lifecycle.coverage.missing,
|
|
2427
|
+
};
|
|
2428
|
+
const finalizer = lifecycle.finalizer;
|
|
2429
|
+
if (typeof finalizer.effective_enabled === "boolean" &&
|
|
2430
|
+
typeof finalizer.execution_status === "string") {
|
|
2431
|
+
const existingDelegate = context["delegate"] !== undefined &&
|
|
2432
|
+
typeof context["delegate"] === "object" &&
|
|
2433
|
+
context["delegate"] !== null
|
|
2434
|
+
? context["delegate"]
|
|
2435
|
+
: {};
|
|
2436
|
+
context["delegate"] = {
|
|
2437
|
+
...existingDelegate,
|
|
2438
|
+
finalizer: {
|
|
2439
|
+
effective_enabled: finalizer.effective_enabled,
|
|
2440
|
+
execution_status: finalizer.execution_status,
|
|
2441
|
+
// The lifecycle contract only admits a score on succeeded
|
|
2442
|
+
// execution; re-check here so the projection cannot surface one
|
|
2443
|
+
// for any other outcome.
|
|
2444
|
+
...(finalizer.execution_status === "succeeded" &&
|
|
2445
|
+
typeof finalizer.score === "number" &&
|
|
2446
|
+
Number.isFinite(finalizer.score)
|
|
2447
|
+
? { score: finalizer.score }
|
|
2448
|
+
: {}),
|
|
2449
|
+
},
|
|
2450
|
+
};
|
|
993
2451
|
}
|
|
994
2452
|
}
|
|
995
|
-
|
|
996
|
-
|
|
2453
|
+
projection["context"] = context;
|
|
2454
|
+
}
|
|
2455
|
+
else if (lifecycle != null &&
|
|
2456
|
+
!isTerminalContextBuilderLifecycleStatus(lifecycle.status)) {
|
|
2457
|
+
// Preflight in flight: `context` is reserved for committed-preflight
|
|
2458
|
+
// semantics, so mid-scan progress rides on its own top-level key.
|
|
2459
|
+
projection["context_progress"] = {
|
|
2460
|
+
phase: lifecycle.phase,
|
|
2461
|
+
status: lifecycle.status,
|
|
2462
|
+
updated_at: lifecycle.updated_at,
|
|
2463
|
+
counts: {
|
|
2464
|
+
files_seen: lifecycle.counts.files_seen,
|
|
2465
|
+
files_considered: lifecycle.counts.files_considered,
|
|
2466
|
+
files_selected: lifecycle.counts.files_selected,
|
|
2467
|
+
files_omitted: lifecycle.counts.files_omitted,
|
|
2468
|
+
search_results: lifecycle.counts.search_results,
|
|
2469
|
+
bytes_read: lifecycle.counts.bytes_read,
|
|
2470
|
+
bytes_written: lifecycle.counts.bytes_written,
|
|
2471
|
+
},
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
// F7 read-time health projection: top-level run status alone overstates
|
|
2475
|
+
// health when on_failure:continue swallowed seat failures or context
|
|
2476
|
+
// preflight completed partial/degraded. Computed only from already-persisted
|
|
2477
|
+
// data; additive and advisory — never mutates status semantics.
|
|
2478
|
+
projection["health"] = computeRunHealth(state, lifecycle);
|
|
2479
|
+
if (state.resolvedRoster !== undefined) {
|
|
2480
|
+
projection["resolved_roster"] = state.resolvedRoster;
|
|
2481
|
+
}
|
|
2482
|
+
if (isPlanReviewRunState(state)) {
|
|
2483
|
+
const terminalExecutionStatus = state.status === "succeeded" ||
|
|
2484
|
+
state.status === "failed" ||
|
|
2485
|
+
state.status === "cancelled"
|
|
2486
|
+
? state.status
|
|
2487
|
+
: undefined;
|
|
2488
|
+
const publication = state.plan_review_state?.publication;
|
|
2489
|
+
const inspected = await inspectPlanReviewPublication(runDir, runId, terminalExecutionStatus ??
|
|
2490
|
+
(includeResult && publication?.committed === true ? "failed" : undefined), publication, includeResult);
|
|
2491
|
+
projection["result_available"] = inspected.result_available;
|
|
2492
|
+
projection["result_bytes"] = inspected.result_bytes;
|
|
2493
|
+
if (inspected.result !== undefined) {
|
|
2494
|
+
projection["result"] = inspected.result;
|
|
2495
|
+
}
|
|
2496
|
+
let planReviewError = (includeResult && publication?.committed === true ? inspected.error : undefined) ??
|
|
2497
|
+
state.plan_review_error ??
|
|
2498
|
+
inspected.error;
|
|
2499
|
+
if (planReviewError === undefined &&
|
|
2500
|
+
publication === undefined &&
|
|
2501
|
+
state.plan_review_state === undefined &&
|
|
2502
|
+
(state.status === "succeeded" || state.status === "failed")) {
|
|
2503
|
+
planReviewError = createPlanReviewPublicationError(runId, state.status, "no_artifact", "Historical plan-review run has no publication marker; rerun with a Track-C-capable ClinkX runtime.");
|
|
2504
|
+
}
|
|
2505
|
+
if (planReviewError !== undefined) {
|
|
2506
|
+
projection["plan_review_error"] = planReviewError;
|
|
2507
|
+
}
|
|
2508
|
+
if (state.status === "failed" || state.status === "cancelled") {
|
|
2509
|
+
if (state.error != null) {
|
|
2510
|
+
projection["error"] = state.error;
|
|
2511
|
+
}
|
|
2512
|
+
if (state.failureClass != null) {
|
|
2513
|
+
projection["failure_class"] = state.failureClass;
|
|
2514
|
+
}
|
|
997
2515
|
}
|
|
998
|
-
releaseMutex();
|
|
999
2516
|
}
|
|
2517
|
+
else {
|
|
2518
|
+
projection["result_available"] = state.status === "succeeded";
|
|
2519
|
+
if (state.status === "succeeded") {
|
|
2520
|
+
const final = state.stages.at(-1);
|
|
2521
|
+
const call = final?.calls[0];
|
|
2522
|
+
// Cheap size hint without reading contents. Containment (via
|
|
2523
|
+
// readContainedSidecar) runs even on a default poll so an absolute path or
|
|
2524
|
+
// symlink escape is rejected regardless of include_result. Everything else
|
|
2525
|
+
// — a missing final call, a missing/unreadable file, an fs race — omits the
|
|
2526
|
+
// hint rather than throwing, preserving the "a cheap poll never throws for a
|
|
2527
|
+
// recoverable/missing-file condition" invariant that liveness polling needs.
|
|
2528
|
+
if (call?.outputInline != null) {
|
|
2529
|
+
projection["result_bytes"] = Buffer.byteLength(call.outputInline, "utf-8");
|
|
2530
|
+
}
|
|
2531
|
+
else if (!includeResult && call?.outputPath != null) {
|
|
2532
|
+
// Skip this size-only open when include_result is set — the block below
|
|
2533
|
+
// opens the sidecar once for both size and content, so a second open here
|
|
2534
|
+
// would be redundant.
|
|
2535
|
+
try {
|
|
2536
|
+
const { size } = await readContainedSidecar(runDir, call.outputPath, false);
|
|
2537
|
+
projection["result_bytes"] = size;
|
|
2538
|
+
}
|
|
2539
|
+
catch (err) {
|
|
2540
|
+
if (err instanceof SidecarPolicyError) {
|
|
2541
|
+
// Absolute path / containment escape rejects even on a cheap poll.
|
|
2542
|
+
throw new Error(`Succeeded run ${runId} ${err.message}`);
|
|
2543
|
+
}
|
|
2544
|
+
// Missing file / fs race / permission on a cheap poll: omit the hint.
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
if (includeResult) {
|
|
2548
|
+
if (call == null) {
|
|
2549
|
+
throw new Error(`Succeeded run ${runId} is missing final call state`);
|
|
2550
|
+
}
|
|
2551
|
+
if (call.outputInline != null) {
|
|
2552
|
+
projection["result"] = call.outputInline;
|
|
2553
|
+
}
|
|
2554
|
+
else if (call.outputPath != null) {
|
|
2555
|
+
try {
|
|
2556
|
+
const { size, content } = await readContainedSidecar(runDir, call.outputPath, true);
|
|
2557
|
+
projection["result_bytes"] = size;
|
|
2558
|
+
projection["result"] = content;
|
|
2559
|
+
}
|
|
2560
|
+
catch (err) {
|
|
2561
|
+
if (err instanceof SidecarPolicyError) {
|
|
2562
|
+
throw new Error(`Succeeded run ${runId} ${err.message}`);
|
|
2563
|
+
}
|
|
2564
|
+
throw new Error(`Succeeded run ${runId} output file is missing or unreadable`);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
else {
|
|
2568
|
+
throw new Error(`Succeeded run ${runId} final call has neither inline nor persisted output`);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
else if (state.status === "failed" || state.status === "cancelled") {
|
|
2573
|
+
if (state.error != null) {
|
|
2574
|
+
projection["error"] = state.error;
|
|
2575
|
+
}
|
|
2576
|
+
if (state.failureClass != null) {
|
|
2577
|
+
projection["failure_class"] = state.failureClass;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
return {
|
|
2582
|
+
content: [{ type: "text", text: JSON.stringify(projection) }],
|
|
2583
|
+
};
|
|
1000
2584
|
}
|
|
1001
|
-
function
|
|
1002
|
-
const
|
|
2585
|
+
async function executeCancelWorkflow(_options, args) {
|
|
2586
|
+
const runId = args["run_id"];
|
|
2587
|
+
if (typeof runId !== "string" || runId === "") {
|
|
2588
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid "run_id" parameter');
|
|
2589
|
+
}
|
|
2590
|
+
try {
|
|
2591
|
+
validateRunId(runId);
|
|
2592
|
+
}
|
|
2593
|
+
catch {
|
|
2594
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid run_id "${runId}". Must match [A-Za-z0-9_-]{1,64}.`);
|
|
2595
|
+
}
|
|
2596
|
+
const text = (value) => ({
|
|
2597
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
2598
|
+
});
|
|
2599
|
+
// 1) Live in-process async run: abort its controller and let the run's own
|
|
2600
|
+
// unwind drive it to terminal "cancelled". We do NOT touch the mutex here —
|
|
2601
|
+
// the detached run's finally owns the slot (touching it risks a double-free).
|
|
2602
|
+
const controller = cancelRegistry.get(runId);
|
|
2603
|
+
if (controller != null) {
|
|
2604
|
+
// The engine can have already persisted a terminal status while its
|
|
2605
|
+
// controller is still in the registry (deleted only in the run's finally).
|
|
2606
|
+
// Re-read first so we never claim "cancelling" for an already-done run.
|
|
2607
|
+
const live = await readRunStateForStatus(runId);
|
|
2608
|
+
if (live != null && live.state.status !== "running" && live.state.status !== "pending") {
|
|
2609
|
+
return text({ run_id: runId, status: live.state.status, already_terminal: true });
|
|
2610
|
+
}
|
|
2611
|
+
if (!controller.signal.aborted) {
|
|
2612
|
+
controller.abort(new WorkflowCancellationError());
|
|
2613
|
+
}
|
|
2614
|
+
return text({ run_id: runId, status: "cancelling" });
|
|
2615
|
+
}
|
|
2616
|
+
// 2) Not in our registry — inspect persisted state via the same path as status.
|
|
2617
|
+
const read = await readRunStateForStatus(runId);
|
|
2618
|
+
if (read == null) {
|
|
2619
|
+
return text({ run_id: runId, status: "not_found" });
|
|
2620
|
+
}
|
|
2621
|
+
const { state, lock } = read;
|
|
2622
|
+
if (state.status !== "running" && state.status !== "pending") {
|
|
2623
|
+
return text({ run_id: runId, status: state.status, already_terminal: true });
|
|
2624
|
+
}
|
|
2625
|
+
if (lock === "unknown") {
|
|
2626
|
+
return text({
|
|
2627
|
+
run_id: runId,
|
|
2628
|
+
status: state.status,
|
|
2629
|
+
cancellable: false,
|
|
2630
|
+
lock_ownership: "unknown",
|
|
2631
|
+
error: `Run lock ownership is unknown for run_id "${runId}"; refusing cancellation until the lock record is repaired.`,
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
// running|pending and not in our registry. The run.lock is the single source
|
|
2635
|
+
// of truth for ownership: reapRunCancelled acquires it itself (reclaiming a
|
|
2636
|
+
// truly stale/dead lock, refusing a live one), closing the TOCTOU where a
|
|
2637
|
+
// concurrent resume_workflow could grab the lock between read and reap.
|
|
2638
|
+
const outcome = await reapRunCancelled(runId);
|
|
2639
|
+
if (outcome === "cancelled") {
|
|
2640
|
+
return text({ run_id: runId, status: "cancelled", reaped: true });
|
|
2641
|
+
}
|
|
2642
|
+
if (outcome === "locked") {
|
|
2643
|
+
// The run is owned by another live worker (or a blocking run): the lock
|
|
2644
|
+
// could not be acquired. Refuse without mutating — this process cannot
|
|
2645
|
+
// safely kill a worker it does not own.
|
|
2646
|
+
return text({ run_id: runId, status: "running", cancellable: false });
|
|
2647
|
+
}
|
|
2648
|
+
if (outcome === "ownership_unknown") {
|
|
2649
|
+
return text({
|
|
2650
|
+
run_id: runId,
|
|
2651
|
+
status: state.status,
|
|
2652
|
+
cancellable: false,
|
|
2653
|
+
lock_ownership: "unknown",
|
|
2654
|
+
error: `Run lock ownership became unknown for run_id "${runId}"; refusing cancellation until the lock record is repaired.`,
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
// "skipped": became terminal between our read and the reap. Re-read and report.
|
|
2658
|
+
const reread = await readRunStateForStatus(runId);
|
|
2659
|
+
const status = reread?.state.status ?? "cancelled";
|
|
2660
|
+
return text({ run_id: runId, status, already_terminal: true });
|
|
2661
|
+
}
|
|
2662
|
+
function executeListWorkflows(options) {
|
|
2663
|
+
const workflows = listWorkflowCatalog(options.workflowsPath);
|
|
2664
|
+
const preflightEnabled = options.contextBuilderRunner != null;
|
|
2665
|
+
const contextDefault = preflightEnabled
|
|
2666
|
+
? options.contextDefault ?? getWorkflowsContextDefault()
|
|
2667
|
+
: options.contextDefault ?? "off";
|
|
2668
|
+
const effectiveContextDefault = resolveWorkflowContextMode(undefined, contextDefault, preflightEnabled, undefined, options.defaultCurator).effectiveMode;
|
|
1003
2669
|
const entries = [];
|
|
1004
2670
|
for (const [name, entry] of workflows) {
|
|
1005
2671
|
const def = entry.definition;
|
|
2672
|
+
const contextStrategy = deriveContextStrategy(def);
|
|
2673
|
+
const runtimeMembers = supportsRuntimeMembers(name);
|
|
1006
2674
|
entries.push({
|
|
1007
2675
|
name,
|
|
1008
2676
|
description: def.description,
|
|
1009
2677
|
version: def.version,
|
|
1010
2678
|
source: entry.source,
|
|
1011
2679
|
shadowed_yaml: entry.shadowedYaml,
|
|
1012
|
-
supports_members:
|
|
1013
|
-
supports_chairman:
|
|
1014
|
-
|
|
2680
|
+
supports_members: runtimeMembers,
|
|
2681
|
+
supports_chairman: runtimeMembers,
|
|
2682
|
+
supports_context_preflight: contextStrategy.supports_context_preflight,
|
|
2683
|
+
effective_context_default: effectiveContextDefault,
|
|
2684
|
+
fails_on_empty_selection: contextStrategy.fails_on_empty_selection,
|
|
2685
|
+
context_anchor_variables: contextStrategy.context_anchor_variables,
|
|
2686
|
+
context_override_variables: contextStrategy.context_override_variables,
|
|
2687
|
+
...(contextStrategy.default_context_profile !== undefined
|
|
2688
|
+
? { default_context_profile: contextStrategy.default_context_profile }
|
|
2689
|
+
: {}),
|
|
2690
|
+
context_recipient_stages: contextStrategy.context_recipient_stages,
|
|
2691
|
+
...(runtimeMembers ? {
|
|
1015
2692
|
members_min: 1,
|
|
1016
2693
|
members_max: 10,
|
|
1017
2694
|
members_stance_required_from_index: 3,
|
|
1018
2695
|
} : {}),
|
|
1019
2696
|
...(entry.path != null ? { path: basename(entry.path) } : {}),
|
|
2697
|
+
...(name === PLAN_REVIEW_RECEIPTS_WORKFLOW
|
|
2698
|
+
? {
|
|
2699
|
+
plan_review: buildCouncilPlanReviewDiscovery(options.planReviewAvailableCliNames ??
|
|
2700
|
+
options.availableCliNames ??
|
|
2701
|
+
[]),
|
|
2702
|
+
}
|
|
2703
|
+
: {}),
|
|
1020
2704
|
variables: Object.fromEntries(Object.entries(def.variables).map(([varName, varDef]) => [
|
|
1021
2705
|
varName,
|
|
1022
2706
|
{
|
|
@@ -1029,8 +2713,16 @@ function executeListWorkflows(workflowsPath, availableCliNames) {
|
|
|
1029
2713
|
});
|
|
1030
2714
|
}
|
|
1031
2715
|
const response = { workflows: entries };
|
|
1032
|
-
if (availableCliNames != null && availableCliNames.length > 0) {
|
|
1033
|
-
response["available_cli_names"] = [...availableCliNames].sort();
|
|
2716
|
+
if (options.availableCliNames != null && options.availableCliNames.length > 0) {
|
|
2717
|
+
response["available_cli_names"] = [...options.availableCliNames].sort();
|
|
2718
|
+
}
|
|
2719
|
+
if (options.adapterCapabilities != null) {
|
|
2720
|
+
const availableClis = [...options.adapterCapabilities()];
|
|
2721
|
+
availableClis.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
2722
|
+
response["available_clis"] = availableClis;
|
|
2723
|
+
}
|
|
2724
|
+
if (options.rosterLoader != null) {
|
|
2725
|
+
response["available_rosters"] = options.rosterLoader.list();
|
|
1034
2726
|
}
|
|
1035
2727
|
return {
|
|
1036
2728
|
content: [
|
|
@@ -1041,10 +2733,137 @@ function executeListWorkflows(workflowsPath, availableCliNames) {
|
|
|
1041
2733
|
],
|
|
1042
2734
|
};
|
|
1043
2735
|
}
|
|
2736
|
+
function includesVar(from, name) {
|
|
2737
|
+
if (typeof from === "string") {
|
|
2738
|
+
return from === name;
|
|
2739
|
+
}
|
|
2740
|
+
return Array.isArray(from) && from.includes(name);
|
|
2741
|
+
}
|
|
2742
|
+
// cb_* variables that tune preflight execution rather than source selection —
|
|
2743
|
+
// overrides, not anchors. Advertised separately as context_override_variables.
|
|
2744
|
+
const CONTEXT_OVERRIDE_VARIABLES = new Set([
|
|
2745
|
+
"cb_allow_secrets",
|
|
2746
|
+
"cb_budget_max_estimated_tokens",
|
|
2747
|
+
"cb_budget_max_files",
|
|
2748
|
+
"cb_budget_max_scan_files",
|
|
2749
|
+
"cb_budget_max_bytes_per_file",
|
|
2750
|
+
"cb_budget_max_total_bytes",
|
|
2751
|
+
"cb_budget_max_search_results",
|
|
2752
|
+
"cb_budget_adapter",
|
|
2753
|
+
"cb_delegate",
|
|
2754
|
+
"cb_context_mode",
|
|
2755
|
+
]);
|
|
2756
|
+
function deriveContextStrategy(def) {
|
|
2757
|
+
const contextRecipientStages = def.stages
|
|
2758
|
+
.filter((stage) => stage.calls.some((call) => includesVar(call.absolute_file_paths_from, "context_bundle_files")))
|
|
2759
|
+
.map((stage) => stage.id);
|
|
2760
|
+
const cbVariables = Object.keys(def.variables)
|
|
2761
|
+
.filter((name) => name.startsWith("cb_"))
|
|
2762
|
+
.sort();
|
|
2763
|
+
const contextAnchorVariables = cbVariables.filter((name) => !CONTEXT_OVERRIDE_VARIABLES.has(name));
|
|
2764
|
+
const contextOverrideVariables = cbVariables.filter((name) => CONTEXT_OVERRIDE_VARIABLES.has(name));
|
|
2765
|
+
const defaultContextProfile = def.variables["cb_profile"]?.default;
|
|
2766
|
+
const hasSelectionScopeVariable = "cb_focus_paths" in def.variables || "cb_search_terms" in def.variables;
|
|
2767
|
+
return {
|
|
2768
|
+
supports_context_preflight: "context_bundle_files" in def.variables &&
|
|
2769
|
+
contextRecipientStages.length > 0 &&
|
|
2770
|
+
hasSelectionScopeVariable,
|
|
2771
|
+
context_anchor_variables: contextAnchorVariables,
|
|
2772
|
+
context_override_variables: contextOverrideVariables,
|
|
2773
|
+
...(typeof defaultContextProfile === "string"
|
|
2774
|
+
? { default_context_profile: defaultContextProfile }
|
|
2775
|
+
: {}),
|
|
2776
|
+
context_recipient_stages: contextRecipientStages,
|
|
2777
|
+
fails_on_empty_selection: def.settings.fail_on_empty_context_selection,
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
1044
2780
|
// ---------------------------------------------------------------------------
|
|
1045
2781
|
// Tool input schemas (JSON Schema)
|
|
1046
2782
|
// ---------------------------------------------------------------------------
|
|
1047
2783
|
const MAX_INLINE_WORKFLOW_DETAILS = 8;
|
|
2784
|
+
const PLAN_REVIEW_INPUT_SCHEMA = {
|
|
2785
|
+
type: "object",
|
|
2786
|
+
description: 'Immutable plan-review contract. Accepted only when workflow is "plan-review-receipts".',
|
|
2787
|
+
properties: {
|
|
2788
|
+
schema_version: { type: "string", const: "council-plan-review-input-v1" },
|
|
2789
|
+
workspace_schema: { type: "string", const: "plan-workspace-v1" },
|
|
2790
|
+
digest_profile: { type: "string", const: "checkpoint-text-v1" },
|
|
2791
|
+
review_id: {
|
|
2792
|
+
type: "string",
|
|
2793
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$",
|
|
2794
|
+
},
|
|
2795
|
+
repository_id: {
|
|
2796
|
+
type: "string",
|
|
2797
|
+
maxLength: 128,
|
|
2798
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*(?:/[A-Za-z0-9][A-Za-z0-9._-]*)*$",
|
|
2799
|
+
},
|
|
2800
|
+
iteration: { type: "integer", minimum: 1 },
|
|
2801
|
+
plan_id: {
|
|
2802
|
+
type: "string",
|
|
2803
|
+
pattern: "^[a-z0-9][a-z0-9-]{0,62}$",
|
|
2804
|
+
},
|
|
2805
|
+
revision: { type: "integer", minimum: 1 },
|
|
2806
|
+
digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
2807
|
+
expected_execution_contract_digest: {
|
|
2808
|
+
type: "string",
|
|
2809
|
+
pattern: "^[0-9a-f]{64}$",
|
|
2810
|
+
},
|
|
2811
|
+
threshold: { type: "number", minimum: 0, maximum: 10 },
|
|
2812
|
+
designated_editor: { type: "string", minLength: 1 },
|
|
2813
|
+
chairman_seat: {
|
|
2814
|
+
type: "string",
|
|
2815
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$",
|
|
2816
|
+
},
|
|
2817
|
+
plan_file: {
|
|
2818
|
+
type: "string",
|
|
2819
|
+
minLength: 1,
|
|
2820
|
+
description: "Absolute path to the plan snapshot source",
|
|
2821
|
+
},
|
|
2822
|
+
seats: {
|
|
2823
|
+
type: "array",
|
|
2824
|
+
minItems: 1,
|
|
2825
|
+
maxItems: 10,
|
|
2826
|
+
items: {
|
|
2827
|
+
type: "object",
|
|
2828
|
+
properties: {
|
|
2829
|
+
seat_id: {
|
|
2830
|
+
type: "string",
|
|
2831
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$",
|
|
2832
|
+
},
|
|
2833
|
+
reviewer: { type: "string", minLength: 1 },
|
|
2834
|
+
cli_name: {
|
|
2835
|
+
type: "string",
|
|
2836
|
+
enum: [...PLAN_REVIEW_V1_CLI_NAMES].sort(),
|
|
2837
|
+
},
|
|
2838
|
+
stance: {
|
|
2839
|
+
type: "string",
|
|
2840
|
+
enum: ["strict", "pragmatic", "adversarial"],
|
|
2841
|
+
},
|
|
2842
|
+
},
|
|
2843
|
+
required: ["seat_id", "reviewer", "cli_name", "stance"],
|
|
2844
|
+
additionalProperties: false,
|
|
2845
|
+
},
|
|
2846
|
+
},
|
|
2847
|
+
},
|
|
2848
|
+
required: [
|
|
2849
|
+
"schema_version",
|
|
2850
|
+
"workspace_schema",
|
|
2851
|
+
"digest_profile",
|
|
2852
|
+
"review_id",
|
|
2853
|
+
"repository_id",
|
|
2854
|
+
"iteration",
|
|
2855
|
+
"plan_id",
|
|
2856
|
+
"revision",
|
|
2857
|
+
"digest",
|
|
2858
|
+
"expected_execution_contract_digest",
|
|
2859
|
+
"threshold",
|
|
2860
|
+
"designated_editor",
|
|
2861
|
+
"chairman_seat",
|
|
2862
|
+
"plan_file",
|
|
2863
|
+
"seats",
|
|
2864
|
+
],
|
|
2865
|
+
additionalProperties: false,
|
|
2866
|
+
};
|
|
1048
2867
|
const RUN_WORKFLOW_SCHEMA = {
|
|
1049
2868
|
type: "object",
|
|
1050
2869
|
properties: {
|
|
@@ -1072,9 +2891,20 @@ const RUN_WORKFLOW_SCHEMA = {
|
|
|
1072
2891
|
description: "When true, append a structured receipt summarizing the workflow run as a content block",
|
|
1073
2892
|
default: false,
|
|
1074
2893
|
},
|
|
2894
|
+
async: {
|
|
2895
|
+
type: "boolean",
|
|
2896
|
+
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.",
|
|
2898
|
+
},
|
|
2899
|
+
roster: {
|
|
2900
|
+
type: "string",
|
|
2901
|
+
pattern: ROSTER_NAME_PATTERN.source,
|
|
2902
|
+
maxLength: 64,
|
|
2903
|
+
description: "Optional named roster preset; exclusive with members, chairman, chairman_model, chairman_effort, and a non-empty variables.cb_delegate.",
|
|
2904
|
+
},
|
|
1075
2905
|
members: {
|
|
1076
2906
|
type: "array",
|
|
1077
|
-
description: "Optional runtime council member composition override. Accepted only for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover). Each entry specifies a CLI adapter
|
|
2907
|
+
description: "Optional runtime council member composition override. Accepted only for council workflows (council-default, council-answer, council-code-review, council-debug, council-discover, council-plan-review). Each entry specifies a CLI adapter, an optional analytical stance, and optional requested model/effort values whose support is adapter-declared. Seats at indices 0-2 may omit stance (inherits default); seats at indices 3-9 MUST provide stance. When provided, both the member stage (stage 0) and reviewer stage (stage 1) are expanded to match the specified count and CLI mapping. Reviewer stances are NOT inherited — reviewers keep their default review lens.",
|
|
1078
2908
|
minItems: 1,
|
|
1079
2909
|
maxItems: 10,
|
|
1080
2910
|
items: {
|
|
@@ -1088,6 +2918,20 @@ const RUN_WORKFLOW_SCHEMA = {
|
|
|
1088
2918
|
type: "string",
|
|
1089
2919
|
description: "Analytical stance or perspective for this council seat. Required for seats at indices 3-9. Server enforces a 200-character limit after whitespace normalization (trim + collapse). Must not contain {{ or }}.",
|
|
1090
2920
|
},
|
|
2921
|
+
model: {
|
|
2922
|
+
type: "string",
|
|
2923
|
+
minLength: 1,
|
|
2924
|
+
maxLength: 128,
|
|
2925
|
+
pattern: MODEL_OVERRIDE_PATTERN.source,
|
|
2926
|
+
description: "Optional requested model for this seat. Support is declared by the selected adapter; omission preserves its configured default.",
|
|
2927
|
+
},
|
|
2928
|
+
effort: {
|
|
2929
|
+
type: "string",
|
|
2930
|
+
minLength: 1,
|
|
2931
|
+
maxLength: 32,
|
|
2932
|
+
pattern: "^[a-z0-9][a-z0-9_-]*$",
|
|
2933
|
+
description: "Optional requested reasoning effort for this seat. Support is declared by the selected adapter; omission preserves its configured default.",
|
|
2934
|
+
},
|
|
1091
2935
|
},
|
|
1092
2936
|
required: ["cli_name"],
|
|
1093
2937
|
additionalProperties: false,
|
|
@@ -1097,6 +2941,21 @@ const RUN_WORKFLOW_SCHEMA = {
|
|
|
1097
2941
|
type: "string",
|
|
1098
2942
|
description: "Optional CLI adapter name override for the council chairman seat. Accepted only for council workflows. Only changes the LLM adapter; the chairman's role and prompt template are preserved.",
|
|
1099
2943
|
},
|
|
2944
|
+
chairman_model: {
|
|
2945
|
+
type: "string",
|
|
2946
|
+
minLength: 1,
|
|
2947
|
+
maxLength: 128,
|
|
2948
|
+
pattern: MODEL_OVERRIDE_PATTERN.source,
|
|
2949
|
+
description: "Optional requested model for the council chairman. Support is declared by the chairman adapter; omission preserves its configured default. May be supplied without chairman.",
|
|
2950
|
+
},
|
|
2951
|
+
chairman_effort: {
|
|
2952
|
+
type: "string",
|
|
2953
|
+
minLength: 1,
|
|
2954
|
+
maxLength: 32,
|
|
2955
|
+
pattern: "^[a-z0-9][a-z0-9_-]*$",
|
|
2956
|
+
description: "Optional requested reasoning effort for the council chairman. Support is declared by the chairman adapter; omission preserves its configured default. May be supplied without chairman.",
|
|
2957
|
+
},
|
|
2958
|
+
plan_review: PLAN_REVIEW_INPUT_SCHEMA,
|
|
1100
2959
|
},
|
|
1101
2960
|
required: ["workflow", "run_id"],
|
|
1102
2961
|
additionalProperties: false,
|
|
@@ -1128,8 +2987,35 @@ const LIST_WORKFLOWS_SCHEMA = {
|
|
|
1128
2987
|
properties: {},
|
|
1129
2988
|
additionalProperties: false,
|
|
1130
2989
|
};
|
|
2990
|
+
const WORKFLOW_RUN_STATUS_SCHEMA = {
|
|
2991
|
+
type: "object",
|
|
2992
|
+
properties: {
|
|
2993
|
+
run_id: {
|
|
2994
|
+
type: "string",
|
|
2995
|
+
pattern: "^[A-Za-z0-9_-]{1,64}$",
|
|
2996
|
+
},
|
|
2997
|
+
include_result: {
|
|
2998
|
+
type: "boolean",
|
|
2999
|
+
default: false,
|
|
3000
|
+
description: "When true, include the generic final result for a succeeded run or the committed plan-review envelope regardless of terminal status. Default false keeps polling body-blind.",
|
|
3001
|
+
},
|
|
3002
|
+
},
|
|
3003
|
+
required: ["run_id"],
|
|
3004
|
+
additionalProperties: false,
|
|
3005
|
+
};
|
|
3006
|
+
const CANCEL_WORKFLOW_SCHEMA = {
|
|
3007
|
+
type: "object",
|
|
3008
|
+
properties: {
|
|
3009
|
+
run_id: {
|
|
3010
|
+
type: "string",
|
|
3011
|
+
pattern: "^[A-Za-z0-9_-]{1,64}$",
|
|
3012
|
+
},
|
|
3013
|
+
},
|
|
3014
|
+
required: ["run_id"],
|
|
3015
|
+
additionalProperties: false,
|
|
3016
|
+
};
|
|
1131
3017
|
/**
|
|
1132
|
-
* Return the
|
|
3018
|
+
* Return the workflow tool definitions for tools/list.
|
|
1133
3019
|
*
|
|
1134
3020
|
* Dynamically discovers available workflows to include in the
|
|
1135
3021
|
* run_workflow description. Safe to call repeatedly — no side effects.
|
|
@@ -1170,6 +3056,22 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
|
|
|
1170
3056
|
}
|
|
1171
3057
|
}
|
|
1172
3058
|
}
|
|
3059
|
+
// plan_review.seats[].cli_name enum (intersection with the v1 provider set)
|
|
3060
|
+
const planReview = props["plan_review"];
|
|
3061
|
+
const planReviewProps = planReview?.["properties"];
|
|
3062
|
+
const planReviewSeats = planReviewProps?.["seats"];
|
|
3063
|
+
const planReviewItems = planReviewSeats?.["items"];
|
|
3064
|
+
const planReviewSeatProps = planReviewItems?.["properties"];
|
|
3065
|
+
const planReviewCliNameSet = new Set(PLAN_REVIEW_V1_CLI_NAMES);
|
|
3066
|
+
const supportedPlanReviewCliNames = sorted.filter((name) => planReviewCliNameSet.has(name));
|
|
3067
|
+
if (planReviewSeatProps?.["cli_name"] != null &&
|
|
3068
|
+
supportedPlanReviewCliNames.length > 0) {
|
|
3069
|
+
planReviewSeatProps["cli_name"] = {
|
|
3070
|
+
...planReviewSeatProps["cli_name"],
|
|
3071
|
+
enum: supportedPlanReviewCliNames,
|
|
3072
|
+
description: `Configured v1 plan-review adapters: ${supportedPlanReviewCliNames.join(", ")}`,
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
1173
3075
|
// chairman enum
|
|
1174
3076
|
if (props["chairman"] != null) {
|
|
1175
3077
|
props["chairman"] = {
|
|
@@ -1212,7 +3114,7 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
|
|
|
1212
3114
|
return [
|
|
1213
3115
|
{
|
|
1214
3116
|
name: "run_workflow",
|
|
1215
|
-
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) to override member/reviewer composition and chairman CLI adapter at runtime.`,
|
|
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.`,
|
|
1216
3118
|
inputSchema: runSchema,
|
|
1217
3119
|
},
|
|
1218
3120
|
{
|
|
@@ -1227,6 +3129,18 @@ export function getWorkflowToolDefinitions(workflowsPath, availableCliNames) {
|
|
|
1227
3129
|
inputSchema: LIST_WORKFLOWS_SCHEMA,
|
|
1228
3130
|
annotations: { readOnlyHint: true },
|
|
1229
3131
|
},
|
|
3132
|
+
{
|
|
3133
|
+
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.",
|
|
3135
|
+
inputSchema: WORKFLOW_RUN_STATUS_SCHEMA,
|
|
3136
|
+
annotations: { readOnlyHint: true },
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
name: "cancel_workflow",
|
|
3140
|
+
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.",
|
|
3141
|
+
inputSchema: CANCEL_WORKFLOW_SCHEMA,
|
|
3142
|
+
annotations: { destructiveHint: true },
|
|
3143
|
+
},
|
|
1230
3144
|
];
|
|
1231
3145
|
}
|
|
1232
3146
|
/** Names of the workflow tools (for dispatch matching). */
|
|
@@ -1234,6 +3148,8 @@ export const WORKFLOW_TOOL_NAMES = new Set([
|
|
|
1234
3148
|
"run_workflow",
|
|
1235
3149
|
"resume_workflow",
|
|
1236
3150
|
"list_workflows",
|
|
3151
|
+
"workflow_run_status",
|
|
3152
|
+
"cancel_workflow",
|
|
1237
3153
|
]);
|
|
1238
3154
|
/**
|
|
1239
3155
|
* Dispatch a workflow tool call.
|
|
@@ -1248,7 +3164,11 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
|
|
|
1248
3164
|
case "resume_workflow":
|
|
1249
3165
|
return await executeResumeWorkflow(server, options, args, signal, progressToken);
|
|
1250
3166
|
case "list_workflows":
|
|
1251
|
-
return executeListWorkflows(options
|
|
3167
|
+
return executeListWorkflows(options);
|
|
3168
|
+
case "workflow_run_status":
|
|
3169
|
+
return await executeWorkflowRunStatus(options, args);
|
|
3170
|
+
case "cancel_workflow":
|
|
3171
|
+
return await executeCancelWorkflow(options, args);
|
|
1252
3172
|
default:
|
|
1253
3173
|
return null;
|
|
1254
3174
|
}
|
|
@@ -1260,7 +3180,8 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
|
|
|
1260
3180
|
* Register workflow tools on an MCP Server instance.
|
|
1261
3181
|
*
|
|
1262
3182
|
* This is the reusable core extracted from createWorkflowServer().
|
|
1263
|
-
* It registers
|
|
3183
|
+
* It registers five tools: run_workflow, resume_workflow, list_workflows,
|
|
3184
|
+
* workflow_run_status, cancel_workflow.
|
|
1264
3185
|
*
|
|
1265
3186
|
* The `options.createBackend` factory determines how clink calls are
|
|
1266
3187
|
* dispatched — callers provide either a child-process or in-process backend.
|
|
@@ -1272,21 +3193,35 @@ export async function handleWorkflowToolCall(server, options, name, args, signal
|
|
|
1272
3193
|
export function registerWorkflowTools(server, options) {
|
|
1273
3194
|
// tools/list
|
|
1274
3195
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1275
|
-
|
|
3196
|
+
activeWorkflowHandlers += 1;
|
|
3197
|
+
try {
|
|
3198
|
+
return { tools: getWorkflowToolDefinitions(options.workflowsPath, options.availableCliNames) };
|
|
3199
|
+
}
|
|
3200
|
+
finally {
|
|
3201
|
+
activeWorkflowHandlers -= 1;
|
|
3202
|
+
notifyWorkflowActivity();
|
|
3203
|
+
}
|
|
1276
3204
|
});
|
|
1277
3205
|
// tools/call
|
|
1278
3206
|
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
3207
|
+
activeWorkflowHandlers += 1;
|
|
3208
|
+
try {
|
|
3209
|
+
const { name, arguments: args } = request.params;
|
|
3210
|
+
const progressToken = request.params._meta?.progressToken;
|
|
3211
|
+
const result = await handleWorkflowToolCall(server, options, name, args ?? {}, extra.signal, progressToken);
|
|
3212
|
+
if (result != null) {
|
|
3213
|
+
return result;
|
|
3214
|
+
}
|
|
3215
|
+
logger.warn({ tool: name }, "unknown tool requested");
|
|
3216
|
+
return {
|
|
3217
|
+
content: [{ type: "text", text: `Unknown tool: ${String(name)}` }],
|
|
3218
|
+
isError: true,
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
finally {
|
|
3222
|
+
activeWorkflowHandlers -= 1;
|
|
3223
|
+
notifyWorkflowActivity();
|
|
1284
3224
|
}
|
|
1285
|
-
logger.warn({ tool: name }, "unknown tool requested");
|
|
1286
|
-
return {
|
|
1287
|
-
content: [{ type: "text", text: `Unknown tool: ${String(name)}` }],
|
|
1288
|
-
isError: true,
|
|
1289
|
-
};
|
|
1290
3225
|
});
|
|
1291
3226
|
}
|
|
1292
3227
|
//# sourceMappingURL=workflow-tools.js.map
|