@geraldmaron/construct 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (556) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1029 -65
  4. package/bin/construct-postinstall.mjs +27 -2
  5. package/config/tag-vocabulary.json +264 -0
  6. package/examples/distribution/sources/deck-one-pager.md +1 -1
  7. package/lib/acp/server.mjs +21 -7
  8. package/lib/adapters-sync.mjs +2 -1
  9. package/lib/audit-trail.mjs +185 -2
  10. package/lib/beads/auto-close.mjs +2 -1
  11. package/lib/beads/drift.mjs +2 -1
  12. package/lib/bridges/copilot-proxy.mjs +20 -5
  13. package/lib/certification/skill-inventory.mjs +10 -1
  14. package/lib/cli/approvals.mjs +147 -0
  15. package/lib/cli-commands.mjs +105 -14
  16. package/lib/comment-lint.mjs +127 -8
  17. package/lib/config/schema.mjs +7 -30
  18. package/lib/config/source-target-registry.mjs +70 -0
  19. package/lib/config/source-targets.mjs +179 -205
  20. package/lib/contracts/coverage.mjs +77 -0
  21. package/lib/contracts/validate.mjs +102 -13
  22. package/lib/contracts/violation-log.mjs +50 -4
  23. package/lib/db/migrate.mjs +69 -0
  24. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  25. package/lib/db/migrations/002_queue_provider.sql +50 -0
  26. package/lib/db/migrations/003_worker_registry.sql +17 -0
  27. package/lib/db/migrations/004_trace_events.sql +16 -0
  28. package/lib/db/migrations/005_shared_memory.sql +15 -0
  29. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  30. package/lib/decisions/enforced-baseline.json +0 -2
  31. package/lib/decisions/registry.mjs +3 -2
  32. package/lib/deployment/parity-contract.mjs +1 -1
  33. package/lib/deployment-mode.mjs +78 -2
  34. package/lib/diagram-export.mjs +10 -1
  35. package/lib/distill.mjs +5 -2
  36. package/lib/docs-verify.mjs +2 -1
  37. package/lib/doctor/cli.mjs +1 -0
  38. package/lib/doctor/command-on-path.mjs +24 -0
  39. package/lib/doctor/diagnosis.mjs +109 -0
  40. package/lib/doctor/embedding-health.mjs +50 -0
  41. package/lib/doctor/engine-health.mjs +93 -0
  42. package/lib/doctor/graph-validate.mjs +47 -0
  43. package/lib/doctor/index.mjs +23 -4
  44. package/lib/doctor/sidecar-providers.mjs +56 -0
  45. package/lib/doctor/watchers/consistency.mjs +93 -1
  46. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  47. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  48. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  49. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  50. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  51. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  52. package/lib/document-export.mjs +52 -27
  53. package/lib/document-extract/docling-client.mjs +9 -5
  54. package/lib/document-extract/docling-sidecar.py +30 -0
  55. package/lib/document-extract.mjs +1 -1
  56. package/lib/document-ingest.mjs +9 -0
  57. package/lib/embed/approval-queue.mjs +184 -88
  58. package/lib/embed/authority-guard.mjs +65 -2
  59. package/lib/embed/capability-jobs.mjs +365 -0
  60. package/lib/embed/capability-lifecycle.mjs +219 -0
  61. package/lib/embed/capability-loader.mjs +303 -0
  62. package/lib/embed/capability-runtime.mjs +58 -0
  63. package/lib/embed/cli.mjs +185 -8
  64. package/lib/embed/config.mjs +4 -0
  65. package/lib/embed/daemon.mjs +125 -6
  66. package/lib/embed/demand-fetch.mjs +190 -54
  67. package/lib/embed/inbox.mjs +12 -10
  68. package/lib/embed/presets/ops-triage.mjs +236 -0
  69. package/lib/embed/presets/pm-feedback.mjs +341 -0
  70. package/lib/embed/presets/tpm.mjs +401 -0
  71. package/lib/embed/providers/jira.mjs +72 -1
  72. package/lib/embed/providers/registry.mjs +140 -33
  73. package/lib/embed/worker.mjs +5 -0
  74. package/lib/embedded-contract/capability.mjs +4 -0
  75. package/lib/embedded-contract/execution.mjs +1 -1
  76. package/lib/embedded-contract/model-resolve.mjs +53 -3
  77. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  78. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  79. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  80. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  81. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  82. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  83. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  84. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  85. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  86. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  88. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  90. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  91. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  92. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  93. package/lib/env-config.mjs +60 -11
  94. package/lib/export-validate.mjs +34 -2
  95. package/lib/extensions/index.mjs +27 -0
  96. package/lib/extensions/loader.mjs +120 -0
  97. package/lib/extensions/manifest-schema.mjs +141 -0
  98. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  99. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  100. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  101. package/lib/extensions/manifests/directory.manifest.json +12 -0
  102. package/lib/extensions/manifests/docling.manifest.json +37 -0
  103. package/lib/extensions/manifests/echo.manifest.json +8 -0
  104. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  105. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  106. package/lib/extensions/manifests/github.manifest.json +52 -0
  107. package/lib/extensions/manifests/linear.manifest.json +50 -0
  108. package/lib/extensions/manifests/local.manifest.json +12 -0
  109. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  110. package/lib/extensions/manifests/openai.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  113. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  114. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  115. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  116. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  117. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  118. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  119. package/lib/extensions/manifests/slack.manifest.json +58 -0
  120. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  121. package/lib/extensions/validate.mjs +175 -0
  122. package/lib/features.mjs +13 -9
  123. package/lib/flows/checkpoint.mjs +184 -0
  124. package/lib/flows/constants.mjs +27 -0
  125. package/lib/flows/define.mjs +146 -0
  126. package/lib/flows/engine.mjs +249 -0
  127. package/lib/flows/errors.mjs +19 -0
  128. package/lib/flows/index.mjs +27 -0
  129. package/lib/flows/joins.mjs +18 -0
  130. package/lib/flows/schema.mjs +90 -0
  131. package/lib/flows/state.mjs +31 -0
  132. package/lib/frameworks/loader.mjs +99 -0
  133. package/lib/frameworks/schema.mjs +157 -0
  134. package/lib/graph/build-from-corpus.mjs +67 -0
  135. package/lib/graph/build-from-embed.mjs +82 -0
  136. package/lib/graph/build-from-registry.mjs +164 -3
  137. package/lib/graph/cli.mjs +456 -12
  138. package/lib/graph/gap-queries.mjs +156 -0
  139. package/lib/graph/gaps.mjs +41 -0
  140. package/lib/graph/impacted.mjs +129 -0
  141. package/lib/graph/runtime-evidence.mjs +177 -0
  142. package/lib/graph/security-coverage.mjs +113 -0
  143. package/lib/graph/staleness.mjs +241 -10
  144. package/lib/graph/store.mjs +24 -7
  145. package/lib/graph/validate.mjs +161 -0
  146. package/lib/headhunt.mjs +9 -8
  147. package/lib/health-check.mjs +15 -7
  148. package/lib/hook-health.mjs +1 -1
  149. package/lib/hooks/agent-tracker.mjs +10 -4
  150. package/lib/hooks/edit-guard.mjs +3 -3
  151. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  152. package/lib/hooks/guard-bash.mjs +41 -15
  153. package/lib/hooks/mcp-health-check.mjs +11 -4
  154. package/lib/hooks/model-fallback.mjs +50 -6
  155. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  156. package/lib/hooks/pre-compact.mjs +181 -173
  157. package/lib/hooks/rule-verifier.mjs +2 -1
  158. package/lib/hooks/session-reflect.mjs +2 -1
  159. package/lib/hooks/session-start.mjs +3 -3
  160. package/lib/host/readiness.mjs +109 -0
  161. package/lib/host-capabilities.mjs +13 -2
  162. package/lib/host-disposition.mjs +29 -8
  163. package/lib/identity.mjs +92 -0
  164. package/lib/ingest/degraded-extract.mjs +96 -0
  165. package/lib/ingest/docling-remote.mjs +2 -1
  166. package/lib/ingest/provider-extract.mjs +7 -19
  167. package/lib/ingest/sidecar-providers.mjs +196 -0
  168. package/lib/ingest-tooling.mjs +11 -5
  169. package/lib/init-unified.mjs +55 -38
  170. package/lib/init-update.mjs +2 -1
  171. package/lib/install/legacy-global-cleanup.mjs +25 -0
  172. package/lib/install/stage-project.mjs +41 -4
  173. package/lib/intake/daemon.mjs +99 -8
  174. package/lib/intake/git-queue.mjs +110 -38
  175. package/lib/intake/prepare.mjs +2 -0
  176. package/lib/intake/queue-registry.mjs +50 -0
  177. package/lib/intake/queue.mjs +133 -17
  178. package/lib/intake/session-prelude.mjs +57 -8
  179. package/lib/integrations/intake-integrations.mjs +61 -111
  180. package/lib/intent-classifier.mjs +43 -5
  181. package/lib/libreoffice-export.mjs +8 -8
  182. package/lib/logging/rotate.mjs +5 -4
  183. package/lib/mcp/broker.mjs +332 -17
  184. package/lib/mcp/denied-store.mjs +95 -0
  185. package/lib/mcp/destructive-approval.mjs +57 -0
  186. package/lib/mcp/destructive-gate.mjs +30 -0
  187. package/lib/mcp/dispatch-envelope.mjs +199 -0
  188. package/lib/mcp/memory-bridge.mjs +2 -1
  189. package/lib/mcp/server.mjs +185 -1268
  190. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  191. package/lib/mcp/tool-definitions-project.mjs +271 -0
  192. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  193. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  194. package/lib/mcp/tool-definitions.mjs +25 -0
  195. package/lib/mcp/tool-rate-limit.mjs +47 -0
  196. package/lib/mcp/tool-registry.mjs +107 -0
  197. package/lib/mcp/tool-safety.mjs +95 -0
  198. package/lib/mcp/tool-surface-parity.mjs +60 -0
  199. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  200. package/lib/mcp/tools/orchestration-run.mjs +197 -19
  201. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  202. package/lib/mcp/tools/project.mjs +25 -8
  203. package/lib/mcp/tools/provider-write.mjs +187 -0
  204. package/lib/mcp/tools/scope.mjs +0 -3
  205. package/lib/mcp/tools/skills.mjs +1 -1
  206. package/lib/mcp/tools/storage.mjs +2 -2
  207. package/lib/mcp/tools/web-search-governance.mjs +96 -0
  208. package/lib/mcp/tools/web-search.mjs +5 -76
  209. package/lib/mcp/transport/auth.mjs +238 -0
  210. package/lib/mcp/transport/http.mjs +136 -0
  211. package/lib/mcp/transport/mode.mjs +31 -0
  212. package/lib/mcp/transport/stdio.mjs +22 -0
  213. package/lib/mcp-catalog.json +4 -4
  214. package/lib/mcp-manager.mjs +34 -23
  215. package/lib/mcp-platform-config.mjs +53 -20
  216. package/lib/mode-capabilities.mjs +109 -0
  217. package/lib/model-router.mjs +42 -9
  218. package/lib/models/catalog.mjs +40 -1
  219. package/lib/net-guard.mjs +247 -0
  220. package/lib/observation-store.mjs +6 -2
  221. package/lib/ollama/provision-context.mjs +2 -1
  222. package/lib/opencode-config.mjs +22 -3
  223. package/lib/opencode-runtime-plugin.mjs +120 -2
  224. package/lib/oracle/actions.mjs +204 -10
  225. package/lib/oracle/cli.mjs +24 -3
  226. package/lib/oracle/daemon-entry.mjs +6 -0
  227. package/lib/oracle/dispatch.mjs +2 -1
  228. package/lib/oracle/execute.mjs +2 -2
  229. package/lib/oracle/gaps.mjs +3 -3
  230. package/lib/oracle/heartbeat.mjs +53 -0
  231. package/lib/oracle/read-model.mjs +69 -13
  232. package/lib/oracle/reconcile.mjs +3 -46
  233. package/lib/oracle/routing.mjs +37 -31
  234. package/lib/oracle/synthesize.mjs +1 -1
  235. package/lib/orchestration/classification.mjs +434 -0
  236. package/lib/orchestration/delegation-flow.mjs +132 -0
  237. package/lib/orchestration/flow-selection.mjs +418 -0
  238. package/lib/orchestration/gates.mjs +244 -0
  239. package/lib/orchestration/host-sampling.mjs +121 -0
  240. package/lib/orchestration/policy-constants.mjs +48 -0
  241. package/lib/orchestration/provider-outcome.mjs +197 -0
  242. package/lib/orchestration/readiness.mjs +114 -13
  243. package/lib/orchestration/run-store-postgres.mjs +32 -26
  244. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  245. package/lib/orchestration/run-store.mjs +25 -12
  246. package/lib/orchestration/runtime.mjs +508 -62
  247. package/lib/orchestration/store.mjs +87 -12
  248. package/lib/orchestration/trace-store.mjs +125 -0
  249. package/lib/orchestration/web-capability.mjs +60 -0
  250. package/lib/orchestration/worker-runtime.mjs +162 -0
  251. package/lib/orchestration/worker.mjs +763 -80
  252. package/lib/orchestration-policy.mjs +67 -1111
  253. package/lib/output-quality.mjs +61 -2
  254. package/lib/packs/cli.mjs +144 -0
  255. package/lib/packs/core-pack.mjs +112 -0
  256. package/lib/packs/enablement.mjs +187 -0
  257. package/lib/packs/index.mjs +23 -0
  258. package/lib/packs/loader.mjs +176 -0
  259. package/lib/packs/manifest-schema.mjs +58 -0
  260. package/lib/packs/prompts.mjs +120 -0
  261. package/lib/packs/validate.mjs +208 -0
  262. package/lib/path-policy.mjs +56 -0
  263. package/lib/plugin-registry.mjs +4 -5
  264. package/lib/policy/audit-gate.mjs +42 -0
  265. package/lib/policy/consumption-budget.mjs +149 -0
  266. package/lib/policy/engine.mjs +81 -6
  267. package/lib/policy/role-authority.mjs +89 -0
  268. package/lib/prompt-composer.js +19 -3
  269. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  270. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  271. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  272. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  273. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  274. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  275. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  276. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  277. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  278. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  279. package/lib/providers/contract.mjs +207 -0
  280. package/lib/providers/credential-bootstrap.mjs +7 -4
  281. package/lib/providers/credential-sources.mjs +14 -2
  282. package/lib/providers/directory/index.mjs +261 -0
  283. package/lib/providers/feedback/index.mjs +392 -0
  284. package/lib/providers/filter-audit.mjs +68 -0
  285. package/lib/providers/filter-schema.mjs +54 -0
  286. package/lib/providers/github/index.mjs +31 -0
  287. package/lib/providers/instance-config.mjs +215 -0
  288. package/lib/providers/op-locate.mjs +96 -0
  289. package/lib/providers/op-run.mjs +64 -9
  290. package/lib/providers/registry.mjs +26 -3
  291. package/lib/providers/secret-audit-wiring.mjs +41 -18
  292. package/lib/providers/secret-resolver.mjs +263 -31
  293. package/lib/publish-template.mjs +6 -3
  294. package/lib/publish-tooling.mjs +4 -0
  295. package/lib/publish.mjs +32 -4
  296. package/lib/queue/pg-queue.mjs +395 -0
  297. package/lib/reflect.mjs +2 -1
  298. package/lib/registry/assemble.mjs +28 -2
  299. package/lib/registry/catalog.mjs +6 -0
  300. package/lib/registry/consolidation.mjs +8 -1
  301. package/lib/registry/custom-scaffold.mjs +200 -0
  302. package/lib/registry/custom-schema.mjs +137 -0
  303. package/lib/registry/loader.mjs +15 -4
  304. package/lib/registry/manifests/format-engines.default.json +15 -0
  305. package/lib/registry/manifests/surface-map.default.json +46 -0
  306. package/lib/registry/retired-paths.mjs +1 -0
  307. package/lib/registry/surface-map.mjs +100 -50
  308. package/lib/registry/validate.mjs +3 -3
  309. package/lib/render-visual-check.mjs +240 -0
  310. package/lib/resources/budget.mjs +40 -17
  311. package/lib/roles/flavor-bindings.mjs +36 -14
  312. package/lib/roles/gateway.mjs +15 -8
  313. package/lib/roots.mjs +107 -0
  314. package/lib/runtime/uv-bootstrap.mjs +32 -6
  315. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  316. package/lib/runtime-env.mjs +5 -2
  317. package/lib/sandbox.mjs +1 -1
  318. package/lib/scheduler/index.mjs +8 -20
  319. package/lib/schema-infer.mjs +31 -2
  320. package/lib/scopes/lifecycle.mjs +29 -25
  321. package/lib/scopes/loader.mjs +49 -12
  322. package/lib/scopes/teams.mjs +1 -1
  323. package/lib/security/ingest-boundary.mjs +80 -0
  324. package/lib/security/recall-wrapper.mjs +99 -0
  325. package/lib/security/trust.mjs +132 -0
  326. package/lib/service-manager.mjs +93 -24
  327. package/lib/setup.mjs +41 -23
  328. package/lib/skills-apply.mjs +4 -2
  329. package/lib/specialists/postconditions.mjs +2 -2
  330. package/lib/state-root.mjs +147 -0
  331. package/lib/status.mjs +597 -6
  332. package/lib/storage/admin.mjs +84 -7
  333. package/lib/storage/backend-registry.mjs +73 -0
  334. package/lib/storage/backend.mjs +63 -9
  335. package/lib/storage/embeddings-openai.mjs +12 -2
  336. package/lib/storage/hybrid-query.mjs +11 -3
  337. package/lib/storage/retrieval-hardening.mjs +384 -0
  338. package/lib/storage/shared-memory.mjs +158 -0
  339. package/lib/storage/vector-client.mjs +69 -2
  340. package/lib/team/health.mjs +72 -0
  341. package/lib/telemetry/backends/local.mjs +9 -3
  342. package/lib/telemetry/client.mjs +3 -7
  343. package/lib/template-registry.mjs +10 -12
  344. package/lib/tenant/context.mjs +82 -0
  345. package/lib/tenant/isolation.mjs +129 -0
  346. package/lib/test-corpus-inventory.mjs +103 -2
  347. package/lib/uninstall/uninstall.mjs +78 -12
  348. package/lib/validator.mjs +4 -6
  349. package/lib/worker/entrypoint.mjs +2 -1
  350. package/lib/worker/run.mjs +2 -2
  351. package/lib/worker/trace.mjs +5 -11
  352. package/lib/workflow-state.mjs +52 -8
  353. package/lib/workflows/liveness.mjs +203 -0
  354. package/lib/workflows/loader.mjs +177 -0
  355. package/lib/workflows/manifest-schema.mjs +71 -0
  356. package/lib/workflows/surface-parity.mjs +118 -0
  357. package/lib/workflows/validate.mjs +127 -0
  358. package/lib/writes/control-plane.mjs +140 -0
  359. package/lib/writes/envelope.mjs +206 -0
  360. package/lib/writes/sent-log.mjs +86 -0
  361. package/lib/writes/write-intent.mjs +109 -0
  362. package/package.json +20 -7
  363. package/personas/construct.md +12 -3
  364. package/registry/agent-manifest.json +117 -0
  365. package/registry/capabilities.json +1987 -0
  366. package/rules/common/comments.md +1 -0
  367. package/schemas/brand-voice.schema.json +24 -0
  368. package/schemas/capability-registry.schema.json +72 -0
  369. package/schemas/certification-run.schema.json +130 -0
  370. package/schemas/demo-recording.schema.json +46 -0
  371. package/schemas/eval-dataset.schema.json +79 -0
  372. package/schemas/execution-capability-profile.schema.json +46 -0
  373. package/schemas/execution-policy.schema.json +114 -0
  374. package/schemas/improvement-proposal.schema.json +65 -0
  375. package/schemas/mcp-tool-output.schema.json +61 -0
  376. package/schemas/platform-capabilities.schema.json +83 -0
  377. package/schemas/project-config.schema.json +215 -0
  378. package/schemas/project-demo.schema.json +60 -0
  379. package/schemas/provider-behavior-matrix.schema.json +91 -0
  380. package/schemas/scope.schema.json +197 -0
  381. package/schemas/specialist-trace.schema.json +107 -0
  382. package/schemas/team.schema.json +99 -0
  383. package/schemas/unified-registry.schema.json +546 -0
  384. package/scripts/sync-specialists.mjs +336 -106
  385. package/skills/docs/adr-workflow.md +1 -1
  386. package/skills/docs/codebase-research-workflow.md +3 -3
  387. package/skills/docs/prd-workflow.md +5 -6
  388. package/skills/docs/runbook-workflow.md +2 -2
  389. package/skills/docs/user-research-workflow.md +3 -3
  390. package/skills/operating/fleet-health-routing.md +77 -0
  391. package/skills/roles/ai-engineer.md +1 -1
  392. package/skills/roles/architect.md +0 -1
  393. package/skills/roles/business-strategist.md +1 -1
  394. package/skills/roles/data-analyst.telemetry.md +1 -1
  395. package/skills/roles/data-engineer.md +1 -1
  396. package/skills/roles/data-engineer.pipeline.md +1 -1
  397. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  398. package/skills/roles/data-engineer.warehouse.md +1 -1
  399. package/skills/roles/designer.accessibility.md +1 -1
  400. package/skills/roles/designer.md +0 -1
  401. package/skills/roles/devil-advocate.md +1 -1
  402. package/skills/roles/docs-keeper.md +1 -1
  403. package/skills/roles/evaluator.md +1 -1
  404. package/skills/roles/explorer.md +1 -1
  405. package/skills/roles/platform-engineer.md +1 -1
  406. package/skills/roles/qa.ai-eval.md +1 -2
  407. package/skills/roles/qa.api-contract.md +0 -1
  408. package/skills/roles/qa.data-pipeline.md +0 -1
  409. package/skills/roles/qa.md +0 -1
  410. package/skills/roles/qa.web-ui.md +0 -1
  411. package/skills/roles/release-manager.md +1 -1
  412. package/skills/roles/researcher.md +0 -2
  413. package/skills/roles/reviewer.md +0 -3
  414. package/skills/roles/security.ai.md +1 -1
  415. package/skills/roles/security.legal-compliance.md +1 -1
  416. package/skills/roles/security.md +0 -1
  417. package/skills/roles/security.privacy.md +0 -1
  418. package/skills/roles/security.supply-chain.md +1 -1
  419. package/skills/roles/sre.md +1 -1
  420. package/skills/roles/test-automation.md +1 -1
  421. package/skills/roles/trace-reviewer.md +1 -1
  422. package/skills/roles/ux-researcher.md +1 -1
  423. package/specialists/artifact-manifest.json +36 -36
  424. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  425. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  426. package/specialists/org/contracts/any-to-debugger.json +7 -1
  427. package/specialists/org/contracts/any-to-designer.json +7 -1
  428. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  429. package/specialists/org/contracts/any-to-explorer.json +13 -4
  430. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  431. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  432. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  433. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  434. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  435. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  436. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  437. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  438. package/specialists/org/contracts/architect-to-operations.json +17 -4
  439. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  440. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  441. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  442. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  443. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  444. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  445. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  446. package/specialists/org/contracts/operations-to-user.json +53 -0
  447. package/specialists/org/contracts/operations-triage-output.json +53 -0
  448. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  449. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  450. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  451. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  452. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  453. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  454. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  455. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  456. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  457. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  458. package/specialists/org/contracts/user-to-construct.json +11 -4
  459. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  460. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  461. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  462. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  463. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  464. package/specialists/org/groups/engineering-group.json +2 -6
  465. package/specialists/org/groups/governance-group.json +2 -3
  466. package/specialists/org/groups/operations-group.json +5 -8
  467. package/specialists/org/groups/product-group.json +4 -8
  468. package/specialists/org/groups/quality-group.json +3 -7
  469. package/specialists/org/groups/strategy-group.json +6 -9
  470. package/specialists/org/models.json.example +8 -0
  471. package/specialists/org/scopes/creative.json +6 -1
  472. package/specialists/org/scopes/operations.json +7 -1
  473. package/specialists/org/scopes/research.json +6 -1
  474. package/specialists/org/scopes/rnd.json +7 -1
  475. package/specialists/org/specialists/cx-architect.json +27 -8
  476. package/specialists/org/specialists/cx-designer.json +22 -8
  477. package/specialists/org/specialists/cx-engineer.json +71 -7
  478. package/specialists/org/specialists/cx-operations.json +89 -15
  479. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  480. package/specialists/org/specialists/cx-product-manager.json +28 -9
  481. package/specialists/org/specialists/cx-qa.json +16 -6
  482. package/specialists/org/specialists/cx-researcher.json +41 -7
  483. package/specialists/org/specialists/cx-reviewer.json +61 -11
  484. package/specialists/org/specialists/cx-security.json +30 -10
  485. package/specialists/org/teams/design-team.json +3 -4
  486. package/specialists/org/teams/engineering-team.json +3 -10
  487. package/specialists/org/teams/governance-team.json +3 -5
  488. package/specialists/org/teams/operations-team.json +6 -12
  489. package/specialists/org/teams/product-management-team.json +2 -3
  490. package/specialists/org/teams/quality-team.json +4 -12
  491. package/specialists/org/teams/research-team.json +3 -3
  492. package/specialists/org/teams/strategy-team.json +7 -14
  493. package/specialists/prompts/cx-architect.md +20 -1
  494. package/specialists/prompts/cx-data-analyst.md +1 -1
  495. package/specialists/prompts/cx-designer.md +12 -4
  496. package/specialists/prompts/cx-engineer.md +15 -1
  497. package/specialists/prompts/cx-operations.md +14 -2
  498. package/specialists/prompts/cx-orchestrator.md +9 -5
  499. package/specialists/prompts/cx-product-manager.md +5 -1
  500. package/specialists/prompts/cx-qa.md +6 -2
  501. package/specialists/prompts/cx-researcher.md +27 -31
  502. package/specialists/prompts/cx-reviewer.md +19 -1
  503. package/specialists/prompts/cx-security.md +5 -1
  504. package/templates/distribution/construct-brand.typ +123 -59
  505. package/templates/distribution/construct-decision.typ +6 -3
  506. package/templates/distribution/construct-pdf.typ +11 -4
  507. package/templates/distribution/construct-prd.typ +6 -3
  508. package/templates/distribution/construct-research.typ +7 -4
  509. package/vendor/pandoc-ext/README.md +3 -0
  510. package/vendor/pandoc-ext/diagram.lua +687 -0
  511. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  512. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  513. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  514. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  515. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  516. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  517. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  518. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  519. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  520. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  521. package/specialists/org/specialists/cx-accessibility.json +0 -69
  522. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  523. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  524. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  525. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  526. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  527. package/specialists/org/specialists/cx-evaluator.json +0 -69
  528. package/specialists/org/specialists/cx-explorer.json +0 -69
  529. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  530. package/specialists/org/specialists/cx-oracle.json +0 -46
  531. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  532. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  533. package/specialists/org/specialists/cx-release-manager.json +0 -77
  534. package/specialists/org/specialists/cx-sre.json +0 -94
  535. package/specialists/org/specialists/cx-test-automation.json +0 -69
  536. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  537. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  538. package/specialists/org/teams/accessibility-team.json +0 -39
  539. package/specialists/org/teams/ux-research-team.json +0 -39
  540. package/specialists/prompts/cx-accessibility.md +0 -55
  541. package/specialists/prompts/cx-ai-engineer.md +0 -124
  542. package/specialists/prompts/cx-business-strategist.md +0 -58
  543. package/specialists/prompts/cx-data-engineer.md +0 -55
  544. package/specialists/prompts/cx-devil-advocate.md +0 -59
  545. package/specialists/prompts/cx-docs-keeper.md +0 -164
  546. package/specialists/prompts/cx-evaluator.md +0 -49
  547. package/specialists/prompts/cx-explorer.md +0 -71
  548. package/specialists/prompts/cx-legal-compliance.md +0 -58
  549. package/specialists/prompts/cx-oracle.md +0 -98
  550. package/specialists/prompts/cx-platform-engineer.md +0 -97
  551. package/specialists/prompts/cx-rd-lead.md +0 -59
  552. package/specialists/prompts/cx-release-manager.md +0 -54
  553. package/specialists/prompts/cx-sre.md +0 -111
  554. package/specialists/prompts/cx-test-automation.md +0 -55
  555. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  556. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -27,19 +27,41 @@
27
27
  * mistake a prepared task for executed specialist output. When the execution
28
28
  * contract resolves to prompt-only or host-direct, Construct owns no specialist
29
29
  * task sequence and the run records that explicitly.
30
+ *
31
+ * Tenant propagation (ADR-0057/A7, LMCP-H1): resolveTenantContext runs once
32
+ * per planRun/executeRun call and its tenantId rides the run record, every
33
+ * task, and every emitted trace/run event. Enterprise mode with no
34
+ * resolvable tenant throws TenantResolutionError — planning and resuming a
35
+ * run both fail closed rather than persisting an unlabeled run.
36
+ *
37
+ * Run-level executionState (LMCP-F4): executeRun aggregates every task's
38
+ * LMCP-F1 executionState (prepared|executed|degraded-executed|failed) into one
39
+ * `run.executionState` via aggregateExecutionState, so a caller (construct
40
+ * status, an MCP host) never has to walk run.tasks to learn whether a run only
41
+ * prepared work or actually executed it. Precedence: failed >
42
+ * degraded-executed > executed > prepared; a zero-task run (prompt-only,
43
+ * host-direct) aggregates to null rather than fabricating a state for work
44
+ * that never happened.
30
45
  */
31
46
 
32
47
  import { randomBytes } from 'node:crypto';
48
+ import { writeFileSync } from 'node:fs';
49
+ import { join } from 'node:path';
33
50
 
34
51
  import { routeRequest } from '../orchestration-policy.mjs';
35
52
  import { resolveExecution } from '../embedded-contract/execution.mjs';
36
53
  import { detectHostCapabilities } from '../host-capabilities.mjs';
37
54
  import { loadProjectConfig } from '../config/project-config.mjs';
38
- import { CHAIN_OF_THOUGHT_MODES } from '../config/schema.mjs';
55
+ import { CHAIN_OF_THOUGHT_MODES, DEFAULT_PROJECT_CONFIG } from '../config/schema.mjs';
39
56
  import { newTraceId, newSpanId, emitTraceEvent } from '../worker/trace.mjs';
40
- import { resolveRunStore } from './store.mjs';
41
- import { runTaskViaProvider, INLINE, PROVIDER, WORKER_BACKEND_SET } from './worker.mjs';
57
+ import { resolveStateDir } from '../state-root.mjs';
58
+ import { resolveRunStore, projectKey } from './store.mjs';
59
+ import { resolveTraceStore } from './trace-store.mjs';
60
+ import { runTaskViaProvider, materializeTaskPrompt, INLINE, PROVIDER, HOST, WORKER_BACKEND_SET } from './worker.mjs';
61
+ import { roleHoldsWebCapability } from './web-capability.mjs';
42
62
  import { emitRunEvent, isCancelRequested, clearCancel } from './events.mjs';
63
+ import { getDeploymentMode } from '../deployment-mode.mjs';
64
+ import { resolveTenantContext } from '../tenant/context.mjs';
43
65
 
44
66
  export const WORKER_BACKENDS = WORKER_BACKEND_SET;
45
67
  export const DEFAULT_WORKER_BACKEND = INLINE;
@@ -85,7 +107,7 @@ function pickExecution(data) {
85
107
  };
86
108
  }
87
109
 
88
- function buildTasks(route) {
110
+ function buildTasks(route, tenantId) {
89
111
  const reasons = route.dispatchReasons || {};
90
112
  const handoffByProducer = new Map();
91
113
  for (const edge of route.contractChain || []) {
@@ -96,6 +118,7 @@ function buildTasks(route) {
96
118
  id: `t${i + 1}`,
97
119
  seq: i,
98
120
  role,
121
+ tenantId,
99
122
  reason: reasons[role] || null,
100
123
  handoffContract: handoffByProducer.get(role.replace(/^cx-/, '')) || null,
101
124
  status: 'queued',
@@ -108,11 +131,26 @@ function buildTasks(route) {
108
131
  }));
109
132
  }
110
133
 
134
+ // Best-effort mirror of a local trace event into the team-shared trace store
135
+ // (LMCP-G10): a no-op in solo mode or when Postgres is unreachable
136
+ // (resolveTraceStore already encodes that fallback), and never allowed to
137
+ // fail the run itself — the local `.cx/traces` JSONL emitTraceEvent already
138
+ // wrote is the durable record either way.
139
+
140
+ function persistTeamTrace({ cwd, env, config, event, tenantId }) {
141
+ if (!event) return;
142
+ try {
143
+ const teamStore = resolveTraceStore({ env, cwd, config });
144
+ if (teamStore.kind !== 'postgres') return;
145
+ teamStore.saveTraceEvent(event, { project: projectKey(config, cwd), tenantId }).catch(() => {});
146
+ } catch { /* observability must not break the caller — see header comment */ }
147
+ }
148
+
111
149
  // The worker backend is a deployment concern: an explicit option wins, then the
112
150
  // run's recorded backend, then config (orchestration.workerBackend), then the
113
151
  // inline default. An unknown value falls back to inline rather than guessing.
114
152
 
115
- function resolveWorkerBackend({ explicit, run, config }) {
153
+ export function resolveWorkerBackend({ explicit, run, config }) {
116
154
  const candidate = explicit || run?.workerBackend || config?.orchestration?.workerBackend || DEFAULT_WORKER_BACKEND;
117
155
  return WORKER_BACKENDS.includes(candidate) ? candidate : DEFAULT_WORKER_BACKEND;
118
156
  }
@@ -127,14 +165,32 @@ function resolveChainOfThought({ explicit, run, config }) {
127
165
  return CHAIN_OF_THOUGHT.includes(candidate) ? candidate : DEFAULT_CHAIN_OF_THOUGHT;
128
166
  }
129
167
 
130
- function loadConfig(cwd, env) {
168
+ // loadProjectConfig reverts the whole file to defaults on any validation
169
+ // error (ADR-0021's fail-safe stance); loadConfig historically discarded the
170
+ // errors/source that reversion carries, so a typo (e.g. store: "sqllite")
171
+ // silently flipped workerBackend back to inline with no user-visible signal.
172
+ // configWarnings names the offending key/value and states defaults were
173
+ // applied, mirrored into planRun's warnings and construct doctor
174
+ // (construct-uccl.5).
175
+
176
+ function loadConfigWithWarnings(cwd, env) {
131
177
  try {
132
- return loadProjectConfig(cwd, env).config || {};
178
+ const loaded = loadProjectConfig(cwd, env);
179
+ const configWarnings = loaded.source === 'invalid'
180
+ ? loaded.errors.map(
181
+ (e) => `construct.config.json invalid (${e}) — reverted to defaults (workerBackend=${DEFAULT_PROJECT_CONFIG.orchestration.workerBackend}). Fix construct.config.json to keep your settings.`,
182
+ )
183
+ : [];
184
+ return { config: loaded.config || {}, configWarnings };
133
185
  } catch {
134
- return {};
186
+ return { config: {}, configWarnings: [] };
135
187
  }
136
188
  }
137
189
 
190
+ function loadConfig(cwd, env) {
191
+ return loadConfigWithWarnings(cwd, env).config;
192
+ }
193
+
138
194
  /**
139
195
  * Plan a run: resolve the execution contract, decompose into a specialist chain,
140
196
  * and persist a durable run record (status `planned`). Pure of model calls.
@@ -150,8 +206,17 @@ export async function planRun(request = {}, { env = process.env, cwd = process.c
150
206
  workerBackend: explicitWorkerBackend = null,
151
207
  } = request;
152
208
 
153
- const config = loadConfig(cwd, env);
154
- const { store, warnings: storeWarnings } = resolveRunStore({ config, env, cwd });
209
+ const { config, configWarnings } = loadConfigWithWarnings(cwd, env);
210
+ const { store, warnings: storeWarnings, degraded: storeDegraded, degradedReason: storeDegradedReason } = resolveRunStore({ config, env, cwd });
211
+ if (storeDegraded) {
212
+ process.stderr.write(`[construct] orchestration store degraded (${storeDegradedReason ?? 'unknown'}): ${storeWarnings.join('; ')}\n`);
213
+ }
214
+
215
+ // ADR-0057 (A7): tenant context is resolved once per run, here, and rides
216
+ // every run/task record below. Enterprise mode with no resolvable tenant
217
+ // throws TenantResolutionError — planRun fails closed rather than planning
218
+ // a run with a null tenant label.
219
+ const { tenantId } = resolveTenantContext({ env, config, mode: getDeploymentMode(env, { cwd }) });
155
220
 
156
221
  const execData = resolveExecution(
157
222
  { workflowType, requestedStrategy, useConstruct, host, hostModel, hostProvider },
@@ -169,11 +234,12 @@ export async function planRun(request = {}, { env = process.env, cwd = process.c
169
234
  // orchestrated execution; prompt-only and host-direct own none, and the run
170
235
  // records that rather than implying orchestration that did not happen.
171
236
  const orchestrates = execData.effectiveStrategy === 'orchestrated';
172
- const tasks = orchestrates ? buildTasks(route) : [];
237
+ const tasks = orchestrates ? buildTasks(route, tenantId) : [];
173
238
 
174
239
  const run = {
175
240
  runId,
176
241
  traceId,
242
+ tenantId,
177
243
  createdAt: now,
178
244
  updatedAt: now,
179
245
  request: { summary: truncate(text, 200), workflowType, requestedStrategy },
@@ -192,7 +258,7 @@ export async function planRun(request = {}, { env = process.env, cwd = process.c
192
258
  },
193
259
  tasks,
194
260
  status: 'planned',
195
- warnings: [...(execData.warnings || []), ...storeWarnings],
261
+ warnings: [...(execData.warnings || []), ...storeWarnings, ...configWarnings],
196
262
  semantics: RUNTIME_SEMANTICS,
197
263
  executionSemantics: execData.semantics,
198
264
  };
@@ -200,9 +266,9 @@ export async function planRun(request = {}, { env = process.env, cwd = process.c
200
266
  await store.saveRun(run);
201
267
  emitTraceEvent({
202
268
  rootDir: cwd, env, traceId, spanId: newSpanId(), eventType: 'task_graph.created',
203
- metadata: { runId, executionMode: run.execution.executionMode, specialists: run.plan.specialists, workerBackend: run.workerBackend },
269
+ metadata: { runId, tenantId, executionMode: run.execution.executionMode, specialists: run.plan.specialists, workerBackend: run.workerBackend },
204
270
  });
205
- emitRunEvent(runId, { type: 'planned', status: 'planned', executionMode: run.execution.executionMode, taskCount: tasks.length });
271
+ emitRunEvent(runId, { type: 'planned', status: 'planned', tenantId, executionMode: run.execution.executionMode, taskCount: tasks.length });
206
272
  return run;
207
273
  }
208
274
 
@@ -213,38 +279,232 @@ function prepareTaskInline(task) {
213
279
  task.executor = 'inline:prepared';
214
280
  task.status = 'prepared';
215
281
  task.finishedAt = new Date().toISOString();
282
+ // The inline backend never resolves a persona or calls a model (ADR-0020), so
283
+ // packId/promptVersion/model/provider/toolGrants stay unset — there is nothing
284
+ // real to report yet. executionState still carries the honest signal: this task
285
+ // was prepared, not executed (LMCP-F1/F4).
286
+ task.executionState = 'prepared';
287
+ // A prepared web-capable task reached no web: mark it so a host never infers live
288
+ // web access from a task that only planned (ADR-0050).
289
+ if (roleHoldsWebCapability(task.role)) task.webCapability = 'prepare-only';
216
290
  }
217
291
 
218
292
  // The provider backend executes one task against the configured model. A failed
219
293
  // task is recorded (status `failed`, task.error) and does not abort the run, so
220
294
  // one specialist failure cannot lose the work of the others.
221
295
 
222
- async function executeTaskViaProvider(task, run, env, fetchImpl, chainOfThought) {
296
+ async function executeTaskViaProvider(task, run, env, fetchImpl, chainOfThought, cwd) {
223
297
  try {
224
298
  const result = await runTaskViaProvider({
225
299
  task, run,
226
300
  model: run.execution?.selectedModel,
227
301
  provider: run.execution?.selectedProvider,
228
- env, fetchImpl, chainOfThought,
302
+ env, fetchImpl, chainOfThought, cwd,
229
303
  });
230
304
  task.output = result.output;
231
305
  task.executor = `provider:${result.provider}:${result.model}`;
232
306
  task.status = 'done';
233
307
  task.finishedAt = new Date().toISOString();
234
308
 
309
+ // Web-capable execution records which grant mode reached (or did not reach) the web
310
+ // and the F08-governed evidence gathered, so a host never has to infer it (ADR-0050).
311
+ if (result.webCapability) {
312
+ task.webCapability = result.webCapability;
313
+ task.webEvidence = result.webEvidence || [];
314
+ task.webCalls = result.webCalls || 0;
315
+ task.webSearchRequests = result.webSearchRequests || 0;
316
+ }
317
+
318
+ // Evidence grounding (construct-5wkl AC#5): a citation the specialist wrote
319
+ // but that never appeared in its own governed webEvidence is unverified —
320
+ // the task still completes (real output, already paid for) but carries the
321
+ // warning rather than looking identical to a fully-grounded answer.
322
+ if (result.evidenceStatus) {
323
+ task.evidenceStatus = result.evidenceStatus;
324
+ task.unverifiedCitations = result.unverifiedCitations || [];
325
+ }
326
+
327
+ // Provider telemetry (construct-5wkl AC#3): redacted, machine-readable
328
+ // execution metadata — provider, model, finish_reason, usage, elapsedMs,
329
+ // retryCount, and reasoning request/return — so a debugging pass never has
330
+ // to re-run the task to learn why it behaved the way it did.
331
+ if (result.providerMeta) task.providerMeta = result.providerMeta;
332
+
333
+ // Persona resolution (LMCP-E2): personaAvailable rides on every task so a
334
+ // host never has to infer whether the specialist ran under its real prompt
335
+ // or the solo-mode fallback; degraded is set only on the fallback path.
336
+ task.personaAvailable = result.personaAvailable;
337
+ if (result.degraded) task.degraded = result.degraded;
338
+
339
+ // Execution provenance (LMCP-F1): which specialist/pack/prompt/model/provider
340
+ // actually ran and under which tool grants — the basis for audit and
341
+ // evaluation. Rides on every provider-executed task, not gated on
342
+ // chainOfThought, same as personaAvailable above.
343
+ task.specialistId = result.specialistId;
344
+ task.packId = result.packId;
345
+ task.promptVersion = result.promptVersion;
346
+ task.model = result.model;
347
+ task.provider = result.provider;
348
+ task.toolGrants = result.toolGrants || [];
349
+ task.executionState = result.executionState;
350
+
235
351
  // `surface` attaches reasoning to the task so every display surface renders
236
352
  // it; `telemetry_only` keeps it off the task (recorded only in the trace).
237
353
  if (chainOfThought === 'surface' && result.reasoning) task.reasoning = result.reasoning;
238
- return { ok: true, reasoning: result.reasoning || '' };
354
+ return {
355
+ ok: true,
356
+ reasoning: result.reasoning || '',
357
+ webCapability: result.webCapability || null,
358
+ personaAvailable: result.personaAvailable,
359
+ personaDegraded: Boolean(result.degraded),
360
+ specialistId: result.specialistId,
361
+ packId: result.packId,
362
+ promptVersion: result.promptVersion,
363
+ model: result.model,
364
+ provider: result.provider,
365
+ toolGrants: result.toolGrants || [],
366
+ executionState: result.executionState,
367
+ };
239
368
  } catch (err) {
240
369
  task.executor = `provider:error`;
241
- task.error = { code: err.code || 'PROVIDER_EXECUTION_FAILED', message: err.message };
370
+ task.error = {
371
+ code: err.code || 'PROVIDER_EXECUTION_FAILED',
372
+ message: err.message,
373
+ ...(err.remediation ? { remediation: err.remediation } : {}),
374
+ ...(Number.isFinite(err.retryCount) ? { retryCount: err.retryCount } : {}),
375
+ };
376
+ task.status = 'failed';
377
+ task.finishedAt = new Date().toISOString();
378
+ // A refused/failed provider call never resolved a real specialist/pack/prompt to
379
+ // report — executionState is the one honest signal available here (LMCP-F1/F4).
380
+ task.executionState = 'failed';
381
+ // A content-shaped failure (empty/reasoning-only/content-filtered/malformed)
382
+ // still reached a real HTTP response, so its redacted provider metadata
383
+ // (construct-5wkl AC#3) is worth keeping even though the task failed.
384
+ if (err.providerMeta) task.providerMeta = err.providerMeta;
385
+ return { ok: false, reasoning: '', executionState: 'failed' };
386
+ }
387
+ }
388
+
389
+ // The host backend materializes one task's prompt (same materializeTaskPrompt the
390
+ // provider executor uses) and stops — no model call runs here. The calling host
391
+ // (the MCP client that requested the run) executes the prompt in its own session
392
+ // and reports the result back through submitHostTaskResult. A materialization
393
+ // failure (e.g. a team/enterprise persona hard-fail) is recorded as a task
394
+ // failure, same posture as a provider call that throws before ever reaching a
395
+ // model — one specialist's prompt failing to resolve does not lose the others.
396
+
397
+ function prepareTaskForHost(task, run, cwd, env) {
398
+ try {
399
+ const prompt = materializeTaskPrompt({ task, run, cwd, env });
400
+ task.hostPrompt = { system: prompt.system, user: prompt.user };
401
+ task.specialistId = prompt.specialistId;
402
+ task.packId = prompt.packId;
403
+ task.promptVersion = prompt.promptVersion;
404
+ task.toolGrants = prompt.toolGrants;
405
+ task.personaAvailable = prompt.personaAvailable;
406
+ if (prompt.degraded) task.degraded = prompt.degraded;
407
+ task.executor = 'host:awaiting';
408
+ task.status = 'awaiting-host';
409
+ task.finishedAt = null;
410
+ task.executionState = 'awaiting-host';
411
+ return { ok: true };
412
+ } catch (err) {
413
+ task.executor = 'host:error';
414
+ task.error = {
415
+ code: err.code || 'HOST_MATERIALIZE_FAILED',
416
+ message: err.message,
417
+ ...(err.remediation ? { remediation: err.remediation } : {}),
418
+ };
242
419
  task.status = 'failed';
243
420
  task.finishedAt = new Date().toISOString();
244
- return { ok: false, reasoning: '' };
421
+ task.executionState = 'failed';
422
+ return { ok: false };
245
423
  }
246
424
  }
247
425
 
426
+ // Run-level executionState aggregates every task's LMCP-F1 executionState
427
+ // (prepared|executed|degraded-executed|failed|awaiting-host) into one honest
428
+ // signal for the whole run (LMCP-F4). A run with no tasks (prompt-only,
429
+ // host-direct) owns no specialist sequence, so it aggregates to null rather
430
+ // than fabricating a state for work that never happened. `failed` wins over
431
+ // everything — a run that failed even one task must never report an
432
+ // aggregate that reads as clean. `degraded-executed` beats `executed` next,
433
+ // so a solo-mode persona fallback is never hidden behind a bare 'executed'
434
+ // when at least one real execution also happened. `awaiting-host` beats
435
+ // `prepared` — a host-backend task materialized its prompt but has not
436
+ // prepared-and-stopped the way the inline backend does, so it is a distinct,
437
+ // more specific "still pending" signal. All-`prepared` is the most specific
438
+ // "nothing ran" signal and only applies when every task shares it.
439
+
440
+ const EXECUTION_STATE_PRECEDENCE = ['failed', 'degraded-executed', 'executed', 'awaiting-host', 'prepared'];
441
+
442
+ function aggregateExecutionState(tasks) {
443
+ if (!Array.isArray(tasks) || tasks.length === 0) return null;
444
+ const present = new Set(tasks.map((t) => t.executionState).filter(Boolean));
445
+ for (const state of EXECUTION_STATE_PRECEDENCE) {
446
+ if (present.has(state)) return state;
447
+ }
448
+ // No task carries a recognized executionState (pre-F1 legacy run) — nothing
449
+ // honest to report, so null rather than guessing.
450
+ return null;
451
+ }
452
+
453
+ // The one honest terminal-status computation, shared by executeRun (the
454
+ // synchronous provider/inline/host-materialization path) and
455
+ // submitHostTaskResult (the asynchronous host-pickup completion path) — a run
456
+ // must resolve to the same completed/completed-with-failures/degraded/
457
+ // completed-prepare-only taxonomy regardless of which of those two callers
458
+ // closes it out, so this is written once and reused, not reimplemented.
459
+
460
+ async function finalizeRun(run, { cwd, env, config, store, cancelled = false, anyFailed = false, webUnavailable = false, personaDegraded = false } = {}) {
461
+ // LMCP-F4: aggregate every task's executionState into one run-level signal
462
+ // before the terminal status is computed, so a reader never has to walk
463
+ // run.tasks to learn whether the run prepared, executed, degraded-executed,
464
+ // awaited a host, or failed.
465
+ run.executionState = aggregateExecutionState(run.tasks);
466
+
467
+ let terminalStatus;
468
+ // degraded can come from the execution contract (no model resolved), a web
469
+ // capability gap, or a solo-mode persona fallback; any source means the run
470
+ // must not surface as a bare success.
471
+ const isDegraded = run.degraded || run.execution?.degraded || webUnavailable || personaDegraded;
472
+ if (isDegraded) {
473
+ run.degraded = true;
474
+ run.degradationReason = run.degradationReason || run.execution?.degradationReason
475
+ || (webUnavailable ? 'capability-unavailable' : personaDegraded ? 'persona-fallback' : 'no-model-resolved');
476
+ }
477
+ // Prepare-only is the more specific "no specialist executed" signal, so an
478
+ // all-prepared run reports that even if also degraded; otherwise any degraded run
479
+ // — zero-task or executed-with-a-gap — is 'degraded', never bare 'completed'.
480
+ if (cancelled) {
481
+ terminalStatus = 'cancelled';
482
+ } else if (anyFailed) {
483
+ terminalStatus = 'completed-with-failures';
484
+ } else if (run.tasks.length > 0 && run.tasks.every((t) => t.status === 'prepared')) {
485
+ terminalStatus = 'completed-prepare-only';
486
+ } else if (isDegraded) {
487
+ terminalStatus = 'degraded';
488
+ } else {
489
+ terminalStatus = 'completed';
490
+ }
491
+ run.status = terminalStatus;
492
+ run.updatedAt = new Date().toISOString();
493
+ await store.saveRun(run);
494
+ clearCancel(run.runId);
495
+ // terminalStatus already encodes prepared-vs-executed (completed-prepare-only)
496
+ // and degraded-vs-clean, but its precedence order (cancelled > failed >
497
+ // prepare-only > degraded) can collapse a run that is BOTH e.g.
498
+ // completed-with-failures AND degraded into one string. executionState
499
+ // (LMCP-F4) and the degraded boolean ride alongside it so a trace/SSE
500
+ // consumer never has to re-read the run store to recover the signal
501
+ // terminalStatus's precedence dropped.
502
+ const completedTrace = emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'lifecycle.completed', metadata: { runId: run.runId, tenantId: run.tenantId, status: run.status, executionState: run.executionState, degraded: Boolean(run.degraded), tasks: run.tasks.length } });
503
+ persistTeamTrace({ cwd, env, config, event: completedTrace, tenantId: run.tenantId });
504
+ emitRunEvent(run.runId, { type: 'completed', status: run.status, executionState: run.executionState, degraded: Boolean(run.degraded), tenantId: run.tenantId });
505
+ return run;
506
+ }
507
+
248
508
  /**
249
509
  * Execute a planned run through the chosen worker backend, persisting after each
250
510
  * task transition so the run is resumable and observable.
@@ -264,53 +524,208 @@ export async function executeRun(cwd, runId, { env = process.env, workerBackend
264
524
  throw err;
265
525
  }
266
526
 
267
- const backend = resolveWorkerBackend({ explicit: workerBackend, run, config });
268
- const chainOfThought = resolveChainOfThought({ run, config });
269
- run.workerBackend = backend;
270
- run.chainOfThought = chainOfThought;
271
- run.status = 'running';
272
- run.updatedAt = new Date().toISOString();
273
- await store.saveRun(run);
274
- emitRunEvent(runId, { type: 'running', status: 'running', workerBackend: backend });
275
-
276
- let anyFailed = false;
277
- let cancelled = false;
278
- for (const task of run.tasks) {
279
- if (isCancelRequested(runId)) { cancelled = true; break; }
280
- task.status = 'running';
281
- task.startedAt = new Date().toISOString();
282
- emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'worker.started', role: task.role, taskId: task.id, metadata: { runId, workerBackend: backend } });
283
- emitRunEvent(runId, { type: 'task', taskId: task.id, role: task.role, status: 'running' });
284
-
285
- let taskReasoning = '';
286
- if (backend === PROVIDER) {
287
- const res = await executeTaskViaProvider(task, run, env, fetchImpl, chainOfThought);
288
- if (!res.ok) anyFailed = true;
289
- taskReasoning = res.reasoning;
290
- } else {
291
- prepareTaskInline(task);
527
+ const runFilePath = join(resolveStateDir(cwd, 'runtime', 'orchestration', 'runs'), `${runId}.json`);
528
+
529
+ // A run persisted before this bead carries no tenantId; resolve it now
530
+ // rather than leave the field permanently absent on resumed runs. Enterprise
531
+ // mode fails closed here exactly as it would have at planRun time.
532
+ if (run.tenantId === undefined) {
533
+ run.tenantId = resolveTenantContext({ env, config, mode: getDeploymentMode(env, { cwd }) }).tenantId;
534
+ }
535
+
536
+ try {
537
+ const backend = resolveWorkerBackend({ explicit: workerBackend, run, config });
538
+ const chainOfThought = resolveChainOfThought({ run, config });
539
+ run.workerBackend = backend;
540
+ run.chainOfThought = chainOfThought;
541
+ run.status = 'running';
542
+ run.updatedAt = new Date().toISOString();
543
+ await store.saveRun(run);
544
+ emitRunEvent(runId, { type: 'running', status: 'running', workerBackend: backend });
545
+
546
+ let anyFailed = false;
547
+ let cancelled = false;
548
+ let webUnavailable = false;
549
+ let personaDegraded = false;
550
+ for (const task of run.tasks) {
551
+ if (isCancelRequested(runId)) { cancelled = true; break; }
552
+ task.status = 'running';
553
+ task.startedAt = new Date().toISOString();
554
+ emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'worker.started', role: task.role, taskId: task.id, metadata: { runId, tenantId: run.tenantId, workerBackend: backend } });
555
+ emitRunEvent(runId, { type: 'task', taskId: task.id, role: task.role, status: 'running', tenantId: run.tenantId });
556
+
557
+ let taskReasoning = '';
558
+ if (backend === PROVIDER) {
559
+ const res = await executeTaskViaProvider(task, run, env, fetchImpl, chainOfThought, cwd);
560
+ if (!res.ok) anyFailed = true;
561
+ if (res.webCapability === 'unavailable') webUnavailable = true;
562
+ if (res.personaDegraded) personaDegraded = true;
563
+ taskReasoning = res.reasoning;
564
+ } else if (backend === HOST) {
565
+ const res = prepareTaskForHost(task, run, cwd, env);
566
+ if (!res.ok) anyFailed = true;
567
+ } else {
568
+ prepareTaskInline(task);
569
+ }
570
+
571
+ // telemetry_only records reasoning to the trace without ever surfacing it to
572
+ // a display; surface keeps it off the trace (it rides on task.reasoning).
573
+ // personaAvailable/degraded ride the trace unconditionally (not gated on
574
+ // chainOfThought) so a persona-degraded run is always discoverable from
575
+ // trace history alone, independent of reasoning-disclosure mode.
576
+ const completedMeta = { runId, tenantId: run.tenantId, status: task.status };
577
+ if (chainOfThought === 'telemetry_only' && taskReasoning) {
578
+ completedMeta.reasoning = taskReasoning;
579
+ completedMeta.reasoningChars = taskReasoning.length;
580
+ }
581
+ if (task.personaAvailable === false) {
582
+ completedMeta.personaAvailable = false;
583
+ completedMeta.degraded = task.degraded || 'persona-fallback';
584
+ }
585
+ // Execution provenance (LMCP-F1): specialist/pack/prompt/model/provider/
586
+ // tool-grants/executionState ride the trace unconditionally whenever the
587
+ // task carries them (inline prepare-only tasks carry only executionState;
588
+ // provider tasks, success or failure, carry the full set) — same
589
+ // always-on pattern as personaAvailable above, so a reader never has to
590
+ // reconstruct provenance from a separate source.
591
+ if (task.executionState !== undefined) completedMeta.executionState = task.executionState;
592
+ if (task.specialistId !== undefined) completedMeta.specialistId = task.specialistId;
593
+ if (task.packId !== undefined) completedMeta.packId = task.packId;
594
+ if (task.promptVersion !== undefined) completedMeta.promptVersion = task.promptVersion;
595
+ if (task.model !== undefined) completedMeta.model = task.model;
596
+ if (task.provider !== undefined) completedMeta.provider = task.provider;
597
+ if (task.toolGrants !== undefined) completedMeta.toolGrants = task.toolGrants;
598
+ emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'worker.completed', role: task.role, taskId: task.id, metadata: completedMeta });
599
+ emitRunEvent(runId, { type: 'task', taskId: task.id, role: task.role, status: task.status, executor: task.executor, ...(task.reasoning ? { reasoning: task.reasoning } : {}), ...(task.error ? { error: task.error } : {}) });
600
+ run.updatedAt = new Date().toISOString();
601
+ await store.saveRun(run);
292
602
  }
293
603
 
294
- // telemetry_only records reasoning to the trace without ever surfacing it to
295
- // a display; surface keeps it off the trace (it rides on task.reasoning).
296
- const completedMeta = { runId, status: task.status };
297
- if (chainOfThought === 'telemetry_only' && taskReasoning) {
298
- completedMeta.reasoning = taskReasoning;
299
- completedMeta.reasoningChars = taskReasoning.length;
604
+ // The host backend never executes synchronously it materializes prompts
605
+ // and stands the run at 'awaiting-host' until the calling host submits every
606
+ // task's result via submitHostTaskResult, which finalizes through the same
607
+ // finalizeRun below. The only case that does NOT stand awaiting is when no
608
+ // task actually reached 'awaiting-host' (materialization failed for every
609
+ // task, or the route planned zero tasks) — there is nothing pending pickup,
610
+ // so the run must resolve to a real terminal state rather than stand
611
+ // forever with nothing to submit.
612
+ if (backend === HOST && run.tasks.some((t) => t.status === 'awaiting-host')) {
613
+ run.executionState = aggregateExecutionState(run.tasks);
614
+ run.status = 'awaiting-host';
615
+ run.updatedAt = new Date().toISOString();
616
+ await store.saveRun(run);
617
+ clearCancel(runId);
618
+ emitRunEvent(runId, { type: 'awaiting-host', status: 'awaiting-host', executionState: run.executionState, tenantId: run.tenantId, taskCount: run.tasks.length });
619
+ return run;
300
620
  }
301
- emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'worker.completed', role: task.role, taskId: task.id, metadata: completedMeta });
302
- emitRunEvent(runId, { type: 'task', taskId: task.id, role: task.role, status: task.status, executor: task.executor, ...(task.reasoning ? { reasoning: task.reasoning } : {}), ...(task.error ? { error: task.error } : {}) });
621
+
622
+ return finalizeRun(run, { cwd, env, config, store, cancelled, anyFailed, webUnavailable, personaDegraded });
623
+ } catch (err) {
624
+ run.status = 'error';
625
+ run.error = { code: err.code || 'RUN_EXECUTE_FAILED', message: err.message };
303
626
  run.updatedAt = new Date().toISOString();
304
- await store.saveRun(run);
627
+ // Direct (non-atomic) write to the existing run file: the file was created by
628
+ // planRun so the inode exists; overwriting an existing file succeeds even on a
629
+ // read-only directory (only creating new files requires write permission on the
630
+ // directory itself). This persists the terminal failure durably so
631
+ // getRun/getRuns surfaces the error status to doctor and orchestration_status.
632
+ try { writeFileSync(runFilePath, `${JSON.stringify(run, null, 2)}\n`); } catch { /* best-effort */ }
633
+ emitRunEvent(runId, { type: 'error', status: 'error', error: run.error });
634
+ throw err;
635
+ }
636
+ }
637
+
638
+ // Terminal run statuses a host result can never be submitted against — a run
639
+ // that already finalized has nothing left awaiting pickup, so a late submit is
640
+ // rejected rather than silently mutating a closed record.
641
+
642
+ const TERMINAL_RUN_STATUSES = new Set(['completed', 'completed-with-failures', 'completed-prepare-only', 'degraded', 'cancelled', 'error']);
643
+
644
+ function hostResultError(code, message) {
645
+ const err = new Error(message);
646
+ err.code = code;
647
+ return err;
648
+ }
649
+
650
+ /**
651
+ * Record the calling host's execution of one `awaiting-host` task, submitted
652
+ * by the host after it ran the materialized prompt (task.hostPrompt) in its own
653
+ * model session. The result is host-reported, not independently verified —
654
+ * every field this writes is tagged `provenanceSource: 'host-reported'` so it
655
+ * can never be mistaken for a construct-verified provider execution
656
+ * (task.executor also differs: `host:<hostRole>` vs `provider:<provider>:<model>`).
657
+ * When every task on the run is terminal, the run is finalized through the
658
+ * SAME honest terminal-status logic executeRun uses (finalizeRun) — completed
659
+ * vs completed-with-failures is decided once, not reimplemented here.
660
+ *
661
+ * Persistence is read-modify-write via store.saveRun, the same single-writer
662
+ * posture executeRun already has; concurrent submissions racing the same run
663
+ * are not safety-guarded here (team-mode row-locking is a deferred follow-up,
664
+ * see the ADR).
665
+ *
666
+ * @param {string} cwd
667
+ * @param {string} runId
668
+ * @param {string} taskId
669
+ * @param {{output:string, model?:string, provider?:string, reasoning?:string}} result
670
+ * @param {object} [opts] { env }
671
+ * @returns {Promise<{run:object, nextTask:object|null}>}
672
+ */
673
+ export async function submitHostTaskResult(cwd, runId, taskId, result = {}, { env = process.env } = {}) {
674
+ const config = loadConfig(cwd, env);
675
+ const { store } = resolveRunStore({ config, env, cwd });
676
+ const run = await store.loadRun(runId);
677
+ if (!run) throw hostResultError('RUN_NOT_FOUND', `Orchestration run not found: ${runId}`);
678
+
679
+ const task = (run.tasks || []).find((t) => t.id === taskId);
680
+ if (!task) throw hostResultError('TASK_NOT_FOUND', `Task ${taskId} not found on run ${runId}.`);
681
+
682
+ if (TERMINAL_RUN_STATUSES.has(run.status)) {
683
+ throw hostResultError('RUN_ALREADY_TERMINAL', `Run ${runId} is already terminal (${run.status}); no task result can be submitted.`);
684
+ }
685
+ if (task.status !== 'awaiting-host') {
686
+ throw hostResultError('TASK_NOT_AWAITING_HOST', `Task ${taskId} is not awaiting host execution (status: ${task.status}).`);
687
+ }
688
+ const { output, model = null, provider = null, reasoning = null } = result;
689
+ if (typeof output !== 'string' || !output.trim()) {
690
+ throw hostResultError('HOST_RESULT_EMPTY_OUTPUT', `Host task result for ${taskId} requires non-empty output.`);
305
691
  }
306
692
 
307
- run.status = cancelled ? 'cancelled' : (anyFailed ? 'completed-with-failures' : 'completed');
693
+ task.output = output;
694
+ task.executor = `host:${run.hostRole || 'unknown'}`;
695
+ task.status = 'done';
696
+ task.finishedAt = new Date().toISOString();
697
+ task.executionState = 'executed';
698
+ // Host-reported, never independently verified — the distinguishing marker a
699
+ // reader needs so this never looks like a construct-verified provider
700
+ // execution (which carries no provenanceSource field at all).
701
+ task.provenanceSource = 'host-reported';
702
+ if (model) task.model = model;
703
+ if (provider) task.provider = provider;
704
+ if (reasoning) task.reasoning = reasoning;
705
+ delete task.hostPrompt;
706
+
707
+ emitTraceEvent({
708
+ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'worker.completed', role: task.role, taskId: task.id,
709
+ metadata: {
710
+ runId, tenantId: run.tenantId, status: task.status, executionState: task.executionState,
711
+ specialistId: task.specialistId, packId: task.packId, promptVersion: task.promptVersion,
712
+ model: task.model, provider: task.provider, toolGrants: task.toolGrants, provenanceSource: task.provenanceSource,
713
+ },
714
+ });
715
+ emitRunEvent(runId, { type: 'task', taskId: task.id, role: task.role, status: task.status, executor: task.executor, ...(task.reasoning ? { reasoning: task.reasoning } : {}) });
716
+
308
717
  run.updatedAt = new Date().toISOString();
309
- await store.saveRun(run);
310
- clearCancel(runId);
311
- emitTraceEvent({ rootDir: cwd, env, traceId: run.traceId, spanId: newSpanId(), eventType: 'lifecycle.completed', metadata: { runId, status: run.status, tasks: run.tasks.length } });
312
- emitRunEvent(runId, { type: 'completed', status: run.status });
313
- return run;
718
+
719
+ const stillAwaiting = run.tasks.some((t) => t.status === 'awaiting-host');
720
+ if (stillAwaiting) {
721
+ await store.saveRun(run);
722
+ const nextTask = run.tasks.find((t) => t.status === 'awaiting-host');
723
+ return { run, nextTask };
724
+ }
725
+
726
+ const anyFailed = run.tasks.some((t) => t.status === 'failed');
727
+ const finalized = await finalizeRun(run, { cwd, env, config, store, anyFailed });
728
+ return { run: finalized, nextTask: null };
314
729
  }
315
730
 
316
731
  /**
@@ -333,6 +748,15 @@ export async function runOrchestration(request = {}, { env = process.env, cwd =
333
748
  */
334
749
  export async function startRun(request = {}, { env = process.env, cwd = process.cwd(), workerBackend = null, fetchImpl } = {}) {
335
750
  const planned = await planRun(request, { env, cwd });
751
+
752
+ // Fire-and-forget: no claim, heartbeat, or retry — process death silently kills the run.
753
+ // Solo mode only; durable team/enterprise worker execution is a separate bead. runMode
754
+ // labels the record as in-process so orchestration_status never conflates it with a durable worker.
755
+
756
+ planned.runMode = 'in-process';
757
+ const { store } = resolveRunStore({ config: loadConfig(cwd, env), env, cwd });
758
+ await store.saveRun(planned);
759
+
336
760
  Promise.resolve()
337
761
  .then(() => executeRun(cwd, planned.runId, { env, workerBackend, fetchImpl }))
338
762
  .catch((err) => {
@@ -349,7 +773,11 @@ export function hostAdapterMetadata(run) {
349
773
  return {
350
774
  runId: run.runId,
351
775
  traceId: run.traceId,
776
+ tenantId: run.tenantId ?? null,
352
777
  status: run.status,
778
+ // LMCP-F4: the run-level aggregate of every task's executionState, or null
779
+ // for a zero-task run (prompt-only/host-direct) or a pre-F4 legacy record.
780
+ executionState: run.executionState ?? aggregateExecutionState(run.tasks) ?? null,
353
781
  requestedStrategy: e.requestedStrategy,
354
782
  effectiveStrategy: e.effectiveStrategy,
355
783
  executionMode: e.executionMode,
@@ -357,11 +785,29 @@ export function hostAdapterMetadata(run) {
357
785
  workerBackend: run.workerBackend,
358
786
  chainOfThought: run.chainOfThought ?? null,
359
787
  hostRole: run.hostRole,
360
- degraded: e.degraded,
361
- degradationReason: e.degradationReason,
788
+ degraded: run.degraded ?? e.degraded ?? false,
789
+ degradationReason: run.degradationReason ?? e.degradationReason ?? null,
362
790
  selectedProvider: e.selectedProvider,
363
791
  selectedModel: e.selectedModel,
364
- tasks: (run.tasks || []).map((t) => ({ id: t.id, role: t.role, status: t.status, executor: t.executor, reasoning: t.reasoning ?? null, output: t.output ?? null, error: t.error ?? null })),
792
+ // ?? null / ?? [] on every LMCP-F1 field: a pre-F1 run record carries none of
793
+ // these keys, so a typed absence is returned rather than `undefined`.
794
+ tasks: (run.tasks || []).map((t) => ({
795
+ id: t.id, role: t.role, status: t.status, executor: t.executor, tenantId: t.tenantId ?? null,
796
+ reasoning: t.reasoning ?? null, output: t.output ?? null, error: t.error ?? null,
797
+ specialistId: t.specialistId ?? null,
798
+ packId: t.packId ?? null,
799
+ promptVersion: t.promptVersion ?? null,
800
+ model: t.model ?? null,
801
+ provider: t.provider ?? null,
802
+ toolGrants: t.toolGrants ?? [],
803
+ executionState: t.executionState ?? null,
804
+ personaAvailable: t.personaAvailable ?? null,
805
+ // Present only on a host-executed task — absence (not a bare `false`)
806
+ // is what keeps a provider-verified task from ever reading the same as
807
+ // a host-reported one.
808
+ ...(t.provenanceSource ? { provenanceSource: t.provenanceSource } : {}),
809
+ ...(t.hostPrompt ? { hostPrompt: t.hostPrompt } : {}),
810
+ })),
365
811
  warnings: run.warnings || [],
366
812
  semantics: run.semantics,
367
813
  executionSemantics: run.executionSemantics,