@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
@@ -0,0 +1,72 @@
1
+ /**
2
+ * lib/team/health.mjs — team-mode queue and worker health read model.
3
+ */
4
+
5
+ import path from 'node:path';
6
+
7
+ import { createSqlClient, closeSqlClient } from '../storage/backend.mjs';
8
+ import { PostgresIntakeQueue } from '../queue/pg-queue.mjs';
9
+ import { WorkerRegistry } from '../orchestration/worker-runtime.mjs';
10
+ import { resolveIntakeTenantId, INTAKE_PROJECT_ENV_KEY, INTAKE_QUEUE_NAME_ENV_KEY } from '../intake/queue.mjs';
11
+
12
+ function resolveProject(rootDir, env = process.env) {
13
+ const explicit = env?.[INTAKE_PROJECT_ENV_KEY];
14
+ if (explicit && String(explicit).trim()) return String(explicit).trim();
15
+ return path.basename(path.resolve(rootDir || process.cwd())).trim() || 'construct';
16
+ }
17
+
18
+ function unavailable(reason) {
19
+ return {
20
+ status: 'unavailable',
21
+ reason,
22
+ queue: null,
23
+ workers: [],
24
+ summary: reason === 'postgres-unavailable'
25
+ ? 'Postgres unavailable; team queue/worker health cannot be read'
26
+ : reason,
27
+ };
28
+ }
29
+
30
+ export async function summarizeTeamHealth({
31
+ rootDir = process.cwd(),
32
+ env = process.env,
33
+ sql = null,
34
+ } = {}) {
35
+ const ownSql = sql || createSqlClient(env);
36
+ if (!ownSql) return unavailable('postgres-unavailable');
37
+
38
+ const project = resolveProject(rootDir, env);
39
+ const tenantId = resolveIntakeTenantId(env);
40
+ const queueName = env?.[INTAKE_QUEUE_NAME_ENV_KEY] || 'intake';
41
+ try {
42
+ const queue = new PostgresIntakeQueue({ sql: ownSql, project, tenantId, queueName });
43
+ const registry = new WorkerRegistry({ sql: ownSql, project, tenantId });
44
+ const [queueStats, workers] = await Promise.all([
45
+ queue.queueStats(),
46
+ registry.list({ includeStale: true }),
47
+ ]);
48
+ const staleWorkers = workers.filter((w) => w.stale || w.status === 'stale').length;
49
+ const activeWorkers = workers.filter((w) => w.status === 'active' && !w.stale).length;
50
+ const status = queueStats.deadLetter > 0 || staleWorkers > 0
51
+ ? 'degraded'
52
+ : 'healthy';
53
+ return {
54
+ status,
55
+ project,
56
+ tenantId,
57
+ queue: queueStats,
58
+ workers,
59
+ activeWorkers,
60
+ staleWorkers,
61
+ deadLetter: queueStats.deadLetter,
62
+ summary: `${queueStats.pending} pending · ${queueStats.claimed} claimed · ${queueStats.deadLetter} dead-letter · ${activeWorkers} active workers · ${staleWorkers} stale workers`,
63
+ };
64
+ } catch (err) {
65
+ return {
66
+ ...unavailable('team-health-read-failed'),
67
+ error: err?.message || String(err),
68
+ };
69
+ } finally {
70
+ if (!sql) await closeSqlClient(ownSql);
71
+ }
72
+ }
@@ -1,17 +1,23 @@
1
1
  /**
2
2
  * lib/telemetry/backends/local.mjs — Local JSONL trace backend.
3
3
  *
4
- * Reads trace events from ~/.cx/traces/*.jsonl files.
4
+ * Reads trace events from the calling project's machine-scoped state root
5
+ * (ADR-0066: `<stateRoot>/traces/*.jsonl`, keyed off cwd — the same location
6
+ * lib/worker/trace.mjs and lib/telemetry/client.mjs write to).
5
7
  * No remote calls. Always available. Used when CONSTRUCT_TRACE_BACKEND=local.
6
8
  */
7
9
 
8
10
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
9
11
  import { join } from 'node:path';
10
12
 
13
+ import { resolveStateDir } from '../../state-root.mjs';
14
+
11
15
  export const name = 'local';
12
16
 
13
- function tracesDir(env = process.env) {
14
- return join(env.CX_TOOLKIT_DIR || process.cwd(), '.cx', 'traces');
17
+ function tracesDir() {
18
+ // ensureDir:false listTraces is read-only and already guards on
19
+ // existsSync(); it must not conjure a traces/ dir just by being called.
20
+ return resolveStateDir(process.cwd(), 'traces', { ensureDir: false });
15
21
  }
16
22
 
17
23
  export async function isAvailable() {
@@ -4,12 +4,12 @@
4
4
  * Local JSONL trace capture is the default. Remote export is opt-in through
5
5
  * CONSTRUCT_TRACE_BACKEND and never throws into callers.
6
6
  */
7
- import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
7
+ import { appendFileSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
  import { randomUUID } from 'node:crypto';
10
10
 
11
11
  import { createIngestClient } from './ingest.mjs';
12
- import { ensureCxDir } from '../project-init-shared.mjs';
12
+ import { resolveStateDir } from '../state-root.mjs';
13
13
 
14
14
  const DEFAULT_MAX_BATCH = 50;
15
15
 
@@ -53,12 +53,8 @@ function traceShard() {
53
53
  function appendLocalTelemetry(type, body, { rootDir, env = process.env, onError = () => {} } = {}) {
54
54
  if (!localTraceEnabled(env)) return;
55
55
  const projectRoot = resolveProjectRoot({ rootDir, env });
56
- const dir = join(projectRoot, '.cx', 'traces');
56
+ const dir = resolveStateDir(projectRoot, 'traces');
57
57
  try {
58
- if (!existsSync(dir)) {
59
- ensureCxDir(projectRoot);
60
- mkdirSync(dir, { recursive: true });
61
- }
62
58
  appendFileSync(join(dir, `${traceShard()}.jsonl`), `${JSON.stringify({
63
59
  traceId: body?.traceId || body?.id || randomUUID(),
64
60
  spanId: body?.id || randomUUID(),
@@ -16,26 +16,24 @@
16
16
  * shipped name.
17
17
  */
18
18
 
19
+ // construct-rf26.11 folded 15 of the 29 legacy specialists into 11 surviving
20
+ // worker anchors. Each folded specialist's template ownership below moved to
21
+ // its anchor's entry (devil-advocate/evaluator/trace-reviewer -> reviewer;
22
+ // legal-compliance -> security; sre -> operations; accessibility -> designer)
23
+ // rather than being dropped, so the reverse-coverage gate still pins these
24
+ // artifacts to a real, callable get_template() reference in the anchor's prompt.
19
25
  export const SPECIALIST_TEMPLATES = {
20
- // Tranche 1 — five new templates authored alongside this map.
21
- 'cx-reviewer': ['code-review-report'],
22
- 'cx-security': ['security-audit-report'],
26
+ 'cx-reviewer': ['code-review-report', 'verdict'],
27
+ 'cx-security': ['security-audit-report', 'verdict'],
23
28
  'cx-qa': ['qa-report', 'test-plan', 'qa-strategy'],
24
29
  'cx-debugger': ['debug-investigation'],
25
- 'cx-devil-advocate': ['verdict'],
26
- 'cx-evaluator': ['verdict'],
27
- 'cx-trace-reviewer': ['verdict'],
28
- 'cx-explorer': ['verdict'],
29
- 'cx-legal-compliance':['verdict'],
30
30
 
31
31
  // Already-wired specialists pinned here so the gate guards them against drift.
32
32
  'cx-architect': ['adr', 'rfc', 'architecture-review'],
33
33
  'cx-researcher': ['research-brief', 'evidence-brief'],
34
- 'cx-sre': ['runbook', 'incident-report', 'postmortem'],
34
+ 'cx-operations': ['runbook', 'incident-report', 'postmortem'],
35
35
  'cx-product-manager': ['prd', 'meta-prd'],
36
-
37
- // Tranche 2 — three lower-frequency artifacts.
38
- 'cx-accessibility': ['accessibility-audit'],
36
+ 'cx-designer': ['accessibility-audit'],
39
37
  };
40
38
 
41
39
  // The orchestrator owns the task-packet shape and emits it at dispatch time
@@ -0,0 +1,82 @@
1
+ /**
2
+ * lib/tenant/context.mjs — tenant context resolution for the control plane.
3
+ *
4
+ * ADR-0057 (A7) IMPLEMENT-NOW: tenantId is data plumbing, not isolation.
5
+ * resolveTenantContext() is the single place a tenant id is derived from
6
+ * config + env and validated, so run/task/queue/audit records all carry the
7
+ * same value rather than each call site re-deriving it ad hoc. Solo and team
8
+ * modes default to the explicit tenant id 'local'. Enterprise mode has no
9
+ * default: a missing/blank tenant id is a fail-closed TenantResolutionError.
10
+ * No tenant isolation (worker/storage separation) is implied or attempted
11
+ * here — see LMCP-H4.
12
+ */
13
+
14
+ export const TENANT_ID_ENV_KEY = 'CONSTRUCT_TENANT_ID';
15
+ export const DEFAULT_TENANT_ID = 'local';
16
+
17
+ // A resolvable tenant id is a non-empty, trimmed string. Enterprise mode
18
+ // treats anything else (missing, blank, whitespace-only) as unresolved.
19
+
20
+ function normalizeTenantId(value) {
21
+ if (typeof value !== 'string') return null;
22
+ const trimmed = value.trim();
23
+ return trimmed ? trimmed : null;
24
+ }
25
+
26
+ export class TenantResolutionError extends Error {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = 'TenantResolutionError';
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Resolve the tenant context once, from config + env, validated against the
35
+ * deployment mode. Returns `{ tenantId, source, mode }`.
36
+ *
37
+ * - solo/team: an explicit id from env/config wins; otherwise defaults to
38
+ * 'local' (source: 'default'). Never throws.
39
+ * - enterprise: an explicit id from env/config is required. A missing or
40
+ * blank id throws TenantResolutionError — enterprise mode fails closed at
41
+ * startup rather than silently running unlabeled multi-tenant traffic.
42
+ *
43
+ * @param {object} [opts]
44
+ * @param {Record<string,string>} [opts.env]
45
+ * @param {object} [opts.config] loaded project config (reads deployment.tenantId)
46
+ * @param {string} [opts.mode] deployment mode; if omitted, callers must pass it
47
+ * explicitly — this module does not import deployment-mode.mjs to avoid a
48
+ * config-loading cycle (deployment-mode.mjs itself loads project config).
49
+ * @returns {{tenantId: string, source: 'env'|'config'|'default', mode: string}}
50
+ */
51
+ export function resolveTenantContext({ env = process.env, config = null, mode = 'solo' } = {}) {
52
+ const fromEnv = normalizeTenantId(env?.[TENANT_ID_ENV_KEY]);
53
+ const fromConfig = !fromEnv ? normalizeTenantId(config?.deployment?.tenantId) : null;
54
+ const resolved = fromEnv || fromConfig;
55
+
56
+ if (resolved) {
57
+ return { tenantId: resolved, source: fromEnv ? 'env' : 'config', mode };
58
+ }
59
+
60
+ if (mode === 'enterprise') {
61
+ throw new TenantResolutionError(
62
+ `enterprise mode requires a resolvable tenant id; set ${TENANT_ID_ENV_KEY} or deployment.tenantId in construct.config.json. ` +
63
+ 'Enterprise mode cannot start without a tenant — see ADR-0057 (A7).',
64
+ );
65
+ }
66
+
67
+ return { tenantId: DEFAULT_TENANT_ID, source: 'default', mode };
68
+ }
69
+
70
+ /**
71
+ * Fail-closed startup guard: call once at process/runtime start for enterprise
72
+ * mode. Re-throws TenantResolutionError with the same actionable message;
73
+ * solo/team never throw here since resolveTenantContext always resolves for
74
+ * them. Exposed separately so startup call sites can guard without needing
75
+ * the resolved value.
76
+ *
77
+ * @param {object} [opts] same shape as resolveTenantContext
78
+ * @returns {{tenantId: string, source: string, mode: string}}
79
+ */
80
+ export function requireTenantContext(opts = {}) {
81
+ return resolveTenantContext(opts);
82
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * lib/tenant/isolation.mjs — cross-tenant read guard for the control plane (LMCP-H5).
3
+ *
4
+ * lib/tenant/context.mjs (H1) resolves a tenant id; lib/identity.mjs (H2) resolves
5
+ * an actor; I3 landed durable audit/denied records that carry both. None of that
6
+ * enforces a boundary — a record tagged tenant "acme" is still just a JSON line a
7
+ * caller with tenant "other" can read if nothing stops them. Physical per-tenant
8
+ * storage backends are LMCP-H4 (not yet landed; queue/store today are scoped by
9
+ * rootDir, not tenant). Until H4 lands, this module is the enforcement layer that
10
+ * makes the tenant tag load-bearing everywhere it already exists:
11
+ *
12
+ * - assertTenantMatch(recordTenant, contextTenant) — throws TenantIsolationViolation
13
+ * (fail-closed) on any mismatch, and on a missing/blank tenant on either side.
14
+ * - scopeToTenant(records, tenantId) — filters a record list down to exactly the
15
+ * records whose `tenant`/`tenantId` field matches, fail-closed (throws) if
16
+ * tenantId itself is unresolved rather than silently returning everything.
17
+ * - readTenantScoped(readAll, tenantId) — composes a raw "read everything" API
18
+ * (queue.listPending, DeniedStore.readAll, readAuditTrail, a store's list*) with
19
+ * scopeToTenant so a caller gets a tenant-safe view over today's rootDir-scoped
20
+ * backends without waiting on H4's physical isolation.
21
+ *
22
+ * Scope: no physical isolation (no per-tenant encryption, no separate
23
+ * database/schema) — see H4 for that. The narrower, verifiable guarantee: any
24
+ * record path that stamps a tenant field cannot be read across a tenant boundary
25
+ * through these accessors, and is fail-closed (throws) rather than fail-open when
26
+ * the tenant dimension is missing or unresolved on either side.
27
+ */
28
+
29
+ export class TenantIsolationViolation extends Error {
30
+ constructor(message, { recordTenant, contextTenant } = {}) {
31
+ super(message);
32
+ this.name = 'TenantIsolationViolation';
33
+ this.recordTenant = recordTenant ?? null;
34
+ this.contextTenant = contextTenant ?? null;
35
+ }
36
+ }
37
+
38
+ function normalize(value) {
39
+ if (typeof value !== 'string') return null;
40
+ const trimmed = value.trim();
41
+ return trimmed ? trimmed : null;
42
+ }
43
+
44
+ /**
45
+ * Fail-closed equality check between a record's tenant and the requesting
46
+ * context's tenant. Throws TenantIsolationViolation on mismatch, and also on
47
+ * either side being missing/blank — an unresolved tenant is never treated as
48
+ * a wildcard that matches everything.
49
+ *
50
+ * @param {string} recordTenant
51
+ * @param {string} contextTenant
52
+ */
53
+ export function assertTenantMatch(recordTenant, contextTenant) {
54
+ const rec = normalize(recordTenant);
55
+ const ctx = normalize(contextTenant);
56
+
57
+ if (!ctx) {
58
+ throw new TenantIsolationViolation(
59
+ 'tenant isolation: requesting context has no resolvable tenant id — refusing read fail-closed',
60
+ { recordTenant: rec, contextTenant: ctx },
61
+ );
62
+ }
63
+ if (!rec) {
64
+ throw new TenantIsolationViolation(
65
+ 'tenant isolation: record has no resolvable tenant id — refusing read fail-closed',
66
+ { recordTenant: rec, contextTenant: ctx },
67
+ );
68
+ }
69
+ if (rec !== ctx) {
70
+ throw new TenantIsolationViolation(
71
+ `tenant isolation violation: record tenant "${rec}" is not readable from tenant "${ctx}" context`,
72
+ { recordTenant: rec, contextTenant: ctx },
73
+ );
74
+ }
75
+ }
76
+
77
+ // Records observed across queue/audit/denied-store use either `tenant` (I3
78
+ // decision-record schema: audit-trail, denied-store) or `tenantId` (H1 queue
79
+ // entries, orchestration run/task records) — checked in that order so one
80
+ // helper covers both shapes without callers needing to know which field name
81
+ // their backend uses.
82
+
83
+ function tenantFieldOf(record) {
84
+ if (record && typeof record === 'object') {
85
+ if ('tenant' in record) return record.tenant;
86
+ if ('tenantId' in record) return record.tenantId;
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Filter a record list down to exactly the records belonging to tenantId.
93
+ * Fail-closed: throws TenantIsolationViolation if tenantId itself is
94
+ * unresolved (missing/blank) rather than returning the unfiltered list —
95
+ * an isolation filter that no-ops on missing context is not a filter.
96
+ * Records with no resolvable tenant field are dropped, not leaked through.
97
+ *
98
+ * @param {object[]} records
99
+ * @param {string} tenantId
100
+ * @returns {object[]}
101
+ */
102
+ export function scopeToTenant(records, tenantId) {
103
+ const ctx = normalize(tenantId);
104
+ if (!ctx) {
105
+ throw new TenantIsolationViolation(
106
+ 'tenant isolation: scopeToTenant called with no resolvable tenant id — refusing to read any records',
107
+ { recordTenant: null, contextTenant: null },
108
+ );
109
+ }
110
+ return (records || []).filter((record) => normalize(tenantFieldOf(record)) === ctx);
111
+ }
112
+
113
+ /**
114
+ * Compose a raw "read everything" accessor with scopeToTenant, so a caller
115
+ * gets a tenant-safe view over a backend that has no native tenant filter
116
+ * (today's rootDir-scoped queue/store/audit readers). `readAll` may be sync
117
+ * or return a promise; the tenant filter is applied to whatever it resolves.
118
+ *
119
+ * @param {function(): (object[]|Promise<object[]>)} readAll
120
+ * @param {string} tenantId
121
+ * @returns {object[]|Promise<object[]>}
122
+ */
123
+ export function readTenantScoped(readAll, tenantId) {
124
+ const result = readAll();
125
+ if (result && typeof result.then === 'function') {
126
+ return result.then((records) => scopeToTenant(records, tenantId));
127
+ }
128
+ return scopeToTenant(result, tenantId);
129
+ }
@@ -80,6 +80,7 @@ export function defaultCorpusInventoryPath(rootDir = process.cwd()) {
80
80
  }
81
81
 
82
82
  function listTestFiles(testsDir) {
83
+ if (!fs.existsSync(testsDir)) return [];
83
84
  const files = [];
84
85
  const walk = (dir) => {
85
86
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -140,6 +141,101 @@ function extractCapabilities(content) {
140
141
  return [...content.matchAll(/@capability\s+([a-z][a-z0-9]*(?:[.-][a-z0-9]+)*)/g)].map((match) => match[1]);
141
142
  }
142
143
 
144
+ // `@embed <manifest-id>` is the embed-node analog of `@capability` (LMCP-P6):
145
+ // an acceptance test declares which embed preset it validates, so the graph can
146
+ // draw a test --validates--> embed edge without the id colliding with the
147
+ // capability id-space walked by extractCapabilities above.
148
+
149
+ function extractEmbedIds(content) {
150
+ return [...content.matchAll(/@embed\s+([a-z][a-z0-9]*(?:[.-][a-z0-9]+)*)/g)].map((match) => match[1]);
151
+ }
152
+
153
+ // Graph builders (lib/graph/build-from-corpus.mjs) need the raw path→capability
154
+ // pairs, not the full classified inventory, so this stays a thin extraction step
155
+ // callers can run over the live tree without regenerating the whole inventory.
156
+
157
+ export function extractCapabilityTestEdges({ rootDir } = {}) {
158
+ const root = findConstructRoot(rootDir);
159
+ const testsDir = path.join(root, 'tests');
160
+ const edges = [];
161
+ for (const absolute of listTestFiles(testsDir)) {
162
+ const rel = path.relative(root, absolute).replace(/\\/g, '/');
163
+ const content = fs.readFileSync(absolute, 'utf8');
164
+ for (const capabilityId of extractCapabilities(content)) edges.push({ testPath: rel, capabilityId });
165
+ }
166
+ return edges;
167
+ }
168
+
169
+ /**
170
+ * Raw path→embed-id pairs from `@embed` annotations, mirroring
171
+ * extractCapabilityTestEdges for the P6 embed-node graph seeder.
172
+ *
173
+ * @param {{ rootDir?: string }} [opts]
174
+ * @returns {Array<{ testPath: string, embedId: string }>}
175
+ */
176
+ export function extractEmbedTestEdges({ rootDir } = {}) {
177
+ const root = findConstructRoot(rootDir);
178
+ const testsDir = path.join(root, 'tests');
179
+ if (!fs.existsSync(testsDir)) return [];
180
+ const edges = [];
181
+ for (const absolute of listTestFiles(testsDir)) {
182
+ const rel = path.relative(root, absolute).replace(/\\/g, '/');
183
+ const content = fs.readFileSync(absolute, 'utf8');
184
+ for (const embedId of extractEmbedIds(content)) edges.push({ testPath: rel, embedId });
185
+ }
186
+ return edges;
187
+ }
188
+
189
+ // `@owasp LLM01[, LLM06]` tags a test with one or more OWASP GenAI Top-10
190
+ // category ids; `@secures <workflow-id>` names the workflow the test protects
191
+ // (LMCP-N8). Together they let the graph carry security coverage as
192
+ // test --secures--> workflow edges with the category recorded on the test node.
193
+
194
+ function extractOwaspCategories(content) {
195
+ const cats = new Set();
196
+ for (const m of content.matchAll(/@owasp\s+([A-Za-z0-9,\s]+)/g)) {
197
+ for (const raw of m[1].split(',')) {
198
+ const id = raw.trim().toUpperCase();
199
+ if (/^LLM(0[1-9]|10)$/.test(id)) cats.add(id);
200
+ }
201
+ }
202
+ return [...cats].sort();
203
+ }
204
+
205
+ function extractSecuredWorkflows(content) {
206
+ const ids = new Set();
207
+ for (const m of content.matchAll(/@secures\s+([a-z0-9,\s-]+)/g)) {
208
+ for (const raw of m[1].split(',')) {
209
+ const id = raw.trim();
210
+ if (/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(id)) ids.add(id);
211
+ }
212
+ }
213
+ return [...ids];
214
+ }
215
+
216
+ /**
217
+ * Security-test annotations for the N8 graph seeder: one entry per test that
218
+ * carries at least one `@owasp` tag, with its categories and any `@secures`
219
+ * workflow ids.
220
+ *
221
+ * @param {{ rootDir?: string }} [opts]
222
+ * @returns {Array<{ testPath: string, owasp: string[], secures: string[] }>}
223
+ */
224
+ export function extractSecurityTestEdges({ rootDir } = {}) {
225
+ const root = findConstructRoot(rootDir);
226
+ const testsDir = path.join(root, 'tests');
227
+ if (!fs.existsSync(testsDir)) return [];
228
+ const entries = [];
229
+ for (const absolute of listTestFiles(testsDir)) {
230
+ const content = fs.readFileSync(absolute, 'utf8');
231
+ const owasp = extractOwaspCategories(content);
232
+ if (owasp.length === 0) continue;
233
+ const rel = path.relative(root, absolute).replace(/\\/g, '/');
234
+ entries.push({ testPath: rel, owasp, secures: extractSecuredWorkflows(content) });
235
+ }
236
+ return entries;
237
+ }
238
+
143
239
  function detectSkipped(content) {
144
240
  return /\btest\.skip\s*\(|\bit\.skip\s*\(|\bdescribe\.skip\s*\(/.test(content);
145
241
  }
@@ -236,14 +332,20 @@ export function formatAuditAtAGlance(inventory) {
236
332
  export function refreshAuditMarkdown(inventory, { rootDir } = {}) {
237
333
  const root = findConstructRoot(rootDir);
238
334
  const auditPath = path.join(root, 'tests', 'AUDIT.md');
239
- const markdown = fs.readFileSync(auditPath, 'utf8');
335
+ let markdown = fs.readFileSync(auditPath, 'utf8');
240
336
  const start = '## At a glance';
241
337
  const end = '## Categories';
242
338
  const startIdx = markdown.indexOf(start);
243
339
  const endIdx = markdown.indexOf(end);
244
340
  if (startIdx === -1 || endIdx === -1) throw new Error('tests/AUDIT.md missing At a glance section');
245
341
  const replacement = `${start}\n\n${formatAuditAtAGlance(inventory)}\n\n`;
246
- return `${markdown.slice(0, startIdx)}${replacement}${markdown.slice(endIdx)}`;
342
+ markdown = `${markdown.slice(0, startIdx)}${replacement}${markdown.slice(endIdx)}`;
343
+ const functional = inventory.summary.byCategory.functional ?? 0;
344
+ markdown = markdown.replace(
345
+ /### 5\. Functional layer \(\d+ tests, \d+ files\)/,
346
+ `### 5. Functional layer (${functional} tests, ${functional} files)`,
347
+ );
348
+ return markdown;
247
349
  }
248
350
 
249
351
  export function validateCorpusInventory({ rootDir, inventory: supplied } = {}) {
@@ -20,6 +20,12 @@
20
20
  * `core.hooksPath` are reversed because they are state install/init created and
21
21
  * own; a user-customized hooksPath is left untouched.
22
22
  *
23
+ * Since construct-ranh moved Claude Code's MCP write path off
24
+ * ~/.claude/settings.json (never read by the CLI for server definitions),
25
+ * MCP cleanup checks both the new locations (~/.claude.json for machine
26
+ * scope, <project>/.mcp.json for project scope) and the legacy
27
+ * settings.json path, so both pre- and post-fix installs are covered.
28
+ *
23
29
  * Flags:
24
30
  * --dry-run print the plan, change nothing
25
31
  * --yes non-interactive; remove auto-risk items, skip ask-risk items
@@ -95,9 +101,11 @@ function buildCategories({ cwd, home, withImages = false }) {
95
101
  const userCxDir = doctorRoot(userHome);
96
102
  const launchAgentPlist = path.join(userHome, 'Library', 'LaunchAgents', `${PRESSURE_GUARD_LABEL}.plist`);
97
103
  const claudeSettings = path.join(userHome, '.claude', 'settings.json');
104
+ const claudeUserConfig = path.join(userHome, '.claude.json');
98
105
  const opencodeConfig = path.join(userHome, '.config', 'opencode', 'opencode.json');
99
106
  const codexConfig = path.join(userHome, '.codex', 'config.toml');
100
- const memoryConfigPaths = { claudeSettings, opencodeConfig, codexConfig };
107
+ const memoryConfigPaths = { claudeSettings, claudeUserConfig, opencodeConfig, codexConfig };
108
+ const dotMcpJson = path.join(cwd, '.mcp.json');
101
109
 
102
110
  return [
103
111
  {
@@ -143,10 +151,15 @@ function buildCategories({ cwd, home, withImages = false }) {
143
151
  id: 'project-settings',
144
152
  scope: 'project',
145
153
  risk: 'auto',
146
- label: '.claude/settings.json (un-merge Construct keys)',
147
- detect: () => detectConstructSettings(dotClaudeSettings),
148
- describe: () => 'Strips Construct hooks + known mcpServers from settings.json; preserves any user-added keys. Deletes the file if it becomes empty.',
149
- execute: () => unmergeSettings(dotClaudeSettings),
154
+ label: '.claude/settings.json + .mcp.json (un-merge Construct keys)',
155
+ detect: () => detectConstructSettings(dotClaudeSettings) || detectConstructMcpJson(dotMcpJson),
156
+ describe: () => 'Strips Construct hooks + known mcpServers from settings.json (legacy) and Construct-managed servers from .mcp.json (construct-ranh scope); preserves any user-added keys. Deletes a file if it becomes empty.',
157
+ execute: () => {
158
+ const parts = [unmergeSettings(dotClaudeSettings)];
159
+ const mcpJsonResult = unmergeMcpJson(dotMcpJson);
160
+ if (mcpJsonResult) parts.push(mcpJsonResult);
161
+ return parts.join('; ');
162
+ },
150
163
  },
151
164
  {
152
165
  id: 'project-cx',
@@ -263,7 +276,7 @@ function buildCategories({ cwd, home, withImages = false }) {
263
276
  risk: 'auto',
264
277
  label: 'memory MCP registration (Claude, OpenCode, Codex)',
265
278
  detect: () => memoryMcpRegistered(memoryConfigPaths),
266
- describe: () => 'Strips the Construct memory MCP bridge from ~/.claude/settings.json, ~/.config/opencode/opencode.json, and ~/.codex/config.toml. Preserves all other entries.',
279
+ describe: () => 'Strips the Construct memory MCP bridge from ~/.claude.json (construct-ranh scope) and ~/.claude/settings.json (legacy), plus ~/.config/opencode/opencode.json and ~/.codex/config.toml. Preserves all other entries.',
267
280
  execute: () => removeMemoryMcp(memoryConfigPaths),
268
281
  },
269
282
  {
@@ -472,6 +485,54 @@ function constructMcpKeys() {
472
485
  } catch { return []; }
473
486
  }
474
487
 
488
+ // Since construct-ranh, `construct sync` writes project-scope MCP servers to
489
+ // .mcp.json rather than .claude/settings.json — mirroring the same
490
+ // core-category + construct-mcp allowlist scripts/sync-specialists.mjs's
491
+ // PROJECT_DEFAULT_MCP_IDS derives from lib/mcp-catalog.json, so uninstall
492
+ // strips exactly what sync would have written, never a user's own project-scope
493
+ // `claude mcp add` entries.
494
+
495
+ function projectManagedMcpIds() {
496
+ try {
497
+ const catalog = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'lib', 'mcp-catalog.json'), 'utf8'));
498
+ const arr = catalog.mcps || catalog.servers || [];
499
+ return new Set([...arr.filter((m) => m.category === 'core').map((m) => m.id), 'construct-mcp']);
500
+ } catch { return new Set(['context7', 'construct-mcp']); }
501
+ }
502
+
503
+ function detectConstructMcpJson(filePath) {
504
+ if (!fs.existsSync(filePath)) return false;
505
+ let config;
506
+ try { config = JSON.parse(fs.readFileSync(filePath, 'utf8')); }
507
+ catch { return false; }
508
+ if (!config || typeof config !== 'object' || !config.mcpServers) return false;
509
+ const ids = projectManagedMcpIds();
510
+ return Object.keys(config.mcpServers).some((k) => ids.has(k));
511
+ }
512
+
513
+ function unmergeMcpJson(filePath) {
514
+ if (!fs.existsSync(filePath)) return null;
515
+ let config;
516
+ try { config = JSON.parse(fs.readFileSync(filePath, 'utf8')); }
517
+ catch { return '.mcp.json was malformed; left untouched'; }
518
+ if (!config || typeof config !== 'object' || !config.mcpServers) return null;
519
+
520
+ let removedCount = 0;
521
+ for (const key of projectManagedMcpIds()) {
522
+ if (key in config.mcpServers) { delete config.mcpServers[key]; removedCount += 1; }
523
+ }
524
+ if (removedCount === 0) return null;
525
+ if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
526
+
527
+ const remaining = Object.keys(config);
528
+ if (remaining.length === 0) {
529
+ fs.unlinkSync(filePath);
530
+ return `.mcp.json removed (file was Construct-only) [stripped ${removedCount} mcpServers]`;
531
+ }
532
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
533
+ return `.mcp.json stripped ${removedCount} mcpServers; preserved ${remaining.join(', ')}`;
534
+ }
535
+
475
536
  function dockerContainerExists(name) {
476
537
  const probe = spawnSync('docker', ['ps', '-a', '--filter', `name=^/${name}$`, '--format', '{{.Names}}'], {
477
538
  encoding: 'utf8',
@@ -545,8 +606,9 @@ function removePressureGuardLaunchAgent(plistPath) {
545
606
  return removed ? 'unregistered and removed plist' : 'unregistered (plist already gone)';
546
607
  }
547
608
 
548
- function memoryMcpRegistered({ claudeSettings, opencodeConfig, codexConfig }) {
609
+ function memoryMcpRegistered({ claudeSettings, claudeUserConfig, opencodeConfig, codexConfig }) {
549
610
  return claudeHasMemoryMcp(claudeSettings)
611
+ || claudeHasMemoryMcp(claudeUserConfig)
550
612
  || opencodeHasMemoryMcp(opencodeConfig)
551
613
  || codexHasMemoryMcp(codexConfig);
552
614
  }
@@ -569,13 +631,17 @@ function codexHasMemoryMcp(filePath) {
569
631
  return MEMORY_MCP_KEYS.some((key) => text.includes(`[mcp_servers.${key}]`) || text.includes(`[mcp_servers.${tomlString(key)}]`));
570
632
  }
571
633
 
572
- // The memory MCP is registered into all three host configs by `construct mcp add
573
- // memory` during install; each host is stripped independently so a missing or
574
- // hand-edited config never blocks the others.
634
+ // The memory MCP is registered into Claude, OpenCode, and Codex by `construct
635
+ // mcp add memory` during install; each host is stripped independently so a
636
+ // missing or hand-edited config never blocks the others. Claude has two
637
+ // possible locations: ~/.claude.json is where construct-ranh moved the write
638
+ // path (Claude Code never reads MCP servers from settings.json), and
639
+ // ~/.claude/settings.json is stripped too for installs made before that fix.
575
640
 
576
- function removeMemoryMcp({ claudeSettings, opencodeConfig, codexConfig }) {
641
+ function removeMemoryMcp({ claudeSettings, claudeUserConfig, opencodeConfig, codexConfig }) {
577
642
  const stripped = [];
578
- if (stripMemoryFromJsonMcp(claudeSettings, 'mcpServers')) stripped.push('claude');
643
+ if (stripMemoryFromJsonMcp(claudeSettings, 'mcpServers')) stripped.push('claude settings.json (legacy)');
644
+ if (stripMemoryFromJsonMcp(claudeUserConfig, 'mcpServers')) stripped.push('claude.json');
579
645
  if (stripMemoryFromJsonMcp(opencodeConfig, 'mcp')) stripped.push('opencode');
580
646
  if (stripMemoryFromCodex(codexConfig)) stripped.push('codex');
581
647
  return stripped.length ? `stripped from ${stripped.join(', ')}` : 'nothing to strip';