@geraldmaron/construct 1.4.2 → 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 (519) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1027 -64
  4. package/examples/distribution/sources/deck-one-pager.md +1 -1
  5. package/lib/acp/server.mjs +21 -7
  6. package/lib/adapters-sync.mjs +2 -1
  7. package/lib/audit-trail.mjs +185 -2
  8. package/lib/beads/auto-close.mjs +2 -1
  9. package/lib/beads/drift.mjs +2 -1
  10. package/lib/bridges/copilot-proxy.mjs +20 -5
  11. package/lib/certification/skill-inventory.mjs +10 -1
  12. package/lib/cli/approvals.mjs +147 -0
  13. package/lib/cli-commands.mjs +105 -14
  14. package/lib/comment-lint.mjs +127 -8
  15. package/lib/config/schema.mjs +6 -29
  16. package/lib/config/source-target-registry.mjs +70 -0
  17. package/lib/config/source-targets.mjs +179 -205
  18. package/lib/contracts/coverage.mjs +77 -0
  19. package/lib/contracts/validate.mjs +102 -13
  20. package/lib/contracts/violation-log.mjs +50 -4
  21. package/lib/db/migrate.mjs +69 -0
  22. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  23. package/lib/db/migrations/002_queue_provider.sql +50 -0
  24. package/lib/db/migrations/003_worker_registry.sql +17 -0
  25. package/lib/db/migrations/004_trace_events.sql +16 -0
  26. package/lib/db/migrations/005_shared_memory.sql +15 -0
  27. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  28. package/lib/decisions/enforced-baseline.json +0 -2
  29. package/lib/decisions/registry.mjs +3 -2
  30. package/lib/deployment/parity-contract.mjs +1 -1
  31. package/lib/deployment-mode.mjs +78 -2
  32. package/lib/diagram-export.mjs +10 -1
  33. package/lib/distill.mjs +5 -2
  34. package/lib/docs-verify.mjs +2 -1
  35. package/lib/doctor/cli.mjs +1 -0
  36. package/lib/doctor/command-on-path.mjs +24 -0
  37. package/lib/doctor/diagnosis.mjs +89 -0
  38. package/lib/doctor/embedding-health.mjs +50 -0
  39. package/lib/doctor/engine-health.mjs +93 -0
  40. package/lib/doctor/graph-validate.mjs +47 -0
  41. package/lib/doctor/index.mjs +18 -4
  42. package/lib/doctor/sidecar-providers.mjs +56 -0
  43. package/lib/doctor/watchers/consistency.mjs +93 -1
  44. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  45. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  46. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  47. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  48. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  49. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  50. package/lib/document-export.mjs +52 -27
  51. package/lib/document-extract/docling-client.mjs +9 -5
  52. package/lib/document-extract/docling-sidecar.py +30 -0
  53. package/lib/document-extract.mjs +1 -1
  54. package/lib/document-ingest.mjs +9 -0
  55. package/lib/embed/approval-queue.mjs +184 -88
  56. package/lib/embed/authority-guard.mjs +65 -2
  57. package/lib/embed/capability-jobs.mjs +365 -0
  58. package/lib/embed/capability-lifecycle.mjs +219 -0
  59. package/lib/embed/capability-loader.mjs +303 -0
  60. package/lib/embed/capability-runtime.mjs +58 -0
  61. package/lib/embed/cli.mjs +185 -8
  62. package/lib/embed/config.mjs +4 -0
  63. package/lib/embed/daemon.mjs +125 -6
  64. package/lib/embed/demand-fetch.mjs +190 -54
  65. package/lib/embed/inbox.mjs +12 -10
  66. package/lib/embed/presets/ops-triage.mjs +236 -0
  67. package/lib/embed/presets/pm-feedback.mjs +341 -0
  68. package/lib/embed/presets/tpm.mjs +401 -0
  69. package/lib/embed/providers/jira.mjs +72 -1
  70. package/lib/embed/providers/registry.mjs +140 -33
  71. package/lib/embedded-contract/capability.mjs +4 -0
  72. package/lib/embedded-contract/execution.mjs +1 -1
  73. package/lib/embedded-contract/model-resolve.mjs +53 -3
  74. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  75. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  76. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  77. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  78. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  79. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  80. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  81. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  82. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  83. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  84. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  85. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  86. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  88. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  90. package/lib/env-config.mjs +50 -9
  91. package/lib/extensions/index.mjs +27 -0
  92. package/lib/extensions/loader.mjs +120 -0
  93. package/lib/extensions/manifest-schema.mjs +141 -0
  94. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  95. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  96. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  97. package/lib/extensions/manifests/directory.manifest.json +12 -0
  98. package/lib/extensions/manifests/docling.manifest.json +37 -0
  99. package/lib/extensions/manifests/echo.manifest.json +8 -0
  100. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  101. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  102. package/lib/extensions/manifests/github.manifest.json +52 -0
  103. package/lib/extensions/manifests/linear.manifest.json +50 -0
  104. package/lib/extensions/manifests/local.manifest.json +12 -0
  105. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  106. package/lib/extensions/manifests/openai.manifest.json +12 -0
  107. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  108. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  109. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  110. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  113. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  114. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  115. package/lib/extensions/manifests/slack.manifest.json +58 -0
  116. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  117. package/lib/extensions/validate.mjs +175 -0
  118. package/lib/features.mjs +13 -9
  119. package/lib/flows/checkpoint.mjs +184 -0
  120. package/lib/flows/constants.mjs +27 -0
  121. package/lib/flows/define.mjs +146 -0
  122. package/lib/flows/engine.mjs +249 -0
  123. package/lib/flows/errors.mjs +19 -0
  124. package/lib/flows/index.mjs +27 -0
  125. package/lib/flows/joins.mjs +18 -0
  126. package/lib/flows/schema.mjs +90 -0
  127. package/lib/flows/state.mjs +31 -0
  128. package/lib/frameworks/loader.mjs +99 -0
  129. package/lib/frameworks/schema.mjs +157 -0
  130. package/lib/graph/build-from-corpus.mjs +67 -0
  131. package/lib/graph/build-from-embed.mjs +82 -0
  132. package/lib/graph/build-from-registry.mjs +164 -3
  133. package/lib/graph/cli.mjs +456 -12
  134. package/lib/graph/gap-queries.mjs +156 -0
  135. package/lib/graph/gaps.mjs +41 -0
  136. package/lib/graph/impacted.mjs +129 -0
  137. package/lib/graph/runtime-evidence.mjs +177 -0
  138. package/lib/graph/security-coverage.mjs +113 -0
  139. package/lib/graph/staleness.mjs +241 -10
  140. package/lib/graph/store.mjs +24 -7
  141. package/lib/graph/validate.mjs +161 -0
  142. package/lib/headhunt.mjs +9 -8
  143. package/lib/health-check.mjs +15 -7
  144. package/lib/hook-health.mjs +1 -1
  145. package/lib/hooks/agent-tracker.mjs +10 -4
  146. package/lib/hooks/edit-guard.mjs +3 -3
  147. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  148. package/lib/hooks/guard-bash.mjs +41 -15
  149. package/lib/hooks/mcp-health-check.mjs +11 -4
  150. package/lib/hooks/model-fallback.mjs +50 -6
  151. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  152. package/lib/hooks/pre-compact.mjs +181 -173
  153. package/lib/hooks/rule-verifier.mjs +2 -1
  154. package/lib/hooks/session-reflect.mjs +2 -1
  155. package/lib/hooks/session-start.mjs +3 -3
  156. package/lib/host-capabilities.mjs +13 -2
  157. package/lib/host-disposition.mjs +19 -7
  158. package/lib/identity.mjs +92 -0
  159. package/lib/ingest/degraded-extract.mjs +96 -0
  160. package/lib/ingest/docling-remote.mjs +2 -1
  161. package/lib/ingest/provider-extract.mjs +7 -19
  162. package/lib/ingest/sidecar-providers.mjs +196 -0
  163. package/lib/ingest-tooling.mjs +11 -5
  164. package/lib/init-unified.mjs +55 -38
  165. package/lib/init-update.mjs +2 -1
  166. package/lib/install/legacy-global-cleanup.mjs +25 -0
  167. package/lib/intake/daemon.mjs +99 -8
  168. package/lib/intake/git-queue.mjs +110 -38
  169. package/lib/intake/queue-registry.mjs +50 -0
  170. package/lib/intake/queue.mjs +133 -17
  171. package/lib/intake/session-prelude.mjs +57 -8
  172. package/lib/integrations/intake-integrations.mjs +61 -111
  173. package/lib/intent-classifier.mjs +43 -5
  174. package/lib/libreoffice-export.mjs +8 -8
  175. package/lib/logging/rotate.mjs +5 -4
  176. package/lib/mcp/broker.mjs +332 -17
  177. package/lib/mcp/denied-store.mjs +95 -0
  178. package/lib/mcp/destructive-gate.mjs +30 -0
  179. package/lib/mcp/dispatch-envelope.mjs +199 -0
  180. package/lib/mcp/memory-bridge.mjs +2 -1
  181. package/lib/mcp/server.mjs +154 -1411
  182. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  183. package/lib/mcp/tool-definitions-project.mjs +271 -0
  184. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  185. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  186. package/lib/mcp/tool-definitions.mjs +25 -0
  187. package/lib/mcp/tool-registry.mjs +107 -0
  188. package/lib/mcp/tool-safety.mjs +1 -0
  189. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  190. package/lib/mcp/tools/orchestration-run.mjs +165 -25
  191. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  192. package/lib/mcp/tools/provider-write.mjs +187 -0
  193. package/lib/mcp/tools/scope.mjs +0 -3
  194. package/lib/mcp/tools/skills.mjs +1 -1
  195. package/lib/mcp/tools/storage.mjs +0 -8
  196. package/lib/mcp/transport/auth.mjs +238 -0
  197. package/lib/mcp/transport/http.mjs +136 -0
  198. package/lib/mcp/transport/mode.mjs +31 -0
  199. package/lib/mcp/transport/stdio.mjs +22 -0
  200. package/lib/mcp-catalog.json +4 -4
  201. package/lib/mcp-manager.mjs +34 -23
  202. package/lib/mcp-platform-config.mjs +31 -7
  203. package/lib/mode-capabilities.mjs +109 -0
  204. package/lib/model-router.mjs +42 -9
  205. package/lib/models/catalog.mjs +40 -1
  206. package/lib/net-guard.mjs +247 -0
  207. package/lib/observation-store.mjs +6 -2
  208. package/lib/ollama/provision-context.mjs +2 -1
  209. package/lib/opencode-config.mjs +22 -3
  210. package/lib/opencode-runtime-plugin.mjs +120 -2
  211. package/lib/oracle/actions.mjs +204 -10
  212. package/lib/oracle/cli.mjs +24 -3
  213. package/lib/oracle/dispatch.mjs +2 -1
  214. package/lib/oracle/execute.mjs +2 -2
  215. package/lib/oracle/gaps.mjs +3 -3
  216. package/lib/oracle/heartbeat.mjs +53 -0
  217. package/lib/oracle/read-model.mjs +69 -13
  218. package/lib/oracle/reconcile.mjs +3 -46
  219. package/lib/oracle/routing.mjs +37 -31
  220. package/lib/oracle/synthesize.mjs +1 -1
  221. package/lib/orchestration/classification.mjs +434 -0
  222. package/lib/orchestration/delegation-flow.mjs +132 -0
  223. package/lib/orchestration/flow-selection.mjs +418 -0
  224. package/lib/orchestration/gates.mjs +244 -0
  225. package/lib/orchestration/host-sampling.mjs +121 -0
  226. package/lib/orchestration/policy-constants.mjs +48 -0
  227. package/lib/orchestration/provider-outcome.mjs +197 -0
  228. package/lib/orchestration/readiness.mjs +114 -13
  229. package/lib/orchestration/run-store-postgres.mjs +32 -26
  230. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  231. package/lib/orchestration/run-store.mjs +25 -12
  232. package/lib/orchestration/runtime.mjs +446 -39
  233. package/lib/orchestration/store.mjs +87 -12
  234. package/lib/orchestration/trace-store.mjs +125 -0
  235. package/lib/orchestration/web-capability.mjs +3 -2
  236. package/lib/orchestration/worker-runtime.mjs +162 -0
  237. package/lib/orchestration/worker.mjs +528 -89
  238. package/lib/orchestration-policy.mjs +67 -1111
  239. package/lib/packs/cli.mjs +144 -0
  240. package/lib/packs/core-pack.mjs +112 -0
  241. package/lib/packs/enablement.mjs +187 -0
  242. package/lib/packs/index.mjs +23 -0
  243. package/lib/packs/loader.mjs +176 -0
  244. package/lib/packs/manifest-schema.mjs +58 -0
  245. package/lib/packs/prompts.mjs +120 -0
  246. package/lib/packs/validate.mjs +208 -0
  247. package/lib/plugin-registry.mjs +4 -5
  248. package/lib/policy/audit-gate.mjs +42 -0
  249. package/lib/policy/consumption-budget.mjs +149 -0
  250. package/lib/policy/engine.mjs +81 -6
  251. package/lib/policy/role-authority.mjs +89 -0
  252. package/lib/prompt-composer.js +19 -3
  253. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  254. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  255. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  256. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  257. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  258. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  259. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  260. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  261. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  262. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  263. package/lib/providers/contract.mjs +207 -0
  264. package/lib/providers/credential-bootstrap.mjs +7 -4
  265. package/lib/providers/credential-sources.mjs +14 -2
  266. package/lib/providers/directory/index.mjs +261 -0
  267. package/lib/providers/feedback/index.mjs +392 -0
  268. package/lib/providers/filter-audit.mjs +68 -0
  269. package/lib/providers/filter-schema.mjs +54 -0
  270. package/lib/providers/github/index.mjs +31 -0
  271. package/lib/providers/instance-config.mjs +215 -0
  272. package/lib/providers/op-locate.mjs +96 -0
  273. package/lib/providers/op-run.mjs +64 -9
  274. package/lib/providers/registry.mjs +26 -3
  275. package/lib/providers/secret-audit-wiring.mjs +6 -0
  276. package/lib/providers/secret-resolver.mjs +240 -23
  277. package/lib/publish-template.mjs +6 -3
  278. package/lib/publish-tooling.mjs +4 -0
  279. package/lib/publish.mjs +32 -4
  280. package/lib/queue/pg-queue.mjs +395 -0
  281. package/lib/reflect.mjs +2 -1
  282. package/lib/registry/assemble.mjs +28 -2
  283. package/lib/registry/consolidation.mjs +8 -1
  284. package/lib/registry/custom-scaffold.mjs +200 -0
  285. package/lib/registry/custom-schema.mjs +137 -0
  286. package/lib/registry/loader.mjs +8 -3
  287. package/lib/registry/manifests/format-engines.default.json +15 -0
  288. package/lib/registry/manifests/surface-map.default.json +46 -0
  289. package/lib/registry/retired-paths.mjs +1 -0
  290. package/lib/registry/surface-map.mjs +100 -50
  291. package/lib/render-visual-check.mjs +240 -0
  292. package/lib/resources/budget.mjs +40 -17
  293. package/lib/roles/flavor-bindings.mjs +36 -14
  294. package/lib/roles/gateway.mjs +15 -8
  295. package/lib/roots.mjs +107 -0
  296. package/lib/runtime/uv-bootstrap.mjs +32 -6
  297. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  298. package/lib/runtime-env.mjs +5 -2
  299. package/lib/scheduler/index.mjs +8 -20
  300. package/lib/schema-infer.mjs +31 -2
  301. package/lib/scopes/lifecycle.mjs +29 -25
  302. package/lib/scopes/loader.mjs +49 -12
  303. package/lib/scopes/teams.mjs +1 -1
  304. package/lib/security/ingest-boundary.mjs +80 -0
  305. package/lib/security/recall-wrapper.mjs +99 -0
  306. package/lib/security/trust.mjs +132 -0
  307. package/lib/service-manager.mjs +38 -19
  308. package/lib/setup.mjs +41 -23
  309. package/lib/skills-apply.mjs +4 -2
  310. package/lib/specialists/postconditions.mjs +2 -2
  311. package/lib/state-root.mjs +147 -0
  312. package/lib/status.mjs +597 -6
  313. package/lib/storage/admin.mjs +77 -5
  314. package/lib/storage/backend-registry.mjs +73 -0
  315. package/lib/storage/backend.mjs +63 -9
  316. package/lib/storage/embeddings-openai.mjs +12 -2
  317. package/lib/storage/hybrid-query.mjs +11 -3
  318. package/lib/storage/retrieval-hardening.mjs +384 -0
  319. package/lib/storage/shared-memory.mjs +158 -0
  320. package/lib/storage/vector-client.mjs +69 -2
  321. package/lib/team/health.mjs +72 -0
  322. package/lib/telemetry/backends/local.mjs +9 -3
  323. package/lib/telemetry/client.mjs +3 -7
  324. package/lib/template-registry.mjs +10 -12
  325. package/lib/tenant/context.mjs +82 -0
  326. package/lib/tenant/isolation.mjs +129 -0
  327. package/lib/test-corpus-inventory.mjs +103 -2
  328. package/lib/uninstall/uninstall.mjs +78 -12
  329. package/lib/validator.mjs +4 -6
  330. package/lib/worker/entrypoint.mjs +2 -1
  331. package/lib/worker/run.mjs +2 -2
  332. package/lib/worker/trace.mjs +5 -11
  333. package/lib/workflow-state.mjs +52 -8
  334. package/lib/workflows/liveness.mjs +203 -0
  335. package/lib/workflows/loader.mjs +177 -0
  336. package/lib/workflows/manifest-schema.mjs +71 -0
  337. package/lib/workflows/surface-parity.mjs +118 -0
  338. package/lib/workflows/validate.mjs +127 -0
  339. package/lib/writes/control-plane.mjs +140 -0
  340. package/lib/writes/envelope.mjs +206 -0
  341. package/lib/writes/sent-log.mjs +86 -0
  342. package/lib/writes/write-intent.mjs +109 -0
  343. package/package.json +16 -8
  344. package/personas/construct.md +12 -3
  345. package/registry/capabilities.json +143 -36
  346. package/rules/common/comments.md +1 -0
  347. package/schemas/project-config.schema.json +0 -12
  348. package/schemas/unified-registry.schema.json +2 -4
  349. package/scripts/sync-specialists.mjs +284 -81
  350. package/skills/docs/adr-workflow.md +1 -1
  351. package/skills/docs/codebase-research-workflow.md +3 -3
  352. package/skills/docs/prd-workflow.md +5 -6
  353. package/skills/docs/runbook-workflow.md +2 -2
  354. package/skills/docs/user-research-workflow.md +3 -3
  355. package/skills/operating/fleet-health-routing.md +77 -0
  356. package/skills/roles/ai-engineer.md +1 -1
  357. package/skills/roles/architect.md +0 -1
  358. package/skills/roles/business-strategist.md +1 -1
  359. package/skills/roles/data-analyst.telemetry.md +1 -1
  360. package/skills/roles/data-engineer.md +1 -1
  361. package/skills/roles/data-engineer.pipeline.md +1 -1
  362. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  363. package/skills/roles/data-engineer.warehouse.md +1 -1
  364. package/skills/roles/designer.accessibility.md +1 -1
  365. package/skills/roles/designer.md +0 -1
  366. package/skills/roles/devil-advocate.md +1 -1
  367. package/skills/roles/docs-keeper.md +1 -1
  368. package/skills/roles/evaluator.md +1 -1
  369. package/skills/roles/explorer.md +1 -1
  370. package/skills/roles/platform-engineer.md +1 -1
  371. package/skills/roles/qa.ai-eval.md +1 -2
  372. package/skills/roles/qa.api-contract.md +0 -1
  373. package/skills/roles/qa.data-pipeline.md +0 -1
  374. package/skills/roles/qa.md +0 -1
  375. package/skills/roles/qa.web-ui.md +0 -1
  376. package/skills/roles/release-manager.md +1 -1
  377. package/skills/roles/researcher.md +0 -2
  378. package/skills/roles/reviewer.md +0 -3
  379. package/skills/roles/security.ai.md +1 -1
  380. package/skills/roles/security.legal-compliance.md +1 -1
  381. package/skills/roles/security.md +0 -1
  382. package/skills/roles/security.privacy.md +0 -1
  383. package/skills/roles/security.supply-chain.md +1 -1
  384. package/skills/roles/sre.md +1 -1
  385. package/skills/roles/test-automation.md +1 -1
  386. package/skills/roles/trace-reviewer.md +1 -1
  387. package/skills/roles/ux-researcher.md +1 -1
  388. package/specialists/artifact-manifest.json +36 -36
  389. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  390. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  391. package/specialists/org/contracts/any-to-debugger.json +7 -1
  392. package/specialists/org/contracts/any-to-designer.json +7 -1
  393. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  394. package/specialists/org/contracts/any-to-explorer.json +13 -4
  395. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  396. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  397. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  398. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  399. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  400. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  401. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  402. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  403. package/specialists/org/contracts/architect-to-operations.json +17 -4
  404. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  405. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  406. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  407. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  408. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  409. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  410. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  411. package/specialists/org/contracts/operations-to-user.json +53 -0
  412. package/specialists/org/contracts/operations-triage-output.json +53 -0
  413. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  414. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  415. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  416. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  417. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  418. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  419. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  420. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  421. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  422. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  423. package/specialists/org/contracts/user-to-construct.json +11 -4
  424. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  425. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  426. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  427. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  428. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  429. package/specialists/org/groups/engineering-group.json +2 -6
  430. package/specialists/org/groups/governance-group.json +2 -3
  431. package/specialists/org/groups/operations-group.json +5 -8
  432. package/specialists/org/groups/product-group.json +4 -8
  433. package/specialists/org/groups/quality-group.json +3 -7
  434. package/specialists/org/groups/strategy-group.json +6 -9
  435. package/specialists/org/models.json.example +8 -0
  436. package/specialists/org/scopes/creative.json +6 -1
  437. package/specialists/org/scopes/operations.json +7 -1
  438. package/specialists/org/scopes/research.json +6 -1
  439. package/specialists/org/scopes/rnd.json +7 -1
  440. package/specialists/org/specialists/cx-architect.json +27 -8
  441. package/specialists/org/specialists/cx-designer.json +22 -8
  442. package/specialists/org/specialists/cx-engineer.json +71 -7
  443. package/specialists/org/specialists/cx-operations.json +89 -15
  444. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  445. package/specialists/org/specialists/cx-product-manager.json +28 -9
  446. package/specialists/org/specialists/cx-qa.json +16 -6
  447. package/specialists/org/specialists/cx-researcher.json +40 -7
  448. package/specialists/org/specialists/cx-reviewer.json +61 -11
  449. package/specialists/org/specialists/cx-security.json +30 -10
  450. package/specialists/org/teams/design-team.json +3 -4
  451. package/specialists/org/teams/engineering-team.json +3 -10
  452. package/specialists/org/teams/governance-team.json +3 -5
  453. package/specialists/org/teams/operations-team.json +6 -12
  454. package/specialists/org/teams/product-management-team.json +2 -3
  455. package/specialists/org/teams/quality-team.json +4 -12
  456. package/specialists/org/teams/research-team.json +3 -3
  457. package/specialists/org/teams/strategy-team.json +7 -14
  458. package/specialists/prompts/cx-architect.md +20 -1
  459. package/specialists/prompts/cx-data-analyst.md +1 -1
  460. package/specialists/prompts/cx-designer.md +12 -4
  461. package/specialists/prompts/cx-engineer.md +15 -1
  462. package/specialists/prompts/cx-operations.md +14 -2
  463. package/specialists/prompts/cx-orchestrator.md +9 -5
  464. package/specialists/prompts/cx-product-manager.md +5 -1
  465. package/specialists/prompts/cx-qa.md +6 -2
  466. package/specialists/prompts/cx-researcher.md +25 -30
  467. package/specialists/prompts/cx-reviewer.md +19 -1
  468. package/specialists/prompts/cx-security.md +5 -1
  469. package/templates/distribution/construct-brand.typ +123 -59
  470. package/templates/distribution/construct-decision.typ +6 -3
  471. package/templates/distribution/construct-pdf.typ +11 -4
  472. package/templates/distribution/construct-prd.typ +6 -3
  473. package/templates/distribution/construct-research.typ +7 -4
  474. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  475. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  476. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  477. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  478. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  479. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  480. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  481. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  482. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  483. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  484. package/specialists/org/specialists/cx-accessibility.json +0 -69
  485. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  486. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  487. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  488. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  489. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  490. package/specialists/org/specialists/cx-evaluator.json +0 -69
  491. package/specialists/org/specialists/cx-explorer.json +0 -69
  492. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  493. package/specialists/org/specialists/cx-oracle.json +0 -46
  494. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  495. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  496. package/specialists/org/specialists/cx-release-manager.json +0 -77
  497. package/specialists/org/specialists/cx-sre.json +0 -94
  498. package/specialists/org/specialists/cx-test-automation.json +0 -69
  499. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  500. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  501. package/specialists/org/teams/accessibility-team.json +0 -39
  502. package/specialists/org/teams/ux-research-team.json +0 -39
  503. package/specialists/prompts/cx-accessibility.md +0 -55
  504. package/specialists/prompts/cx-ai-engineer.md +0 -124
  505. package/specialists/prompts/cx-business-strategist.md +0 -58
  506. package/specialists/prompts/cx-data-engineer.md +0 -55
  507. package/specialists/prompts/cx-devil-advocate.md +0 -59
  508. package/specialists/prompts/cx-docs-keeper.md +0 -164
  509. package/specialists/prompts/cx-evaluator.md +0 -49
  510. package/specialists/prompts/cx-explorer.md +0 -71
  511. package/specialists/prompts/cx-legal-compliance.md +0 -58
  512. package/specialists/prompts/cx-oracle.md +0 -98
  513. package/specialists/prompts/cx-platform-engineer.md +0 -97
  514. package/specialists/prompts/cx-rd-lead.md +0 -59
  515. package/specialists/prompts/cx-release-manager.md +0 -54
  516. package/specialists/prompts/cx-sre.md +0 -111
  517. package/specialists/prompts/cx-test-automation.md +0 -55
  518. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  519. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -8,27 +8,139 @@
8
8
  * `Broker.invoke({...})`, every denial emits a typed error rather than
9
9
  * a silent fallthrough, and every brokered call appends a `tool.called`
10
10
  * trace event tagged with the policy decision.
11
+ *
12
+ * LMCP-I3: every decision (allow/deny/approval_required) is also recorded
13
+ * as durable evidence under one schema — {decisionId, actor, tenant,
14
+ * project, tool, target, risk, outcome, correlationId, ts} — via
15
+ * `_recordDecision`. Denied decisions additionally persist to
16
+ * lib/mcp/denied-store.mjs so a PolicyDenied throw is never the only trace
17
+ * of the denial. `correlationId` reuses an inbound `traceId` when the
18
+ * caller has one, or mints a fresh trace-shaped id, so the same value ties
19
+ * the broker call to its `tool.called` trace event and to any run/task/
20
+ * external write layered on top by the caller.
11
21
  */
12
22
 
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import crypto from 'node:crypto';
13
26
  import { policyDecision } from '../policy/engine.mjs';
14
- import { emitTraceEvent } from '../worker/trace.mjs';
27
+ import { emitTraceEvent, newTraceId } from '../worker/trace.mjs';
28
+ import { getDeploymentMode } from '../deployment-mode.mjs';
29
+ import { loadProjectConfig } from '../config/project-config.mjs';
30
+ import { ApprovalQueue } from '../embed/approval-queue.mjs';
31
+ import { resolveTenantContext } from '../tenant/context.mjs';
32
+ import { appendAuditRecord } from '../audit-trail.mjs';
33
+ import { DeniedStore } from './denied-store.mjs';
34
+ import { ConsumptionBudgetStore, BudgetExceeded } from '../policy/consumption-budget.mjs';
15
35
 
16
36
  const DEFAULT_RATE_WINDOW_MS = 60_000;
17
37
  const DEFAULT_RATE_BUDGET = 30;
18
38
 
39
+ // Every broker decision — allow, deny, or approval-required — is recorded
40
+ // under this one schema so the durable denied-store and the audit-trail
41
+ // entry for the same decision can be cross-referenced by decisionId, and a
42
+ // correlationId ties the decision back to the trace event and, downstream,
43
+ // to whatever run/task/external write the tool call was part of.
44
+
45
+ function newDecisionId() {
46
+ return `decision-${crypto.randomBytes(8).toString('hex')}`;
47
+ }
48
+
49
+ function buildDecisionRecord({ decision, outcome, actor, tenant, project, tool, target, risk, correlationId, ts }) {
50
+ return {
51
+ decisionId: newDecisionId(),
52
+ actor: actor ?? 'unknown',
53
+ tenant: tenant ?? 'unknown',
54
+ project: project ?? null,
55
+ tool: tool ?? 'unknown',
56
+ target: target ?? null,
57
+ risk: risk || 'low',
58
+ outcome,
59
+ correlationId,
60
+ reason: decision?.reason ?? null,
61
+ source: decision?.source ?? null,
62
+ ts,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * BrokerStore — file-backed durable storage for broker rate-limit state.
68
+ *
69
+ * Persists call timestamps to `<rootDir>/.cx/broker-state.json` so that
70
+ * rate-limit windows survive broker instance recreation and server restarts.
71
+ * In-memory `_data` is the authoritative write buffer; `load`/`save` sync
72
+ * against disk. All disk I/O is best-effort: a disk failure leaves in-memory
73
+ * state intact without crashing the broker.
74
+ * Rate-limit windows are keyed by actor+tool+windowStart (epoch ms).
75
+ */
76
+ export class BrokerStore {
77
+ constructor() {
78
+ // key → timestamp[] (ms since epoch), trimmed to current window on read
79
+ this._data = {};
80
+ }
81
+
82
+ /** Load persisted state from `storePath`. No-op if the file is absent or unreadable. */
83
+ load(storePath) {
84
+ try {
85
+ const raw = fs.readFileSync(storePath, 'utf8');
86
+ const parsed = JSON.parse(raw);
87
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
88
+ this._data = parsed;
89
+ }
90
+ } catch { /* absent or corrupt file — start fresh */ }
91
+ }
92
+
93
+ /** Persist current state to `storePath`. Best-effort. */
94
+ save(storePath) {
95
+ try {
96
+ fs.mkdirSync(path.dirname(storePath), { recursive: true });
97
+ fs.writeFileSync(storePath, JSON.stringify(this._data), 'utf8');
98
+ } catch { /* best-effort */ }
99
+ }
100
+
101
+ /**
102
+ * getRateLimitState(actor, tool, windowMs) → timestamps[] within the window.
103
+ * Trims stale entries before returning.
104
+ */
105
+ getRateLimitState(actor, tool, windowMs) {
106
+ const key = `${actor}::${tool}`;
107
+ const now = Date.now();
108
+ const all = this._data[key] || [];
109
+ const fresh = all.filter((ts) => now - ts < windowMs);
110
+ this._data[key] = fresh;
111
+ return fresh;
112
+ }
113
+
114
+ /**
115
+ * incrementRateLimitState(actor, tool, windowMs) → updated timestamps[].
116
+ * Appends `Date.now()` and trims stale entries.
117
+ */
118
+ incrementRateLimitState(actor, tool, windowMs) {
119
+ const key = `${actor}::${tool}`;
120
+ const now = Date.now();
121
+ const all = this._data[key] || [];
122
+ const fresh = all.filter((ts) => now - ts < windowMs);
123
+ fresh.push(now);
124
+ this._data[key] = fresh;
125
+ return fresh;
126
+ }
127
+ }
128
+
19
129
  export class PolicyDenied extends Error {
20
- constructor(decision) {
130
+ constructor(decision, correlationId = null) {
21
131
  super(`policy denied: ${decision.reason}`);
22
132
  this.name = 'PolicyDenied';
23
133
  this.decision = decision;
134
+ this.correlationId = correlationId;
24
135
  }
25
136
  }
26
137
 
27
138
  export class ApprovalRequired extends Error {
28
- constructor(decision) {
139
+ constructor(decision, correlationId = null) {
29
140
  super(`approval required: ${decision.reason}`);
30
141
  this.name = 'ApprovalRequired';
31
142
  this.decision = decision;
143
+ this.correlationId = correlationId;
32
144
  }
33
145
  }
34
146
 
@@ -49,6 +161,13 @@ export class Broker {
49
161
  rateBudget = DEFAULT_RATE_BUDGET,
50
162
  rateWindowMs = DEFAULT_RATE_WINDOW_MS,
51
163
  now = () => Date.now(),
164
+ store,
165
+ approvalQueue,
166
+ deniedStore,
167
+ consumptionBudgets,
168
+ tenantContext,
169
+ auditRecorder = appendAuditRecord,
170
+ env = process.env,
52
171
  } = {}) {
53
172
  if (!rootDir) throw new Error('Broker: rootDir is required');
54
173
  this.rootDir = rootDir;
@@ -57,42 +176,170 @@ export class Broker {
57
176
  this.rateBudget = rateBudget;
58
177
  this.rateWindowMs = rateWindowMs;
59
178
  this.now = now;
179
+ // Durable store for rate-limit state.
180
+ this._storePath = path.join(rootDir, '.cx', 'broker-state.json');
181
+ this._store = store instanceof BrokerStore ? store : new BrokerStore();
182
+ if (!(store instanceof BrokerStore)) {
183
+ this._store.load(this._storePath);
184
+ }
185
+ // Approval queue for durable awaiting_approval state (per ADR-0056).
186
+ this.approvalQueue = approvalQueue || null;
187
+ // Durable store for denied decisions (LMCP-I3) — every PolicyDenied
188
+ // throw also lands a row here, keyed by decisionId, before the error
189
+ // leaves invoke().
190
+ this._deniedStore = deniedStore instanceof DeniedStore ? deniedStore : new DeniedStore({ rootDir });
191
+ // Durable per-actor/run consumption budgets (LMCP-N5) — checked pre-
192
+ // execution when a caller passes `runId`; a caller that never passes
193
+ // one sees no budget behavior at all (backward compatible).
194
+ this._consumptionBudgets = consumptionBudgets instanceof ConsumptionBudgetStore
195
+ ? consumptionBudgets
196
+ : new ConsumptionBudgetStore({ rootDir, env });
197
+ // Audit recorder is injectable so tests can capture records without
198
+ // touching the real ~/.cx/audit-trail.jsonl file.
199
+ this._auditRecorder = auditRecorder;
200
+ // Tenant resolved once per broker instance (env/config are stable for
201
+ // the process lifetime); reused for every decision record.
202
+ this._tenant = tenantContext || resolveTenantContext({ env, mode: getDeploymentMode(env, { cwd: rootDir }) }).tenantId;
203
+ // Keep this.calls as an in-memory Map for callers that read it directly
60
204
  this.calls = new Map();
61
205
  }
62
206
 
207
+ /**
208
+ * Attach an ApprovalQueue instance to this broker after construction.
209
+ */
210
+ setApprovalQueue(queue) {
211
+ this.approvalQueue = queue;
212
+ }
213
+
63
214
  _checkRate(role, tool) {
64
- const key = `${role}::${tool}`;
65
- const ts = this.now();
66
- const window = this.calls.get(key) || [];
67
- const fresh = window.filter((t) => ts - t < this.rateWindowMs);
215
+ const fresh = this._store.getRateLimitState(role, tool, this.rateWindowMs);
68
216
  if (fresh.length >= this.rateBudget) throw new RateLimited(role, tool, this.rateBudget);
69
- fresh.push(ts);
70
- this.calls.set(key, fresh);
217
+ this._store.incrementRateLimitState(role, tool, this.rateWindowMs);
218
+ // Persist after every update so the state is durable across restarts.
219
+ this._store.save(this._storePath);
220
+ // Keep the legacy Map in sync for any callers inspecting it directly.
221
+ this.calls.set(`${role}::${tool}`, this._store.getRateLimitState(role, tool, this.rateWindowMs));
222
+ }
223
+
224
+ // requestedBy already carries the H2 identity record (identityToRecord()
225
+ // shape: userId/serviceId/role/...) at every call site observed in
226
+ // lib/mcp/server.mjs and lib/writes/envelope.mjs. Fall back to role, then
227
+ // to 'unknown' rather than fabricating an identity that was never resolved.
228
+
229
+ _resolveActor(role, requestedBy) {
230
+ if (requestedBy && typeof requestedBy === 'object') {
231
+ const id = requestedBy.userId || requestedBy.serviceId;
232
+ if (id) return id;
233
+ }
234
+ return role || 'unknown';
235
+ }
236
+
237
+ /**
238
+ * Record a broker decision as durable evidence: a denied-store row when
239
+ * the outcome is 'denied' or 'budget_denied' (LMCP-N5 — a budget refusal
240
+ * is a denial under the same schema, just from a different gate than the
241
+ * policy engine), and an audit-trail entry for every outcome (allow/deny/
242
+ * approval/budget_denied) under the same schema. Both writes are
243
+ * best-effort — a disk failure here must never break tool dispatch, the
244
+ * same guarantee lib/mcp/server.mjs already gives its own audit call.
245
+ */
246
+ _recordDecision({ decision, outcome, role, tool, action, risk, project, requestedBy, correlationId }) {
247
+ const record = buildDecisionRecord({
248
+ decision,
249
+ outcome,
250
+ actor: this._resolveActor(role, requestedBy),
251
+ tenant: this._tenant,
252
+ project,
253
+ tool,
254
+ target: action ?? null,
255
+ risk,
256
+ correlationId,
257
+ ts: new Date(this.now()).toISOString(),
258
+ });
259
+
260
+ if (outcome === 'denied' || outcome === 'budget_denied') {
261
+ try { this._deniedStore.append(record); } catch { /* best-effort, mirrors BrokerStore.save */ }
262
+ }
263
+
264
+ try { this._auditRecorder({ agent: 'mcp-broker', ...record }); } catch { /* audit trail unavailable must not fail the call */ }
265
+
266
+ return record;
71
267
  }
72
268
 
73
269
  /**
74
270
  * Invoke a tool through the broker. The `execute` function does the
75
271
  * actual work; the broker decides whether to call it.
76
272
  *
273
+ * When approval is required, returns a structured `{ status, approvalId,
274
+ * resumeToken, expiresAt }` instead of throwing. Callers that retry with
275
+ * the same tool+args will match the existing approval record. After the
276
+ * record is approved (via CLI or API), a call with the same args executes
277
+ * the tool. Pass `resumeToken` explicitly to bypass argsHash dedup.
278
+ *
77
279
  * @param {object} args
78
280
  * @param {string} args.role
79
281
  * @param {string} args.tool
80
282
  * @param {string} args.action
283
+ * @param {object} [args.toolArgs] - Raw tool arguments for argsHash dedup
81
284
  * @param {string} [args.risk]
82
285
  * @param {string} [args.project]
83
286
  * @param {string} [args.traceId]
287
+ * @param {string} [args.resumeToken] - Explicit resume token (bypasses dedup)
288
+ * @param {object} [args.requestedBy] - Actor identity (identityToRecord() shape)
289
+ * @param {string} [args.runId] - LMCP-N5: attributes consumption to a run; omitted means no budget check at all
290
+ * @param {number} [args.tokenEstimate] - LMCP-N5: tokens this call is expected to consume, counted toward the run's token budget
291
+ * @param {object} [args.budget] - LMCP-N5: explicit budget override (defaults to the deployment-mode default)
84
292
  * @param {Function} args.execute
85
- * @returns {Promise<{result, decision}>}
293
+ * @returns {Promise<{result, decision, correlationId}|{status, approvalId, resumeToken, expiresAt, correlationId}>}
86
294
  */
87
- async invoke({ role, tool, action, risk, project, traceId, execute }) {
295
+ async invoke({ role, tool, action, toolArgs, risk, project, traceId, resumeToken, requestedBy, runId, tokenEstimate = 0, budget, execute }) {
88
296
  if (typeof execute !== 'function') throw new Error('Broker.invoke: execute function is required');
89
297
 
298
+ // If a resumeToken is provided, look up the approval record directly.
299
+ if (resumeToken && this.approvalQueue) {
300
+ const record = this.approvalQueue.getByResumeToken(resumeToken);
301
+ if (!record) return { status: 'not_found', resumeToken };
302
+ if (record.state === 'approved') {
303
+ const result = await execute();
304
+ return { result, decision: { allowed: true, reason: 'resumed after approval', approvalRequired: false, source: 'approval-queue' } };
305
+ }
306
+ if (record.state === 'denied') return { status: 'denied', approvalId: record.approvalId, reason: record.reason };
307
+ if (record.state === 'expired') return { status: 'expired', approvalId: record.approvalId };
308
+ return { status: 'awaiting_approval', approvalId: record.approvalId, resumeToken: record.resumeToken, expiresAt: record.expiresAt };
309
+ }
310
+
311
+ // Compute argsHash for dedup with the approval queue.
312
+ const argsHash = this.approvalQueue ? ApprovalQueue.hashToolCall(tool, toolArgs ?? {}) : null;
313
+
314
+ // Check if there is already an approval record for this exact tool call.
315
+ if (this.approvalQueue && argsHash) {
316
+ this.approvalQueue.expireStale();
317
+ const existing = this.approvalQueue.findByToolArgs(tool, argsHash);
318
+ if (existing) {
319
+ if (existing.state === 'approved') {
320
+ const result = await execute();
321
+ return { result, decision: { allowed: true, reason: 'approved via queue', approvalRequired: false, source: 'approval-queue' } };
322
+ }
323
+ if (existing.state === 'denied') return { status: 'denied', approvalId: existing.approvalId, reason: existing.reason };
324
+ if (existing.state === 'expired') return { status: 'expired', approvalId: existing.approvalId };
325
+ return { status: 'awaiting_approval', approvalId: existing.approvalId, resumeToken: existing.resumeToken, expiresAt: existing.expiresAt };
326
+ }
327
+ }
328
+
90
329
  const decision = this.policy({ role, project, tool, action, risk });
91
330
 
331
+ // correlationId ties this broker decision to the tool.called trace event
332
+ // (same value as traceId), and downstream to whatever run/task/external
333
+ // write the caller layers on top — reuse an inbound traceId so a caller
334
+ // that already has one keeps a single id across the whole chain, and
335
+ // mint a trace-shaped id only when the caller supplied none.
336
+
337
+ const correlationId = traceId || newTraceId();
338
+
92
339
  this.emit({
93
340
  rootDir: this.rootDir,
94
341
  eventType: 'tool.called',
95
- traceId,
342
+ traceId: correlationId,
96
343
  project,
97
344
  role,
98
345
  metadata: {
@@ -106,19 +353,87 @@ export class Broker {
106
353
  },
107
354
  });
108
355
 
109
- if (!decision.allowed) throw new PolicyDenied(decision);
110
- if (decision.approvalRequired) throw new ApprovalRequired(decision);
356
+ if (!decision.allowed) {
357
+ this._recordDecision({ decision, outcome: 'denied', role, tool, action, risk, project, requestedBy, correlationId });
358
+ throw new PolicyDenied(decision, correlationId);
359
+ }
360
+
361
+ if (decision.approvalRequired) {
362
+ this._recordDecision({ decision, outcome: 'approval_required', role, tool, action, risk, project, requestedBy, correlationId });
363
+ if (this.approvalQueue) {
364
+ const record = this.approvalQueue.enqueue({
365
+ tool,
366
+ args: toolArgs ?? {},
367
+ surface: 'mcp',
368
+ argsHash,
369
+ requestedBy: requestedBy ?? { role },
370
+ });
371
+ return {
372
+ status: 'awaiting_approval',
373
+ approvalId: record.approvalId,
374
+ resumeToken: record.resumeToken,
375
+ expiresAt: record.expiresAt,
376
+ correlationId,
377
+ };
378
+ }
379
+ // No queue configured — fall back to throwing for backward compat.
380
+ throw new ApprovalRequired(decision, correlationId);
381
+ }
382
+
383
+ this._recordDecision({ decision, outcome: 'allowed', role, tool, action, risk, project, requestedBy, correlationId });
384
+
385
+ // LMCP-N5: a caller with no runId gets no budget behavior at all — only
386
+ // an attributed run accrues durable consumption, so this is additive to
387
+ // every call site that predates runId.
388
+ if (runId) {
389
+ const actor = this._resolveActor(role, requestedBy);
390
+ const toolCallCheck = this._consumptionBudgets.check(actor, runId, 'toolCalls', 1, { budget });
391
+ const tokenCheck = tokenEstimate > 0
392
+ ? this._consumptionBudgets.check(actor, runId, 'tokens', tokenEstimate, { budget })
393
+ : { allowed: true };
394
+ const failed = !toolCallCheck.allowed ? { kind: 'toolCalls', check: toolCallCheck } : (!tokenCheck.allowed ? { kind: 'tokens', check: tokenCheck } : null);
395
+ if (failed) {
396
+ const budgetDecision = {
397
+ allowed: false,
398
+ approvalRequired: false,
399
+ reason: `consumption budget exceeded: ${failed.kind} would reach ${failed.check.projected} against a budget of ${failed.check.cap}`,
400
+ source: 'consumption-budget',
401
+ };
402
+ this._recordDecision({ decision: budgetDecision, outcome: 'budget_denied', role, tool, action, risk, project, requestedBy, correlationId });
403
+ throw new BudgetExceeded({ actor, runId, kind: failed.kind, cap: failed.check.cap, projected: failed.check.projected });
404
+ }
405
+ }
111
406
 
112
407
  this._checkRate(role, tool);
113
408
  const result = await execute();
114
- return { result, decision };
409
+
410
+ if (runId) {
411
+ const actor = this._resolveActor(role, requestedBy);
412
+ this._consumptionBudgets.record(actor, runId, 'toolCalls', 1);
413
+ if (tokenEstimate > 0) this._consumptionBudgets.record(actor, runId, 'tokens', tokenEstimate);
414
+ }
415
+
416
+ return { result, decision, correlationId };
115
417
  }
116
418
  }
117
419
 
118
- export function isBrokered(env = process.env) {
420
+ export { BudgetExceeded };
421
+
422
+ export function isBrokered(env = process.env, { cwd } = {}) {
119
423
  const override = env?.CONSTRUCT_MCP_BROKER;
120
424
  if (override === 'on') return true;
121
425
  if (override === 'off') return false;
122
- const mode = env?.CONSTRUCT_DEPLOYMENT_MODE || 'solo';
426
+
427
+ // Same env > config > default precedence as getDeploymentMode: 'auto' (or an
428
+ // absent key) falls through to the deployment-mode default below.
429
+
430
+ try {
431
+ const { config } = loadProjectConfig(cwd, env);
432
+ const fromConfig = config?.deployment?.mcpBroker;
433
+ if (fromConfig === 'on') return true;
434
+ if (fromConfig === 'off') return false;
435
+ } catch { /* loader is best-effort — fall through to deployment-mode default */ }
436
+
437
+ const mode = getDeploymentMode(env, { cwd });
123
438
  return mode === 'team' || mode === 'enterprise';
124
439
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * lib/mcp/denied-store.mjs — durable store for denied policy decisions.
3
+ *
4
+ * LMCP-I3: a PolicyDenied throw is a transient exception unless the decision
5
+ * that produced it is also written somewhere a reviewer can re-open later.
6
+ * DeniedStore appends one JSONL line per denied decision to
7
+ * `<rootDir>/.cx/denied-decisions.jsonl` — append-only, never rewritten, so a
8
+ * repeated-denial pattern for one actor+tool is visible as a policy-tuning
9
+ * signal without replaying the audit-trail chain. Every record uses the same
10
+ * schema as the audit-trail entry for the same decision — {decisionId, actor,
11
+ * tenant, project, tool, target, risk, outcome, correlationId, ts} — so the
12
+ * two stores can be cross-referenced by decisionId. Disk I/O is best-effort:
13
+ * a write failure never breaks the broker call it originated from.
14
+ *
15
+ * readAllForTenant (LMCP-H5) composes readAll with
16
+ * lib/tenant/isolation.mjs#readTenantScoped so any caller reading denied
17
+ * decisions for a specific tenant gets a fail-closed, tenant-scoped view —
18
+ * a record from another tenant is excluded, and an unresolved tenantId
19
+ * throws rather than silently returning the unfiltered store.
20
+ */
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+
25
+ import { readTenantScoped } from '../tenant/isolation.mjs';
26
+
27
+ const DENIED_STORE_SUBPATH = path.join('.cx', 'denied-decisions.jsonl');
28
+
29
+ export function deniedStorePath(rootDir) {
30
+ return path.join(rootDir, DENIED_STORE_SUBPATH);
31
+ }
32
+
33
+ export class DeniedStore {
34
+ constructor({ rootDir, file } = {}) {
35
+ if (!rootDir && !file) throw new Error('DeniedStore: rootDir or file is required');
36
+ this.file = file || deniedStorePath(rootDir);
37
+ }
38
+
39
+ /**
40
+ * Append a denied-decision record. Returns the record on success, or null
41
+ * if the write failed (disk full, permissions) — best-effort, never throws.
42
+ *
43
+ * @param {object} record - {decisionId, actor, tenant, project, tool, target, risk, outcome, correlationId, ts}
44
+ */
45
+ append(record) {
46
+ try {
47
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
48
+ fs.appendFileSync(this.file, `${JSON.stringify(record)}\n`, 'utf8');
49
+ return record;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Read all durable denied records. Returns [] if the store is absent or
57
+ * unreadable rather than throwing — a missing store means zero denials so
58
+ * far, not a hard error.
59
+ */
60
+ readAll() {
61
+ try {
62
+ const raw = fs.readFileSync(this.file, 'utf8');
63
+ return raw
64
+ .split('\n')
65
+ .filter(Boolean)
66
+ .map((line) => {
67
+ try { return JSON.parse(line); } catch { return null; }
68
+ })
69
+ .filter(Boolean);
70
+ } catch {
71
+ return [];
72
+ }
73
+ }
74
+
75
+ /** Find denied records matching decisionId. Linear scan — store is low-volume by design. */
76
+ findByDecisionId(decisionId) {
77
+ return this.readAll().filter((r) => r.decisionId === decisionId);
78
+ }
79
+
80
+ /** Find denied records matching correlationId, tying a denial back to its originating call. */
81
+ findByCorrelationId(correlationId) {
82
+ return this.readAll().filter((r) => r.correlationId === correlationId);
83
+ }
84
+
85
+ /**
86
+ * Tenant-scoped read: same records as readAll(), filtered to exactly the
87
+ * given tenantId. Fail-closed — throws TenantIsolationViolation rather
88
+ * than returning the unfiltered store when tenantId is missing/blank.
89
+ *
90
+ * @param {string} tenantId
91
+ */
92
+ readAllForTenant(tenantId) {
93
+ return readTenantScoped(() => this.readAll(), tenantId);
94
+ }
95
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * lib/mcp/destructive-gate.mjs — unified gate for destructive-class MCP tools.
3
+ *
4
+ * Replaces per-tool confirm:true booleans: a tool classified destructive in
5
+ * TOOL_SAFETY requires a valid out-of-band approval token (consumeApprovalToken)
6
+ * before the broker will dispatch it. Single choke point for all destructive tools.
7
+ */
8
+
9
+ import { TOOL_SAFETY } from './tool-safety.mjs';
10
+ import { consumeApprovalToken } from './destructive-approval.mjs';
11
+
12
+ const DESTRUCTIVE_CLASS = 'destructive';
13
+
14
+ export function checkDestructiveGate(toolName, toolArgs, opts = {}) {
15
+ const safety = TOOL_SAFETY[toolName];
16
+ if (!safety || safety.class !== DESTRUCTIVE_CLASS) {
17
+ return { gated: false, allowed: true };
18
+ }
19
+
20
+ const token = toolArgs?.approval_token;
21
+ if (!consumeApprovalToken(toolName, token)) {
22
+ return {
23
+ gated: true,
24
+ allowed: false,
25
+ reason: `${toolName} requires a valid out-of-band approval token (destructive tool; confirm=true alone is not authorization). Use 'construct tokens issue ${toolName}' or set approval_token.`,
26
+ };
27
+ }
28
+
29
+ return { gated: true, allowed: true };
30
+ }