@geraldmaron/construct 1.4.2 → 1.5.1

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 +57 -45
  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 +273 -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 +104 -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,16 +8,24 @@
8
8
  * engine returns allow-without-approval everywhere.
9
9
  *
10
10
  * Decision precedence:
11
- * 1. Team-level decisions gate specialist decisions forbidden by team blocks the action.
12
- * 2. Explicit deny in the role's fence denied + typed reason.
13
- * 3. Action falls in the role's approvalRequired list → allowed but
11
+ * 0. Mandatory-audit gate (LMCP-H5)enterprise only: if the audit sink is
12
+ * down, every action is denied fail-closed before any other check runs.
13
+ * 1. Identity-anchored role authority (LMCP-I6) in team/enterprise, an
14
+ * env-claimed role not among the identity's registered grants is
15
+ * denied before any manifest is consulted.
16
+ * 2. Team-level decisions gate specialist decisions — forbidden by team blocks the action.
17
+ * 3. Explicit deny in the role's fence → denied + typed reason.
18
+ * 4. Action falls in the role's approvalRequired list → allowed but
14
19
  * approvalRequired = true (UI must collect human consent).
15
- * 4. risk === 'high' and role is not in HIGH_RISK_AUTONOMOUS → approval required.
16
- * 5. Otherwiseallowed.
20
+ * 5. risk === 'high' and role is not in HIGH_RISK_AUTONOMOUS → approval required.
21
+ * 6. Deny-by-default: team deny unless explicit grant; enterprise → deny always.
22
+ * 7. solo → allowed.
17
23
  */
18
24
 
19
25
  import { readFileSync, existsSync } from 'node:fs';
20
26
  import { loadRegistry } from '../registry/loader.mjs';
27
+ import { authorizeRoleClaim } from './role-authority.mjs';
28
+ import { enforceMandatoryAudit } from './audit-gate.mjs';
21
29
 
22
30
  const HIGH_RISK_AUTONOMOUS = new Set(['security', 'sre', 'release-manager']);
23
31
 
@@ -97,6 +105,25 @@ function manifestFor(role, manifests) {
97
105
  return manifests?.personas?.[role] || null;
98
106
  }
99
107
 
108
+ /**
109
+ * Return the default decision for unclassified (no matching explicit rule) tool calls.
110
+ *
111
+ * - solo → 'allow' (unchanged permissive behaviour)
112
+ * - team → 'deny-unclassified' (deny unless an explicit grant exists or the
113
+ * user escapes with an approval flow)
114
+ * - enterprise → 'deny' (hard deny regardless of approval escape)
115
+ *
116
+ * @param {'solo'|'team'|'enterprise'} deploymentMode
117
+ * @returns {'allow'|'deny-unclassified'|'deny'}
118
+ */
119
+ export function getDefaultDecision(deploymentMode) {
120
+ switch (deploymentMode) {
121
+ case 'enterprise': return 'deny';
122
+ case 'team': return 'deny-unclassified';
123
+ default: return 'allow'; // solo and unknown → permissive
124
+ }
125
+ }
126
+
100
127
  function actionMatches(pattern, action) {
101
128
  if (!pattern || !action) return false;
102
129
  if (pattern === action) return true;
@@ -125,15 +152,41 @@ function needsApprovalFromManifest(action, manifest) {
125
152
  * @param {string} input.action
126
153
  * @param {string} [input.decision] — optional decision id for team-level gate
127
154
  * @param {string} [input.risk]
155
+ * @param {'solo'|'team'|'enterprise'} [input.deploymentMode='solo'] — controls deny-by-default behaviour
156
+ * @param {object} [input.identity] — resolved actor/service identity (lib/identity.mjs). In
157
+ * team/enterprise, `input.role` is validated as a claim against this identity's registered
158
+ * grants (see lib/policy/role-authority.mjs) before any manifest lookup.
128
159
  * @param {object} [opts]
160
+ * @param {function} [opts.checkSink] — injectable audit-sink probe for LMCP-H5 tests;
161
+ * defaults to the real fs-backed checkAuditSinkAvailable.
129
162
  * @returns {{allowed: boolean, reason: string, approvalRequired: boolean, source: string}}
130
163
  */
131
164
  export function policyDecision(input = {}, opts = {}) {
132
- const { role, tool, action, decision, risk = 'low' } = input;
165
+ const { role, tool, action, decision, risk = 'low', deploymentMode = 'solo', identity } = input;
133
166
  if (!role) return { allowed: false, reason: 'role is required', approvalRequired: false, source: 'engine' };
134
167
  if (!tool) return { allowed: false, reason: 'tool is required', approvalRequired: false, source: 'engine' };
135
168
  if (!action) return { allowed: false, reason: 'action is required', approvalRequired: false, source: 'engine' };
136
169
 
170
+ const auditGateDecision = enforceMandatoryAudit({
171
+ deploymentMode,
172
+ ...(opts.checkSink ? { checkSink: opts.checkSink } : {}),
173
+ });
174
+ if (auditGateDecision) return auditGateDecision;
175
+
176
+ if ((deploymentMode === 'team' || deploymentMode === 'enterprise') && identity) {
177
+ if (identity.source === 'implicit-solo') {
178
+ return { allowed: false, reason: `Identity not resolved for ${deploymentMode} mode`, approvalRequired: false, source: 'identity-boundary' };
179
+ }
180
+
181
+ // Identity-anchored authority (LMCP-I6): the role claim under evaluation
182
+ // must be among the identity's registered grants — an env-declared role
183
+ // with no matching grant is denied before any manifest lookup runs.
184
+ const authority = authorizeRoleClaim({ identity, role, deploymentMode });
185
+ if (!authority.authorized) {
186
+ return { allowed: false, reason: authority.reason, approvalRequired: false, source: authority.source };
187
+ }
188
+ }
189
+
137
190
  const manifests = opts.manifests || loadRoleManifests(opts.manifestPath);
138
191
  const manifest = manifestFor(role, manifests);
139
192
 
@@ -186,6 +239,28 @@ export function policyDecision(input = {}, opts = {}) {
186
239
  };
187
240
  }
188
241
 
242
+ // Deny-by-default for team and enterprise modes.
243
+ // No explicit rule granted this action — consult the deployment-mode default.
244
+ const defaultDecision = getDefaultDecision(deploymentMode);
245
+ if (defaultDecision === 'deny') {
246
+ return {
247
+ allowed: false,
248
+ reason: 'deny-by-default',
249
+ approvalRequired: false,
250
+ mode: deploymentMode,
251
+ source: 'deny-by-default',
252
+ };
253
+ }
254
+ if (defaultDecision === 'deny-unclassified') {
255
+ return {
256
+ allowed: false,
257
+ reason: 'deny-by-default',
258
+ approvalRequired: false,
259
+ mode: deploymentMode,
260
+ source: 'deny-by-default',
261
+ };
262
+ }
263
+
189
264
  return {
190
265
  allowed: true,
191
266
  reason: `action "${action}" permitted for role "${role}"`,
@@ -0,0 +1,89 @@
1
+ /**
2
+ * lib/policy/role-authority.mjs — identity-anchored role claim authority.
3
+ *
4
+ * A6/ADR-0056 requires policy decisions to bind to actor/service identity
5
+ * (H2, lib/identity.mjs), not to a self-declared env var. Prior to this
6
+ * module, `CONSTRUCT_ROLE` alone selected the role manifest a caller was
7
+ * evaluated against — a worker could export the env var and claim any
8
+ * role's authority, including `security` or `release-manager`, with no
9
+ * check against who the identity actually is.
10
+ *
11
+ * `authorizeRoleClaim` closes that gap: it takes the resolved identity
12
+ * (from resolveIdentity()) and the role claim under evaluation, and
13
+ * decides whether the identity is entitled to act as that role.
14
+ *
15
+ * Registered-grants source: `identity.grants` — a list of role ids the
16
+ * identity is registered for. This is the seam LMCP-G5 (worker
17
+ * registration/heartbeat) and pack `toolGrantsRequested` (LMCP-E1) populate;
18
+ * neither exists as a live registration store yet, so until G5 lands the
19
+ * only way an identity acquires a grant is an explicit `identity.grants`
20
+ * array set by the caller that resolved the identity (e.g. from a signed
21
+ * header, a worker registration record, or test fixture). No implicit
22
+ * grants are invented here — an identity with no `grants` array has none.
23
+ *
24
+ * Mode behavior:
25
+ * - solo: always authorized. CONSTRUCT_ROLE remains a documented
26
+ * solo-only convenience (lib/identity.mjs env fallback); there is no
27
+ * registration authority to check against in single-user mode.
28
+ * - team / enterprise: the claimed role must be present in the
29
+ * identity's `grants`. A role claim sourced from `env-fallback` (an
30
+ * env var with no actor identity headers at all) is never itself
31
+ * trusted as a grant — it is a claim, and claims are checked, not
32
+ * trusted. Missing or non-matching grants are DENIED (fail closed).
33
+ */
34
+
35
+ export class RoleAuthorityError extends Error {
36
+ constructor(message) { super(message); this.name = 'RoleAuthorityError'; }
37
+ }
38
+
39
+ /**
40
+ * Return the set of role ids an identity is registered for.
41
+ *
42
+ * `identity.grants` is the only source read today (see file header for
43
+ * why). Absent/malformed values resolve to an empty set — never to "all
44
+ * roles" — so a caller who forgets to populate grants fails closed rather
45
+ * than silently trusting the claim.
46
+ *
47
+ * @param {object} identity
48
+ * @returns {Set<string>}
49
+ */
50
+ export function resolveIdentityGrants(identity) {
51
+ const grants = identity?.grants;
52
+ if (!Array.isArray(grants)) return new Set();
53
+ return new Set(grants.filter((g) => typeof g === 'string' && g.length > 0));
54
+ }
55
+
56
+ /**
57
+ * Decide whether `role` is an authorized claim for `identity` under
58
+ * `deploymentMode`.
59
+ *
60
+ * @param {object} input
61
+ * @param {object} input.identity - resolved identity (lib/identity.mjs shape)
62
+ * @param {string} input.role - the role claim under evaluation
63
+ * @param {'solo'|'team'|'enterprise'} [input.deploymentMode='solo']
64
+ * @returns {{authorized: boolean, reason: string, source: string}}
65
+ */
66
+ export function authorizeRoleClaim({ identity, role, deploymentMode = 'solo' } = {}) {
67
+ if (!role) {
68
+ return { authorized: false, reason: 'role claim is required', source: 'role-authority' };
69
+ }
70
+
71
+ if (deploymentMode !== 'team' && deploymentMode !== 'enterprise') {
72
+ return { authorized: true, reason: 'solo mode: env-claimed role permitted (documented convenience)', source: 'solo-ergonomics' };
73
+ }
74
+
75
+ if (!identity) {
76
+ return { authorized: false, reason: `no identity resolved to validate role claim "${role}" in ${deploymentMode} mode`, source: 'role-authority' };
77
+ }
78
+
79
+ const grants = resolveIdentityGrants(identity);
80
+ if (grants.has(role)) {
81
+ return { authorized: true, reason: `role "${role}" is a registered grant for this identity`, source: 'identity-grant' };
82
+ }
83
+
84
+ return {
85
+ authorized: false,
86
+ reason: `role claim "${role}" is not in this identity's registered grants in ${deploymentMode} mode`,
87
+ source: 'role-authority.ungranted',
88
+ };
89
+ }
@@ -39,7 +39,7 @@ import { estimateTokens, estimatePromptTokens, estimateTokensSync } from './toke
39
39
  import { cxDir } from './paths.mjs';
40
40
  import { getStrategyDigestSync } from './strategy-store.mjs';
41
41
  import { loadRegistry } from './registry/loader.mjs';
42
- import { bindingForSpecialist, resolveRoleOverlayId } from './roles/flavor-bindings.mjs';
42
+ import { bindingForSpecialist, bindingsForSpecialistId, resolveRoleOverlayId } from './roles/flavor-bindings.mjs';
43
43
 
44
44
  const MAX_OBSERVATIONS = 3;
45
45
 
@@ -254,12 +254,23 @@ export function composePrompt(agentName, {
254
254
  }
255
255
 
256
256
  const shortName = String(agentName).replace(/^cx-/, '');
257
- const flavorMapping = bindingForSpecialist(shortName);
258
257
 
259
258
  // When workspaceType is provided and roleFlavors doesn't already specify this agent's flavor,
260
259
  // auto-select the matching overlay so agents get domain guidance without requiring explicit caller config.
261
260
  const effectiveRoleFlavors = resolveRoleFlavors(roleFlavors, workspaceType, shortName);
262
261
 
262
+ // Since construct-rf26.11, one specialist can carry several flavor bindings
263
+ // (e.g. cx-engineer owns engineer/ai-engineer/platform-engineer/data-engineer
264
+ // overlays). Prefer whichever candidate binding's classifierKey the caller
265
+ // actually named in roleFlavors over the specialist's own bare-name binding,
266
+ // so a flavor-specific request (roleFlavors.platformEngineer) still resolves
267
+ // even though bindingForSpecialist(shortName) alone would only ever return
268
+ // the base 'engineer' entry.
269
+ const candidates = bindingsForSpecialistId(agentName);
270
+ const flavorMapping = (effectiveRoleFlavors
271
+ && candidates.find((b) => effectiveRoleFlavors[b.classifierKey]))
272
+ || bindingForSpecialist(shortName);
273
+
263
274
  if (flavorMapping && effectiveRoleFlavors) {
264
275
  const flavor = effectiveRoleFlavors[flavorMapping.classifierKey];
265
276
  if (flavor) {
@@ -276,7 +287,12 @@ export function composePrompt(agentName, {
276
287
  type: 'role-flavor',
277
288
  priority: PRIORITY['role-flavor'],
278
289
  label: roleId,
279
- content: `### ${flavor === 'core' ? shortName : flavor} domain guidance\n\n${overlayContent}`,
290
+ // flavorMapping.rolePrefix (not the agent's own shortName) names the
291
+ // domain guidance: since construct-rf26.11 one specialist can carry
292
+ // several flavor bindings (e.g. cx-engineer), and shortName only
293
+ // ever reflects the specialist's own bare name, not which of its
294
+ // several possible overlays actually matched.
295
+ content: `### ${flavor === 'core' ? flavorMapping.rolePrefix : flavor} domain guidance\n\n${overlayContent}`,
280
296
  tokenBudget: selectedProfile.roleFlavorTokens,
281
297
  });
282
298
  }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * lib/providers/contract/adapters/confluence/governed-write.mjs —
3
+ * envelope-shaped Confluence write adapter.
4
+ *
5
+ * Wraps a Confluence REST transport in the provider shape
6
+ * lib/writes/envelope.mjs expects: write(config, payload), meta.id,
7
+ * search(). This is the only exposed entry point for Confluence writes —
8
+ * specialists and CLI callers route through writeWithEnvelope(), never
9
+ * through a raw Confluence client, so dedup, retry, dry-run, approval, and
10
+ * audit stay centralized in the envelope (LMCP-J2) rather than being
11
+ * reimplemented per-adapter.
12
+ *
13
+ * Adds the three behaviors a generic envelope cannot provide:
14
+ *
15
+ * - Duplicate detection by title+space: before create, searches the
16
+ * target space for an existing page with the same title (via the
17
+ * injected `confluenceTransport.searchPagesByTitle`). A match returns a
18
+ * `page-duplicate` result carrying a `linkback` to the existing page
19
+ * instead of creating a second page — mirrors the GitHub adapter's
20
+ * marker-search dedup (LMCP-J-github) but keyed on title+space rather
21
+ * than a hidden marker, since Confluence page search has no analogue to
22
+ * GitHub's full-text issue search over a hidden comment.
23
+ * - Version-conflict recovery on update: Confluence page updates require
24
+ * the current `version.number`; if the page changed since the caller
25
+ * last fetched it, the API returns 409. On 409 this adapter refetches
26
+ * the live page, re-renders the caller's update against the fresh
27
+ * version, and returns a `version-conflict` result carrying the fresh
28
+ * page and a `retryPayload` — the caller (or a human via the envelope's
29
+ * approval path) re-approves before the adapter retries the write. The
30
+ * adapter never blindly overwrites a page that changed underneath it.
31
+ * - Linkback comment: after a successful create or update, posts a footer
32
+ * comment on the page recording the write's idempotency key, so anyone
33
+ * reading the page in Confluence can trace which governed write
34
+ * produced or last touched it.
35
+ */
36
+
37
+ import { AuthError } from '../../errors.mjs';
38
+
39
+ const LINKBACK_PREFIX = 'construct:write:';
40
+
41
+ function linkbackComment(idempotencyKey) {
42
+ return `<p>Published via Construct governed write (${LINKBACK_PREFIX}${idempotencyKey}).</p>`;
43
+ }
44
+
45
+ /**
46
+ * @param {object} opts
47
+ * @param {object} opts.confluenceTransport - underlying Confluence REST transport; must implement
48
+ * `searchPagesByTitle(spaceId, title)`, `getPage(pageId)`, `createPage(fields)`,
49
+ * `updatePage(fields)`, `createFooterComment(pageId, body)`.
50
+ */
51
+ export function createGovernedConfluenceProvider({ confluenceTransport } = {}) {
52
+ if (!confluenceTransport) throw new Error('createGovernedConfluenceProvider: confluenceTransport is required');
53
+
54
+ async function postLinkback(pageId, idempotencyKey) {
55
+ if (!idempotencyKey) return;
56
+ try {
57
+ await confluenceTransport.createFooterComment(pageId, linkbackComment(idempotencyKey));
58
+ } catch {
59
+ // Linkback is best-effort audit trail on the page itself; the
60
+ // envelope's sent-log is the authoritative record, so a comment
61
+ // failure must not fail an otherwise-successful publish.
62
+ }
63
+ }
64
+
65
+ async function writePage(payload) {
66
+ const { spaceId, title, body, parentId, idempotencyKey } = payload;
67
+ if (!spaceId) throw new Error('confluence governed write: payload.spaceId is required for type "page"');
68
+ if (!title) throw new Error('confluence governed write: payload.title is required for type "page"');
69
+
70
+ let existing;
71
+ try {
72
+ existing = await confluenceTransport.searchPagesByTitle(spaceId, title);
73
+ } catch (err) {
74
+ throw mapWriteTransportError(err, { spaceId });
75
+ }
76
+
77
+ const match = (existing ?? []).find((p) => p.title === title);
78
+ if (match) {
79
+ return {
80
+ type: 'page-duplicate',
81
+ id: match.id,
82
+ title: match.title,
83
+ url: match.url,
84
+ linkback: match.url,
85
+ };
86
+ }
87
+
88
+ try {
89
+ const result = await confluenceTransport.createPage({ spaceId, title, body, parentId });
90
+ await postLinkback(result.id, idempotencyKey);
91
+ return { type: 'page-created', id: result.id, title: result.title, version: result.version, url: result.url };
92
+ } catch (err) {
93
+ throw mapWriteTransportError(err, { spaceId });
94
+ }
95
+ }
96
+
97
+ async function writePageUpdate(payload) {
98
+ const { pageId, title, body, version, idempotencyKey } = payload;
99
+ if (!pageId) throw new Error('confluence governed write: payload.pageId is required for type "page-update"');
100
+ if (version === undefined || version === null) {
101
+ throw new Error('confluence governed write: payload.version is required for type "page-update"');
102
+ }
103
+
104
+ try {
105
+ const result = await confluenceTransport.updatePage({ pageId, title, body, version });
106
+ await postLinkback(pageId, idempotencyKey);
107
+ return { type: 'page-updated', id: result.id, title: result.title, version: result.version, url: result.url };
108
+ } catch (err) {
109
+ if (err?.status === 409) {
110
+ const fresh = await confluenceTransport.getPage(pageId);
111
+ return {
112
+ type: 'version-conflict',
113
+ id: pageId,
114
+ url: fresh.url,
115
+ currentVersion: fresh.version,
116
+ currentBody: fresh.body,
117
+ currentTitle: fresh.title,
118
+ retryPayload: {
119
+ type: 'page-update',
120
+ pageId,
121
+ title: title ?? fresh.title,
122
+ body,
123
+ version: fresh.version,
124
+ idempotencyKey,
125
+ },
126
+ };
127
+ }
128
+ throw mapWriteTransportError(err, { pageId });
129
+ }
130
+ }
131
+
132
+ return {
133
+ meta: {
134
+ id: 'confluence',
135
+ displayName: 'Confluence (governed)',
136
+ capabilities: ['write', 'search'],
137
+ description: 'Envelope-routed Confluence writes with title+space dedup, version-conflict recovery, and linkback comments.',
138
+ },
139
+
140
+ async write(config, payload) {
141
+ if (payload?.type === 'page') return writePage(payload);
142
+ if (payload?.type === 'page-update') return writePageUpdate(payload);
143
+ throw new Error(`confluence governed write: unsupported type "${payload?.type}" (only 'page' and 'page-update' are supported)`);
144
+ },
145
+
146
+ async search(config, query) {
147
+ const spaceId = typeof query === 'object' ? query?.spaceId : undefined;
148
+ const title = typeof query === 'object' ? query?.title : query;
149
+ return confluenceTransport.searchPagesByTitle(spaceId, title);
150
+ },
151
+
152
+ /**
153
+ * Render the payload the envelope would submit, without calling search,
154
+ * getPage, or the transport. Feeds the envelope's dry-run path
155
+ * (lib/writes/envelope.mjs `dryRun: true`), giving a human reviewing a
156
+ * pending write the exact storage-format body and target coordinates
157
+ * before anything is sent.
158
+ *
159
+ * @param {object} payload
160
+ * @returns {{ type: string, fields: object }}
161
+ */
162
+ renderDryRun(payload) {
163
+ if (payload?.type === 'page') {
164
+ return {
165
+ type: 'page',
166
+ fields: {
167
+ spaceId: payload.spaceId,
168
+ title: payload.title,
169
+ parentId: payload.parentId,
170
+ body: { representation: 'storage', value: payload.body },
171
+ },
172
+ };
173
+ }
174
+ if (payload?.type === 'page-update') {
175
+ return {
176
+ type: 'page-update',
177
+ fields: {
178
+ pageId: payload.pageId,
179
+ title: payload.title,
180
+ version: { number: payload.version },
181
+ body: { representation: 'storage', value: payload.body },
182
+ },
183
+ };
184
+ }
185
+ throw new Error(`confluence governed write: cannot render dry-run for unsupported type "${payload?.type}"`);
186
+ },
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Map a create/update/search transport failure (401/403/404) to an
192
+ * actionable message. 409 (version conflict) is handled by the caller
193
+ * directly and never reaches this mapper.
194
+ *
195
+ * @param {Error & {status?: number}} err
196
+ * @param {{ spaceId?: string, pageId?: string }} ctx
197
+ * @returns {Error}
198
+ */
199
+ function mapWriteTransportError(err, ctx) {
200
+ const status = err?.status;
201
+ if (status === 401) {
202
+ return new AuthError('Confluence authentication failed. Check CONFLUENCE_EMAIL / CONFLUENCE_TOKEN.', { provider: 'confluence' });
203
+ }
204
+ if (status === 403) {
205
+ const target = ctx.spaceId ? `space "${ctx.spaceId}"` : `page "${ctx.pageId}"`;
206
+ return new Error(`Forbidden: the authenticated account lacks permission to write to ${target}. Check the confluence-content:write scope.`);
207
+ }
208
+ if (status === 404) {
209
+ const target = ctx.spaceId ? `Space "${ctx.spaceId}"` : `Page "${ctx.pageId}"`;
210
+ return new Error(`${target} was not found, or the authenticated account cannot access it.`);
211
+ }
212
+ return err instanceof Error ? err : new Error(String(err));
213
+ }
214
+
215
+ export default createGovernedConfluenceProvider;
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "confluence-write",
3
+ "version": "1.0.0",
4
+ "kind": "data-source",
5
+ "capabilities": ["write", "search"],
6
+ "secretEnvKeys": ["CONFLUENCE_URL", "CONFLUENCE_EMAIL", "CONFLUENCE_TOKEN"],
7
+ "operations": ["write:page", "write:page-update", "search"],
8
+ "securityClassification": "trusted",
9
+ "owner": "construct-core",
10
+ "compatVersion": 1,
11
+ "installSource": "builtin",
12
+ "exposure": "envelope-only",
13
+ "envelopeModule": "lib/writes/envelope.mjs",
14
+ "adapterModule": "lib/providers/contract/adapters/confluence/governed-write.mjs",
15
+ "transportModule": "lib/providers/contract/adapters/confluence/transport.mjs",
16
+ "notes": "Colocated write-capability manifest for LMCP-J4. Follows the lib/extensions/manifests/*.manifest.json schema (see atlassian-confluence.manifest.json, which declares read/search only). Wiring this into the shared lib/extensions/manifests/ discovery directory is out of scope for this bead; capabilities here are reachable only via writeWithEnvelope(), never a direct provider-registry lookup."
17
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * lib/providers/contract/adapters/confluence/transport.mjs — real Confluence
3
+ * Cloud REST v2 transport for the governed write adapter.
4
+ *
5
+ * Implements the `confluenceTransport` shape governed-write.mjs depends on:
6
+ * `searchPagesByTitle`, `getPage`, `createPage`, `updatePage`,
7
+ * `createFooterComment`. Kept separate from governed-write.mjs so tests can
8
+ * inject a fake transport (tests/fakes/fake-confluence-transport.mjs)
9
+ * without any network dependency, and separate from the read-oriented
10
+ * adapter in ./index.mjs so this write path never shares mutable state with
11
+ * the read path.
12
+ *
13
+ * Auth: API token via CONFLUENCE_URL + CONFLUENCE_EMAIL + CONFLUENCE_TOKEN
14
+ * env vars, or passed directly via config.
15
+ */
16
+
17
+ import { AuthError, RateLimitError } from '../../errors.mjs';
18
+ import { guardedFetch } from '../../../../net-guard.mjs';
19
+
20
+ export function createConfluenceTransport(config = {}) {
21
+ const baseUrl = (config.baseUrl ?? process.env.CONFLUENCE_URL ?? '').replace(/\/$/, '');
22
+ const email = config.email ?? process.env.CONFLUENCE_EMAIL;
23
+ const token = config.token ?? process.env.CONFLUENCE_TOKEN;
24
+
25
+ if (!baseUrl || !email || !token) {
26
+ throw new AuthError(
27
+ 'Confluence transport requires CONFLUENCE_URL, CONFLUENCE_EMAIL, and CONFLUENCE_TOKEN (or config.baseUrl/email/token)',
28
+ { provider: 'confluence' },
29
+ );
30
+ }
31
+
32
+ const auth = `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
33
+
34
+ // Egress runs through the N7 guard (SSRF/rebinding); a private-address
35
+ // self-hosted instance is an explicit, audited opt-in, never the default.
36
+ const allowPrivate = config.allowPrivateEgress ?? process.env.CONSTRUCT_NET_ALLOW_PRIVATE_EGRESS === '1';
37
+
38
+ async function request(path, { method = 'GET', body } = {}) {
39
+ const res = await guardedFetch(`${baseUrl}${path}`, {
40
+ method,
41
+ headers: {
42
+ Authorization: auth,
43
+ 'Content-Type': 'application/json',
44
+ Accept: 'application/json',
45
+ },
46
+ body: body ? JSON.stringify(body) : undefined,
47
+ }, { allowPrivate });
48
+
49
+ if (res.status === 429) {
50
+ const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60', 10);
51
+ throw new RateLimitError('Confluence rate limit hit', { provider: 'confluence', retryAfter });
52
+ }
53
+ if (!res.ok) {
54
+ const text = await res.text().catch(() => '');
55
+ const err = new Error(`Confluence API ${method} ${path} failed: ${res.status} ${text.slice(0, 300)}`);
56
+ err.status = res.status;
57
+ throw err;
58
+ }
59
+ if (res.status === 204) return null;
60
+ return res.json();
61
+ }
62
+
63
+ return {
64
+ async searchPagesByTitle(spaceId, title) {
65
+ const params = new URLSearchParams({ title, 'space-id': spaceId, limit: '10' });
66
+ const data = await request(`/wiki/api/v2/pages?${params.toString()}`);
67
+ return (data.results ?? []).map((p) => ({
68
+ id: p.id,
69
+ title: p.title,
70
+ spaceId: p.spaceId,
71
+ version: p.version?.number ?? null,
72
+ url: `${baseUrl}${p._links?.webui ?? ''}`,
73
+ }));
74
+ },
75
+
76
+ async getPage(pageId) {
77
+ const data = await request(`/wiki/api/v2/pages/${pageId}?body-format=storage`);
78
+ return {
79
+ id: data.id,
80
+ title: data.title,
81
+ spaceId: data.spaceId,
82
+ version: data.version?.number ?? null,
83
+ body: data.body?.storage?.value ?? '',
84
+ url: `${baseUrl}${data._links?.webui ?? ''}`,
85
+ };
86
+ },
87
+
88
+ async createPage({ spaceId, title, body, parentId }) {
89
+ const reqBody = {
90
+ spaceId,
91
+ status: 'current',
92
+ title,
93
+ parentId: parentId ?? undefined,
94
+ body: { representation: 'storage', value: body },
95
+ };
96
+ const data = await request('/wiki/api/v2/pages', { method: 'POST', body: reqBody });
97
+ return {
98
+ id: data.id,
99
+ title: data.title,
100
+ version: data.version?.number ?? 1,
101
+ url: `${baseUrl}${data._links?.webui ?? ''}`,
102
+ };
103
+ },
104
+
105
+ async updatePage({ pageId, title, body, version }) {
106
+ const reqBody = {
107
+ id: pageId,
108
+ status: 'current',
109
+ title,
110
+ version: { number: version },
111
+ body: { representation: 'storage', value: body },
112
+ };
113
+ const data = await request(`/wiki/api/v2/pages/${pageId}`, { method: 'PUT', body: reqBody });
114
+ return {
115
+ id: data.id,
116
+ title: data.title,
117
+ version: data.version?.number ?? version,
118
+ url: `${baseUrl}${data._links?.webui ?? ''}`,
119
+ };
120
+ },
121
+
122
+ async createFooterComment(pageId, body) {
123
+ const data = await request('/wiki/api/v2/footer-comments', {
124
+ method: 'POST',
125
+ body: { pageId, body: { representation: 'storage', value: body } },
126
+ });
127
+ return {
128
+ id: data.id,
129
+ url: `${baseUrl}${data._links?.webui ?? ''}`,
130
+ };
131
+ },
132
+ };
133
+ }
134
+
135
+ export default createConfluenceTransport;