@geraldmaron/construct 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (556) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1029 -65
  4. package/bin/construct-postinstall.mjs +27 -2
  5. package/config/tag-vocabulary.json +264 -0
  6. package/examples/distribution/sources/deck-one-pager.md +1 -1
  7. package/lib/acp/server.mjs +21 -7
  8. package/lib/adapters-sync.mjs +2 -1
  9. package/lib/audit-trail.mjs +185 -2
  10. package/lib/beads/auto-close.mjs +2 -1
  11. package/lib/beads/drift.mjs +2 -1
  12. package/lib/bridges/copilot-proxy.mjs +20 -5
  13. package/lib/certification/skill-inventory.mjs +10 -1
  14. package/lib/cli/approvals.mjs +147 -0
  15. package/lib/cli-commands.mjs +105 -14
  16. package/lib/comment-lint.mjs +127 -8
  17. package/lib/config/schema.mjs +7 -30
  18. package/lib/config/source-target-registry.mjs +70 -0
  19. package/lib/config/source-targets.mjs +179 -205
  20. package/lib/contracts/coverage.mjs +77 -0
  21. package/lib/contracts/validate.mjs +102 -13
  22. package/lib/contracts/violation-log.mjs +50 -4
  23. package/lib/db/migrate.mjs +69 -0
  24. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  25. package/lib/db/migrations/002_queue_provider.sql +50 -0
  26. package/lib/db/migrations/003_worker_registry.sql +17 -0
  27. package/lib/db/migrations/004_trace_events.sql +16 -0
  28. package/lib/db/migrations/005_shared_memory.sql +15 -0
  29. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  30. package/lib/decisions/enforced-baseline.json +0 -2
  31. package/lib/decisions/registry.mjs +3 -2
  32. package/lib/deployment/parity-contract.mjs +1 -1
  33. package/lib/deployment-mode.mjs +78 -2
  34. package/lib/diagram-export.mjs +10 -1
  35. package/lib/distill.mjs +5 -2
  36. package/lib/docs-verify.mjs +2 -1
  37. package/lib/doctor/cli.mjs +1 -0
  38. package/lib/doctor/command-on-path.mjs +24 -0
  39. package/lib/doctor/diagnosis.mjs +109 -0
  40. package/lib/doctor/embedding-health.mjs +50 -0
  41. package/lib/doctor/engine-health.mjs +93 -0
  42. package/lib/doctor/graph-validate.mjs +47 -0
  43. package/lib/doctor/index.mjs +23 -4
  44. package/lib/doctor/sidecar-providers.mjs +56 -0
  45. package/lib/doctor/watchers/consistency.mjs +93 -1
  46. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  47. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  48. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  49. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  50. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  51. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  52. package/lib/document-export.mjs +52 -27
  53. package/lib/document-extract/docling-client.mjs +9 -5
  54. package/lib/document-extract/docling-sidecar.py +30 -0
  55. package/lib/document-extract.mjs +1 -1
  56. package/lib/document-ingest.mjs +9 -0
  57. package/lib/embed/approval-queue.mjs +184 -88
  58. package/lib/embed/authority-guard.mjs +65 -2
  59. package/lib/embed/capability-jobs.mjs +365 -0
  60. package/lib/embed/capability-lifecycle.mjs +219 -0
  61. package/lib/embed/capability-loader.mjs +303 -0
  62. package/lib/embed/capability-runtime.mjs +58 -0
  63. package/lib/embed/cli.mjs +185 -8
  64. package/lib/embed/config.mjs +4 -0
  65. package/lib/embed/daemon.mjs +125 -6
  66. package/lib/embed/demand-fetch.mjs +190 -54
  67. package/lib/embed/inbox.mjs +12 -10
  68. package/lib/embed/presets/ops-triage.mjs +236 -0
  69. package/lib/embed/presets/pm-feedback.mjs +341 -0
  70. package/lib/embed/presets/tpm.mjs +401 -0
  71. package/lib/embed/providers/jira.mjs +72 -1
  72. package/lib/embed/providers/registry.mjs +140 -33
  73. package/lib/embed/worker.mjs +5 -0
  74. package/lib/embedded-contract/capability.mjs +4 -0
  75. package/lib/embedded-contract/execution.mjs +1 -1
  76. package/lib/embedded-contract/model-resolve.mjs +53 -3
  77. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  78. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  79. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  80. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  81. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  82. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  83. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  84. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  85. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  86. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  88. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  90. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  91. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  92. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  93. package/lib/env-config.mjs +60 -11
  94. package/lib/export-validate.mjs +34 -2
  95. package/lib/extensions/index.mjs +27 -0
  96. package/lib/extensions/loader.mjs +120 -0
  97. package/lib/extensions/manifest-schema.mjs +141 -0
  98. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  99. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  100. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  101. package/lib/extensions/manifests/directory.manifest.json +12 -0
  102. package/lib/extensions/manifests/docling.manifest.json +37 -0
  103. package/lib/extensions/manifests/echo.manifest.json +8 -0
  104. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  105. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  106. package/lib/extensions/manifests/github.manifest.json +52 -0
  107. package/lib/extensions/manifests/linear.manifest.json +50 -0
  108. package/lib/extensions/manifests/local.manifest.json +12 -0
  109. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  110. package/lib/extensions/manifests/openai.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  113. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  114. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  115. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  116. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  117. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  118. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  119. package/lib/extensions/manifests/slack.manifest.json +58 -0
  120. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  121. package/lib/extensions/validate.mjs +175 -0
  122. package/lib/features.mjs +13 -9
  123. package/lib/flows/checkpoint.mjs +184 -0
  124. package/lib/flows/constants.mjs +27 -0
  125. package/lib/flows/define.mjs +146 -0
  126. package/lib/flows/engine.mjs +249 -0
  127. package/lib/flows/errors.mjs +19 -0
  128. package/lib/flows/index.mjs +27 -0
  129. package/lib/flows/joins.mjs +18 -0
  130. package/lib/flows/schema.mjs +90 -0
  131. package/lib/flows/state.mjs +31 -0
  132. package/lib/frameworks/loader.mjs +99 -0
  133. package/lib/frameworks/schema.mjs +157 -0
  134. package/lib/graph/build-from-corpus.mjs +67 -0
  135. package/lib/graph/build-from-embed.mjs +82 -0
  136. package/lib/graph/build-from-registry.mjs +164 -3
  137. package/lib/graph/cli.mjs +456 -12
  138. package/lib/graph/gap-queries.mjs +156 -0
  139. package/lib/graph/gaps.mjs +41 -0
  140. package/lib/graph/impacted.mjs +129 -0
  141. package/lib/graph/runtime-evidence.mjs +177 -0
  142. package/lib/graph/security-coverage.mjs +113 -0
  143. package/lib/graph/staleness.mjs +241 -10
  144. package/lib/graph/store.mjs +24 -7
  145. package/lib/graph/validate.mjs +161 -0
  146. package/lib/headhunt.mjs +9 -8
  147. package/lib/health-check.mjs +15 -7
  148. package/lib/hook-health.mjs +1 -1
  149. package/lib/hooks/agent-tracker.mjs +10 -4
  150. package/lib/hooks/edit-guard.mjs +3 -3
  151. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  152. package/lib/hooks/guard-bash.mjs +41 -15
  153. package/lib/hooks/mcp-health-check.mjs +11 -4
  154. package/lib/hooks/model-fallback.mjs +50 -6
  155. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  156. package/lib/hooks/pre-compact.mjs +181 -173
  157. package/lib/hooks/rule-verifier.mjs +2 -1
  158. package/lib/hooks/session-reflect.mjs +2 -1
  159. package/lib/hooks/session-start.mjs +3 -3
  160. package/lib/host/readiness.mjs +109 -0
  161. package/lib/host-capabilities.mjs +13 -2
  162. package/lib/host-disposition.mjs +29 -8
  163. package/lib/identity.mjs +92 -0
  164. package/lib/ingest/degraded-extract.mjs +96 -0
  165. package/lib/ingest/docling-remote.mjs +2 -1
  166. package/lib/ingest/provider-extract.mjs +7 -19
  167. package/lib/ingest/sidecar-providers.mjs +196 -0
  168. package/lib/ingest-tooling.mjs +11 -5
  169. package/lib/init-unified.mjs +55 -38
  170. package/lib/init-update.mjs +2 -1
  171. package/lib/install/legacy-global-cleanup.mjs +25 -0
  172. package/lib/install/stage-project.mjs +41 -4
  173. package/lib/intake/daemon.mjs +99 -8
  174. package/lib/intake/git-queue.mjs +110 -38
  175. package/lib/intake/prepare.mjs +2 -0
  176. package/lib/intake/queue-registry.mjs +50 -0
  177. package/lib/intake/queue.mjs +133 -17
  178. package/lib/intake/session-prelude.mjs +57 -8
  179. package/lib/integrations/intake-integrations.mjs +61 -111
  180. package/lib/intent-classifier.mjs +43 -5
  181. package/lib/libreoffice-export.mjs +8 -8
  182. package/lib/logging/rotate.mjs +5 -4
  183. package/lib/mcp/broker.mjs +332 -17
  184. package/lib/mcp/denied-store.mjs +95 -0
  185. package/lib/mcp/destructive-approval.mjs +57 -0
  186. package/lib/mcp/destructive-gate.mjs +30 -0
  187. package/lib/mcp/dispatch-envelope.mjs +199 -0
  188. package/lib/mcp/memory-bridge.mjs +2 -1
  189. package/lib/mcp/server.mjs +185 -1268
  190. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  191. package/lib/mcp/tool-definitions-project.mjs +271 -0
  192. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  193. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  194. package/lib/mcp/tool-definitions.mjs +25 -0
  195. package/lib/mcp/tool-rate-limit.mjs +47 -0
  196. package/lib/mcp/tool-registry.mjs +107 -0
  197. package/lib/mcp/tool-safety.mjs +95 -0
  198. package/lib/mcp/tool-surface-parity.mjs +60 -0
  199. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  200. package/lib/mcp/tools/orchestration-run.mjs +197 -19
  201. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  202. package/lib/mcp/tools/project.mjs +25 -8
  203. package/lib/mcp/tools/provider-write.mjs +187 -0
  204. package/lib/mcp/tools/scope.mjs +0 -3
  205. package/lib/mcp/tools/skills.mjs +1 -1
  206. package/lib/mcp/tools/storage.mjs +2 -2
  207. package/lib/mcp/tools/web-search-governance.mjs +96 -0
  208. package/lib/mcp/tools/web-search.mjs +5 -76
  209. package/lib/mcp/transport/auth.mjs +238 -0
  210. package/lib/mcp/transport/http.mjs +136 -0
  211. package/lib/mcp/transport/mode.mjs +31 -0
  212. package/lib/mcp/transport/stdio.mjs +22 -0
  213. package/lib/mcp-catalog.json +4 -4
  214. package/lib/mcp-manager.mjs +34 -23
  215. package/lib/mcp-platform-config.mjs +53 -20
  216. package/lib/mode-capabilities.mjs +109 -0
  217. package/lib/model-router.mjs +42 -9
  218. package/lib/models/catalog.mjs +40 -1
  219. package/lib/net-guard.mjs +247 -0
  220. package/lib/observation-store.mjs +6 -2
  221. package/lib/ollama/provision-context.mjs +2 -1
  222. package/lib/opencode-config.mjs +22 -3
  223. package/lib/opencode-runtime-plugin.mjs +120 -2
  224. package/lib/oracle/actions.mjs +204 -10
  225. package/lib/oracle/cli.mjs +24 -3
  226. package/lib/oracle/daemon-entry.mjs +6 -0
  227. package/lib/oracle/dispatch.mjs +2 -1
  228. package/lib/oracle/execute.mjs +2 -2
  229. package/lib/oracle/gaps.mjs +3 -3
  230. package/lib/oracle/heartbeat.mjs +53 -0
  231. package/lib/oracle/read-model.mjs +69 -13
  232. package/lib/oracle/reconcile.mjs +3 -46
  233. package/lib/oracle/routing.mjs +37 -31
  234. package/lib/oracle/synthesize.mjs +1 -1
  235. package/lib/orchestration/classification.mjs +434 -0
  236. package/lib/orchestration/delegation-flow.mjs +132 -0
  237. package/lib/orchestration/flow-selection.mjs +418 -0
  238. package/lib/orchestration/gates.mjs +244 -0
  239. package/lib/orchestration/host-sampling.mjs +121 -0
  240. package/lib/orchestration/policy-constants.mjs +48 -0
  241. package/lib/orchestration/provider-outcome.mjs +197 -0
  242. package/lib/orchestration/readiness.mjs +114 -13
  243. package/lib/orchestration/run-store-postgres.mjs +32 -26
  244. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  245. package/lib/orchestration/run-store.mjs +25 -12
  246. package/lib/orchestration/runtime.mjs +508 -62
  247. package/lib/orchestration/store.mjs +87 -12
  248. package/lib/orchestration/trace-store.mjs +125 -0
  249. package/lib/orchestration/web-capability.mjs +60 -0
  250. package/lib/orchestration/worker-runtime.mjs +162 -0
  251. package/lib/orchestration/worker.mjs +763 -80
  252. package/lib/orchestration-policy.mjs +67 -1111
  253. package/lib/output-quality.mjs +61 -2
  254. package/lib/packs/cli.mjs +144 -0
  255. package/lib/packs/core-pack.mjs +112 -0
  256. package/lib/packs/enablement.mjs +187 -0
  257. package/lib/packs/index.mjs +23 -0
  258. package/lib/packs/loader.mjs +176 -0
  259. package/lib/packs/manifest-schema.mjs +58 -0
  260. package/lib/packs/prompts.mjs +120 -0
  261. package/lib/packs/validate.mjs +208 -0
  262. package/lib/path-policy.mjs +56 -0
  263. package/lib/plugin-registry.mjs +4 -5
  264. package/lib/policy/audit-gate.mjs +42 -0
  265. package/lib/policy/consumption-budget.mjs +149 -0
  266. package/lib/policy/engine.mjs +81 -6
  267. package/lib/policy/role-authority.mjs +89 -0
  268. package/lib/prompt-composer.js +19 -3
  269. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  270. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  271. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  272. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  273. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  274. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  275. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  276. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  277. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  278. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  279. package/lib/providers/contract.mjs +207 -0
  280. package/lib/providers/credential-bootstrap.mjs +7 -4
  281. package/lib/providers/credential-sources.mjs +14 -2
  282. package/lib/providers/directory/index.mjs +261 -0
  283. package/lib/providers/feedback/index.mjs +392 -0
  284. package/lib/providers/filter-audit.mjs +68 -0
  285. package/lib/providers/filter-schema.mjs +54 -0
  286. package/lib/providers/github/index.mjs +31 -0
  287. package/lib/providers/instance-config.mjs +215 -0
  288. package/lib/providers/op-locate.mjs +96 -0
  289. package/lib/providers/op-run.mjs +64 -9
  290. package/lib/providers/registry.mjs +26 -3
  291. package/lib/providers/secret-audit-wiring.mjs +41 -18
  292. package/lib/providers/secret-resolver.mjs +263 -31
  293. package/lib/publish-template.mjs +6 -3
  294. package/lib/publish-tooling.mjs +4 -0
  295. package/lib/publish.mjs +32 -4
  296. package/lib/queue/pg-queue.mjs +395 -0
  297. package/lib/reflect.mjs +2 -1
  298. package/lib/registry/assemble.mjs +28 -2
  299. package/lib/registry/catalog.mjs +6 -0
  300. package/lib/registry/consolidation.mjs +8 -1
  301. package/lib/registry/custom-scaffold.mjs +200 -0
  302. package/lib/registry/custom-schema.mjs +137 -0
  303. package/lib/registry/loader.mjs +15 -4
  304. package/lib/registry/manifests/format-engines.default.json +15 -0
  305. package/lib/registry/manifests/surface-map.default.json +46 -0
  306. package/lib/registry/retired-paths.mjs +1 -0
  307. package/lib/registry/surface-map.mjs +100 -50
  308. package/lib/registry/validate.mjs +3 -3
  309. package/lib/render-visual-check.mjs +240 -0
  310. package/lib/resources/budget.mjs +40 -17
  311. package/lib/roles/flavor-bindings.mjs +36 -14
  312. package/lib/roles/gateway.mjs +15 -8
  313. package/lib/roots.mjs +107 -0
  314. package/lib/runtime/uv-bootstrap.mjs +32 -6
  315. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  316. package/lib/runtime-env.mjs +5 -2
  317. package/lib/sandbox.mjs +1 -1
  318. package/lib/scheduler/index.mjs +8 -20
  319. package/lib/schema-infer.mjs +31 -2
  320. package/lib/scopes/lifecycle.mjs +29 -25
  321. package/lib/scopes/loader.mjs +49 -12
  322. package/lib/scopes/teams.mjs +1 -1
  323. package/lib/security/ingest-boundary.mjs +80 -0
  324. package/lib/security/recall-wrapper.mjs +99 -0
  325. package/lib/security/trust.mjs +132 -0
  326. package/lib/service-manager.mjs +93 -24
  327. package/lib/setup.mjs +41 -23
  328. package/lib/skills-apply.mjs +4 -2
  329. package/lib/specialists/postconditions.mjs +2 -2
  330. package/lib/state-root.mjs +147 -0
  331. package/lib/status.mjs +597 -6
  332. package/lib/storage/admin.mjs +84 -7
  333. package/lib/storage/backend-registry.mjs +73 -0
  334. package/lib/storage/backend.mjs +63 -9
  335. package/lib/storage/embeddings-openai.mjs +12 -2
  336. package/lib/storage/hybrid-query.mjs +11 -3
  337. package/lib/storage/retrieval-hardening.mjs +384 -0
  338. package/lib/storage/shared-memory.mjs +158 -0
  339. package/lib/storage/vector-client.mjs +69 -2
  340. package/lib/team/health.mjs +72 -0
  341. package/lib/telemetry/backends/local.mjs +9 -3
  342. package/lib/telemetry/client.mjs +3 -7
  343. package/lib/template-registry.mjs +10 -12
  344. package/lib/tenant/context.mjs +82 -0
  345. package/lib/tenant/isolation.mjs +129 -0
  346. package/lib/test-corpus-inventory.mjs +103 -2
  347. package/lib/uninstall/uninstall.mjs +78 -12
  348. package/lib/validator.mjs +4 -6
  349. package/lib/worker/entrypoint.mjs +2 -1
  350. package/lib/worker/run.mjs +2 -2
  351. package/lib/worker/trace.mjs +5 -11
  352. package/lib/workflow-state.mjs +52 -8
  353. package/lib/workflows/liveness.mjs +203 -0
  354. package/lib/workflows/loader.mjs +177 -0
  355. package/lib/workflows/manifest-schema.mjs +71 -0
  356. package/lib/workflows/surface-parity.mjs +118 -0
  357. package/lib/workflows/validate.mjs +127 -0
  358. package/lib/writes/control-plane.mjs +140 -0
  359. package/lib/writes/envelope.mjs +206 -0
  360. package/lib/writes/sent-log.mjs +86 -0
  361. package/lib/writes/write-intent.mjs +109 -0
  362. package/package.json +20 -7
  363. package/personas/construct.md +12 -3
  364. package/registry/agent-manifest.json +117 -0
  365. package/registry/capabilities.json +1987 -0
  366. package/rules/common/comments.md +1 -0
  367. package/schemas/brand-voice.schema.json +24 -0
  368. package/schemas/capability-registry.schema.json +72 -0
  369. package/schemas/certification-run.schema.json +130 -0
  370. package/schemas/demo-recording.schema.json +46 -0
  371. package/schemas/eval-dataset.schema.json +79 -0
  372. package/schemas/execution-capability-profile.schema.json +46 -0
  373. package/schemas/execution-policy.schema.json +114 -0
  374. package/schemas/improvement-proposal.schema.json +65 -0
  375. package/schemas/mcp-tool-output.schema.json +61 -0
  376. package/schemas/platform-capabilities.schema.json +83 -0
  377. package/schemas/project-config.schema.json +215 -0
  378. package/schemas/project-demo.schema.json +60 -0
  379. package/schemas/provider-behavior-matrix.schema.json +91 -0
  380. package/schemas/scope.schema.json +197 -0
  381. package/schemas/specialist-trace.schema.json +107 -0
  382. package/schemas/team.schema.json +99 -0
  383. package/schemas/unified-registry.schema.json +546 -0
  384. package/scripts/sync-specialists.mjs +336 -106
  385. package/skills/docs/adr-workflow.md +1 -1
  386. package/skills/docs/codebase-research-workflow.md +3 -3
  387. package/skills/docs/prd-workflow.md +5 -6
  388. package/skills/docs/runbook-workflow.md +2 -2
  389. package/skills/docs/user-research-workflow.md +3 -3
  390. package/skills/operating/fleet-health-routing.md +77 -0
  391. package/skills/roles/ai-engineer.md +1 -1
  392. package/skills/roles/architect.md +0 -1
  393. package/skills/roles/business-strategist.md +1 -1
  394. package/skills/roles/data-analyst.telemetry.md +1 -1
  395. package/skills/roles/data-engineer.md +1 -1
  396. package/skills/roles/data-engineer.pipeline.md +1 -1
  397. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  398. package/skills/roles/data-engineer.warehouse.md +1 -1
  399. package/skills/roles/designer.accessibility.md +1 -1
  400. package/skills/roles/designer.md +0 -1
  401. package/skills/roles/devil-advocate.md +1 -1
  402. package/skills/roles/docs-keeper.md +1 -1
  403. package/skills/roles/evaluator.md +1 -1
  404. package/skills/roles/explorer.md +1 -1
  405. package/skills/roles/platform-engineer.md +1 -1
  406. package/skills/roles/qa.ai-eval.md +1 -2
  407. package/skills/roles/qa.api-contract.md +0 -1
  408. package/skills/roles/qa.data-pipeline.md +0 -1
  409. package/skills/roles/qa.md +0 -1
  410. package/skills/roles/qa.web-ui.md +0 -1
  411. package/skills/roles/release-manager.md +1 -1
  412. package/skills/roles/researcher.md +0 -2
  413. package/skills/roles/reviewer.md +0 -3
  414. package/skills/roles/security.ai.md +1 -1
  415. package/skills/roles/security.legal-compliance.md +1 -1
  416. package/skills/roles/security.md +0 -1
  417. package/skills/roles/security.privacy.md +0 -1
  418. package/skills/roles/security.supply-chain.md +1 -1
  419. package/skills/roles/sre.md +1 -1
  420. package/skills/roles/test-automation.md +1 -1
  421. package/skills/roles/trace-reviewer.md +1 -1
  422. package/skills/roles/ux-researcher.md +1 -1
  423. package/specialists/artifact-manifest.json +36 -36
  424. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  425. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  426. package/specialists/org/contracts/any-to-debugger.json +7 -1
  427. package/specialists/org/contracts/any-to-designer.json +7 -1
  428. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  429. package/specialists/org/contracts/any-to-explorer.json +13 -4
  430. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  431. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  432. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  433. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  434. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  435. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  436. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  437. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  438. package/specialists/org/contracts/architect-to-operations.json +17 -4
  439. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  440. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  441. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  442. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  443. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  444. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  445. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  446. package/specialists/org/contracts/operations-to-user.json +53 -0
  447. package/specialists/org/contracts/operations-triage-output.json +53 -0
  448. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  449. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  450. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  451. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  452. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  453. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  454. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  455. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  456. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  457. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  458. package/specialists/org/contracts/user-to-construct.json +11 -4
  459. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  460. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  461. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  462. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  463. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  464. package/specialists/org/groups/engineering-group.json +2 -6
  465. package/specialists/org/groups/governance-group.json +2 -3
  466. package/specialists/org/groups/operations-group.json +5 -8
  467. package/specialists/org/groups/product-group.json +4 -8
  468. package/specialists/org/groups/quality-group.json +3 -7
  469. package/specialists/org/groups/strategy-group.json +6 -9
  470. package/specialists/org/models.json.example +8 -0
  471. package/specialists/org/scopes/creative.json +6 -1
  472. package/specialists/org/scopes/operations.json +7 -1
  473. package/specialists/org/scopes/research.json +6 -1
  474. package/specialists/org/scopes/rnd.json +7 -1
  475. package/specialists/org/specialists/cx-architect.json +27 -8
  476. package/specialists/org/specialists/cx-designer.json +22 -8
  477. package/specialists/org/specialists/cx-engineer.json +71 -7
  478. package/specialists/org/specialists/cx-operations.json +89 -15
  479. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  480. package/specialists/org/specialists/cx-product-manager.json +28 -9
  481. package/specialists/org/specialists/cx-qa.json +16 -6
  482. package/specialists/org/specialists/cx-researcher.json +41 -7
  483. package/specialists/org/specialists/cx-reviewer.json +61 -11
  484. package/specialists/org/specialists/cx-security.json +30 -10
  485. package/specialists/org/teams/design-team.json +3 -4
  486. package/specialists/org/teams/engineering-team.json +3 -10
  487. package/specialists/org/teams/governance-team.json +3 -5
  488. package/specialists/org/teams/operations-team.json +6 -12
  489. package/specialists/org/teams/product-management-team.json +2 -3
  490. package/specialists/org/teams/quality-team.json +4 -12
  491. package/specialists/org/teams/research-team.json +3 -3
  492. package/specialists/org/teams/strategy-team.json +7 -14
  493. package/specialists/prompts/cx-architect.md +20 -1
  494. package/specialists/prompts/cx-data-analyst.md +1 -1
  495. package/specialists/prompts/cx-designer.md +12 -4
  496. package/specialists/prompts/cx-engineer.md +15 -1
  497. package/specialists/prompts/cx-operations.md +14 -2
  498. package/specialists/prompts/cx-orchestrator.md +9 -5
  499. package/specialists/prompts/cx-product-manager.md +5 -1
  500. package/specialists/prompts/cx-qa.md +6 -2
  501. package/specialists/prompts/cx-researcher.md +27 -31
  502. package/specialists/prompts/cx-reviewer.md +19 -1
  503. package/specialists/prompts/cx-security.md +5 -1
  504. package/templates/distribution/construct-brand.typ +123 -59
  505. package/templates/distribution/construct-decision.typ +6 -3
  506. package/templates/distribution/construct-pdf.typ +11 -4
  507. package/templates/distribution/construct-prd.typ +6 -3
  508. package/templates/distribution/construct-research.typ +7 -4
  509. package/vendor/pandoc-ext/README.md +3 -0
  510. package/vendor/pandoc-ext/diagram.lua +687 -0
  511. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  512. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  513. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  514. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  515. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  516. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  517. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  518. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  519. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  520. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  521. package/specialists/org/specialists/cx-accessibility.json +0 -69
  522. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  523. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  524. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  525. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  526. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  527. package/specialists/org/specialists/cx-evaluator.json +0 -69
  528. package/specialists/org/specialists/cx-explorer.json +0 -69
  529. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  530. package/specialists/org/specialists/cx-oracle.json +0 -46
  531. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  532. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  533. package/specialists/org/specialists/cx-release-manager.json +0 -77
  534. package/specialists/org/specialists/cx-sre.json +0 -94
  535. package/specialists/org/specialists/cx-test-automation.json +0 -69
  536. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  537. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  538. package/specialists/org/teams/accessibility-team.json +0 -39
  539. package/specialists/org/teams/ux-research-team.json +0 -39
  540. package/specialists/prompts/cx-accessibility.md +0 -55
  541. package/specialists/prompts/cx-ai-engineer.md +0 -124
  542. package/specialists/prompts/cx-business-strategist.md +0 -58
  543. package/specialists/prompts/cx-data-engineer.md +0 -55
  544. package/specialists/prompts/cx-devil-advocate.md +0 -59
  545. package/specialists/prompts/cx-docs-keeper.md +0 -164
  546. package/specialists/prompts/cx-evaluator.md +0 -49
  547. package/specialists/prompts/cx-explorer.md +0 -71
  548. package/specialists/prompts/cx-legal-compliance.md +0 -58
  549. package/specialists/prompts/cx-oracle.md +0 -98
  550. package/specialists/prompts/cx-platform-engineer.md +0 -97
  551. package/specialists/prompts/cx-rd-lead.md +0 -59
  552. package/specialists/prompts/cx-release-manager.md +0 -54
  553. package/specialists/prompts/cx-sre.md +0 -111
  554. package/specialists/prompts/cx-test-automation.md +0 -55
  555. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  556. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -0,0 +1,200 @@
1
+ /**
2
+ * lib/registry/custom-scaffold.mjs — CLI-facing scaffolding for user-authored
3
+ * custom specialists and teams (construct-rf26.13).
4
+ *
5
+ * Writes into one of two extension tiers, both read back by
6
+ * lib/registry/assemble.mjs at the same precedence ADR-0052 establishes for
7
+ * provider manifests (builtin -> user -> project):
8
+ * - "user" ~/.construct/org/** — usable across every project on the machine
9
+ * - "project" <rootDir>/.cx/org/** — git-tracked, highest precedence, scoped
10
+ * to one project (the existing overlay
11
+ * introduced by ADR-0046)
12
+ *
13
+ * Refuses to overwrite an existing file unless { force: true }, mirroring
14
+ * lib/specialists/scaffold.mjs's createSpecialistDraft convention. Validates
15
+ * with lib/registry/custom-schema.mjs before writing anything, so a scaffolded
16
+ * record is guaranteed registry-loadable the moment it lands on disk.
17
+ */
18
+
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { homeDir } from '../paths.mjs';
22
+ import { assembleRegistry } from './assemble.mjs';
23
+ import { validateCustomSpecialist, validateCustomTeam } from './custom-schema.mjs';
24
+
25
+ const ID_RE = /^[a-z][a-z0-9-]*$/;
26
+
27
+ export function customOrgDir(scope, { rootDir = process.cwd() } = {}) {
28
+ if (scope === 'user') return path.join(homeDir(), '.construct', 'org');
29
+ return path.join(rootDir, '.cx', 'org');
30
+ }
31
+
32
+ function existingTeamIds(rootDir) {
33
+ try {
34
+ return Object.keys(assembleRegistry(rootDir).teams);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function renderPromptStub({ id, role, description }) {
41
+ return [
42
+ `# cx-${id}`,
43
+ '',
44
+ `Role: ${role}`,
45
+ '',
46
+ description || 'TODO: describe this specialist\'s perspective and output format.',
47
+ '',
48
+ '## Anti-fabrication contract',
49
+ '',
50
+ 'Every load-bearing claim cites a source the reader can re-verify. When a fact is not in the source, write `unknown` or `[unverified]`. See `rules/common/no-fabrication.md`.',
51
+ '',
52
+ ].join('\n');
53
+ }
54
+
55
+ /**
56
+ * Scaffold a custom specialist JSON record (+ a minimal prompt stub) into the
57
+ * requested tier. Throws with actionable, field-named errors on any failure.
58
+ * @returns {{ path: string, relPath: string, promptPath: string, record: object, scope: string }}
59
+ */
60
+ export function createCustomSpecialist({
61
+ rootDir = process.cwd(),
62
+ scope = 'project',
63
+ id,
64
+ role,
65
+ description,
66
+ modelTier = 'standard',
67
+ reasoningEffort,
68
+ skills = [],
69
+ fence,
70
+ team,
71
+ teamId,
72
+ handoffCandidates = [],
73
+ claudeTools = 'Read,Grep,Glob,LS',
74
+ displayName,
75
+ force = false,
76
+ } = {}) {
77
+ if (!id || !ID_RE.test(id)) {
78
+ throw new Error(`invalid specialist id "${id}" — use lowercase kebab-case (e.g. "widget-specialist")`);
79
+ }
80
+ if (scope !== 'user' && scope !== 'project') {
81
+ throw new Error(`invalid scope "${scope}" — use "user" or "project"`);
82
+ }
83
+
84
+ const orgDir = customOrgDir(scope, { rootDir });
85
+ const specialistsDir = path.join(orgDir, 'specialists');
86
+ const filePath = path.join(specialistsDir, `cx-${id}.json`);
87
+ if (fs.existsSync(filePath) && !force) {
88
+ throw new Error(`${filePath} already exists — pass force to overwrite`);
89
+ }
90
+
91
+ const resolvedTeam = team || teamId;
92
+ const knownTeams = existingTeamIds(rootDir);
93
+ if (resolvedTeam && knownTeams && !knownTeams.includes(resolvedTeam)) {
94
+ throw new Error(
95
+ `team "${resolvedTeam}" does not exist — create it first with \`construct team create ${resolvedTeam} --owner=${role || id}\`, ` +
96
+ `or reference one of: ${knownTeams.slice(0, 10).join(', ')}${knownTeams.length > 10 ? ', …' : ''}`,
97
+ );
98
+ }
99
+
100
+ const promptRelPath = path.posix.join(
101
+ path.relative(rootDir, orgDir).split(path.sep).join('/'),
102
+ 'prompts',
103
+ `cx-${id}.md`,
104
+ );
105
+
106
+ const record = {
107
+ name: id,
108
+ displayName: displayName || description,
109
+ description,
110
+ role: role || id,
111
+ modelTier,
112
+ ...(reasoningEffort ? { reasoningEffort } : {}),
113
+ skills,
114
+ fence: fence || { allowedPaths: [] },
115
+ claudeTools,
116
+ promptFile: promptRelPath,
117
+ handoffCandidates,
118
+ team: resolvedTeam,
119
+ teamId: resolvedTeam,
120
+ internal: true,
121
+ custom: true,
122
+ };
123
+
124
+ const errors = validateCustomSpecialist(record, { rootDir, checkPromptFileExists: false });
125
+ if (errors.length) {
126
+ throw new Error(`custom specialist "${id}" failed validation:\n ${errors.join('\n ')}`);
127
+ }
128
+
129
+ const promptDir = path.join(orgDir, 'prompts');
130
+ const promptPath = path.join(rootDir, promptRelPath);
131
+ fs.mkdirSync(promptDir, { recursive: true });
132
+ if (!fs.existsSync(promptPath) || force) {
133
+ fs.writeFileSync(promptPath, renderPromptStub({ id, role: record.role, description }));
134
+ }
135
+
136
+ fs.mkdirSync(specialistsDir, { recursive: true });
137
+ fs.writeFileSync(filePath, JSON.stringify(record, null, 2) + '\n');
138
+
139
+ return { path: filePath, relPath: path.relative(rootDir, filePath), promptPath, record, scope };
140
+ }
141
+
142
+ /**
143
+ * Scaffold a custom team JSON record into the requested tier. Throws with
144
+ * actionable, field-named errors on any failure.
145
+ * @returns {{ path: string, relPath: string, record: object, scope: string }}
146
+ */
147
+ export function createCustomTeam({
148
+ rootDir = process.cwd(),
149
+ scope = 'project',
150
+ id,
151
+ name,
152
+ owner,
153
+ roles,
154
+ specialists = [],
155
+ charter,
156
+ decisionRights = [],
157
+ forbiddenDecisions = [],
158
+ escalationPath,
159
+ contact = {},
160
+ groupId,
161
+ force = false,
162
+ } = {}) {
163
+ if (!id || !ID_RE.test(id)) {
164
+ throw new Error(`invalid team id "${id}" — use lowercase kebab-case (e.g. "widget-team")`);
165
+ }
166
+ if (scope !== 'user' && scope !== 'project') {
167
+ throw new Error(`invalid scope "${scope}" — use "user" or "project"`);
168
+ }
169
+
170
+ const orgDir = customOrgDir(scope, { rootDir });
171
+ const teamsDir = path.join(orgDir, 'teams');
172
+ const filePath = path.join(teamsDir, `${id}.json`);
173
+ if (fs.existsSync(filePath) && !force) {
174
+ throw new Error(`${filePath} already exists — pass force to overwrite`);
175
+ }
176
+
177
+ const resolvedRoles = roles && roles.length ? roles : (owner ? [owner] : []);
178
+ const record = {
179
+ name: name || id,
180
+ owner,
181
+ roles: resolvedRoles,
182
+ specialists,
183
+ decisionRights,
184
+ forbiddenDecisions,
185
+ escalationPath: escalationPath && escalationPath.length ? escalationPath : [owner, 'orchestrator'].filter(Boolean),
186
+ charter,
187
+ contact,
188
+ ...(groupId ? { groupId } : {}),
189
+ };
190
+
191
+ const errors = validateCustomTeam({ id, ...record });
192
+ if (errors.length) {
193
+ throw new Error(`custom team "${id}" failed validation:\n ${errors.join('\n ')}`);
194
+ }
195
+
196
+ fs.mkdirSync(teamsDir, { recursive: true });
197
+ fs.writeFileSync(filePath, JSON.stringify(record, null, 2) + '\n');
198
+
199
+ return { path: filePath, relPath: path.relative(rootDir, filePath), record, scope };
200
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * lib/registry/custom-schema.mjs — validation contract for user-authored custom
3
+ * specialists and teams (construct-rf26.13).
4
+ *
5
+ * Hand-rolled validator, matching the style already established by
6
+ * lib/specialists/schema.mjs and lib/config/schema.mjs, rather than pulling in
7
+ * a schema-validation dependency: every error names the offending field and
8
+ * says what to do about it, so a scaffold failure is actionable without
9
+ * cross-referencing a generic validator dump. Reuses lib/specialists/schema.mjs's
10
+ * ALLOWED_TOOLS allowlist so a custom specialist is held to the same
11
+ * claudeTools contract as a built-in one.
12
+ */
13
+
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { ALLOWED_TOOLS } from '../specialists/schema.mjs';
17
+
18
+ export const MODEL_TIERS = ['fast', 'standard', 'reasoning'];
19
+ const SKILL_REF_RE = /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/;
20
+ const ID_RE = /^[a-z][a-z0-9-]*$/;
21
+
22
+ /**
23
+ * Validate one custom specialist record. Returns an array of actionable error
24
+ * strings; empty means the record passes.
25
+ */
26
+ export function validateCustomSpecialist(record, { rootDir, checkPromptFileExists = true } = {}) {
27
+ const errors = [];
28
+ const id = record?.name || '(unnamed)';
29
+
30
+ if (!record || typeof record !== 'object') {
31
+ return [`custom specialist: record must be an object`];
32
+ }
33
+
34
+ if (!record.name || !ID_RE.test(record.name)) {
35
+ errors.push(`${id}: "name" must be lowercase kebab-case (e.g. "widget-specialist"), got ${JSON.stringify(record.name)}`);
36
+ }
37
+
38
+ if (!record.description || String(record.description).trim().length < 20) {
39
+ errors.push(`${id}: "description" is required and must be at least 20 characters — it is load-bearing for orchestration routing`);
40
+ } else if (String(record.description).length > 500) {
41
+ errors.push(`${id}: "description" is too long (>500 chars) — keep it scannable`);
42
+ }
43
+
44
+ if (!record.role || typeof record.role !== 'string') {
45
+ errors.push(`${id}: "role" is required (e.g. "widget-specialist") — a team's "owner" field must match a specialist's "role" for the team to validate`);
46
+ }
47
+
48
+ if (!record.modelTier || !MODEL_TIERS.includes(record.modelTier)) {
49
+ errors.push(`${id}: "modelTier" must be one of ${MODEL_TIERS.join(' | ')}, got ${JSON.stringify(record.modelTier)}`);
50
+ }
51
+
52
+ const team = record.team || record.teamId;
53
+ if (!team || typeof team !== 'string') {
54
+ errors.push(`${id}: "team" (or "teamId") is required — reference an existing team id, or create one first with \`construct team create <id> --owner=${record.role || '<role>'}\``);
55
+ }
56
+
57
+ if (!Array.isArray(record.skills) || record.skills.length === 0) {
58
+ errors.push(`${id}: "skills" must be a non-empty array of skill bundle references (e.g. ["frontend-design/accessibility"])`);
59
+ } else {
60
+ record.skills.forEach((skill, i) => {
61
+ if (typeof skill !== 'string' || !SKILL_REF_RE.test(skill)) {
62
+ errors.push(`${id}: skills[${i}] "${skill}" is not a valid skill bundle reference — expected "<bundle>/<skill>" (e.g. "frontend-design/accessibility")`);
63
+ }
64
+ });
65
+ }
66
+
67
+ if (!record.fence || typeof record.fence !== 'object' || Array.isArray(record.fence)) {
68
+ errors.push(`${id}: "fence" is required — at minimum { "allowedPaths": [...] } to declare the permission boundary`);
69
+ } else if (!Array.isArray(record.fence.allowedPaths) || record.fence.allowedPaths.length === 0) {
70
+ errors.push(`${id}: "fence.allowedPaths" must be a non-empty array of path globs this specialist may touch`);
71
+ }
72
+
73
+ if (record.handoffCandidates !== undefined && !Array.isArray(record.handoffCandidates)) {
74
+ errors.push(`${id}: "handoffCandidates" must be an array of role ids (the delegation spec — who this specialist hands off to)`);
75
+ }
76
+
77
+ if (record.claudeTools) {
78
+ const tools = String(record.claudeTools).split(',').map((t) => t.trim()).filter(Boolean);
79
+ for (const tool of tools) {
80
+ if (!ALLOWED_TOOLS.has(tool)) {
81
+ errors.push(`${id}: unknown tool "${tool}" in claudeTools — host platforms silently drop unknown tool names, causing this specialist to appear to work without it`);
82
+ }
83
+ }
84
+ }
85
+
86
+ if (checkPromptFileExists && record.promptFile && rootDir) {
87
+ const promptPath = path.join(rootDir, record.promptFile);
88
+ if (!fs.existsSync(promptPath)) {
89
+ errors.push(`${id}: promptFile "${record.promptFile}" does not exist under ${rootDir}`);
90
+ }
91
+ }
92
+
93
+ return errors;
94
+ }
95
+
96
+ /**
97
+ * Validate one custom team record. Returns an array of actionable error
98
+ * strings; empty means the record passes.
99
+ */
100
+ export function validateCustomTeam(record) {
101
+ const errors = [];
102
+ const id = record?.id || record?.name || '(unnamed)';
103
+
104
+ if (!record || typeof record !== 'object') {
105
+ return [`custom team: record must be an object`];
106
+ }
107
+
108
+ if (!record.name || typeof record.name !== 'string') {
109
+ errors.push(`${id}: "name" is required (human-readable team name, e.g. "Widget Team")`);
110
+ }
111
+
112
+ if (!record.owner || typeof record.owner !== 'string') {
113
+ errors.push(`${id}: "owner" is required — the role id that holds primary accountability (must match a specialist's "role" field)`);
114
+ }
115
+
116
+ if (!Array.isArray(record.roles) || record.roles.length === 0) {
117
+ errors.push(`${id}: "roles" must be a non-empty array of role ids that constitute this team`);
118
+ } else if (record.owner && !record.roles.includes(record.owner)) {
119
+ errors.push(`${id}: "roles" must include the owner role "${record.owner}"`);
120
+ }
121
+
122
+ if (!record.charter || String(record.charter).trim().length < 20) {
123
+ errors.push(`${id}: "charter" is required and must be at least 20 characters — a one-paragraph mission statement for what this team owns`);
124
+ }
125
+
126
+ for (const field of ['decisionRights', 'forbiddenDecisions', 'escalationPath', 'specialists']) {
127
+ if (record[field] !== undefined && !Array.isArray(record[field])) {
128
+ errors.push(`${id}: "${field}" must be an array of strings when present`);
129
+ }
130
+ }
131
+
132
+ if (record.contact !== undefined && (typeof record.contact !== 'object' || Array.isArray(record.contact))) {
133
+ errors.push(`${id}: "contact" must be an object when present (e.g. { "slack": "#team", "owner": "role-id" })`);
134
+ }
135
+
136
+ return errors;
137
+ }
@@ -10,14 +10,20 @@
10
10
 
11
11
  import fs from 'fs';
12
12
  import path from 'path';
13
- import { fileURLToPath } from 'url';
14
13
  import { validate } from './validator.mjs';
15
14
  import { assembleRegistry, orgDirMtime, listTeamsFromRegistry, listGroupsHierarchical } from './assemble.mjs';
15
+ import { packageRoot } from '../roots.mjs';
16
16
 
17
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
- const ROOT_DIR = path.resolve(__dirname, '..', '..');
17
+ // packageRoot (not a local __dirname-derived constant) because under a
18
+ // Bun-compiled binary every bundled module's import.meta.url collapses to
19
+ // the same virtual /$bunfs/root path, losing this file's real nesting
20
+ // under lib/registry/; packageRoot already falls back to the compiled
21
+ // binary's real on-disk directory (see lib/roots.mjs).
22
+
23
+ const ROOT_DIR = packageRoot;
19
24
 
20
25
  let _registry = null;
26
+ let _rootDir = null;
21
27
  let _orgMtime = null;
22
28
  let _legacyOverlayMtime = null;
23
29
 
@@ -62,7 +68,10 @@ export function loadRegistry(opts = {}) {
62
68
  legacyOverlay = null;
63
69
  }
64
70
 
65
- if (_registry && _orgMtime === orgMtime && _legacyOverlayMtime === legacyOverlayMtime) {
71
+ // The cache key must include rootDir: two roots can share an orgMtime/overlay mtime
72
+ // (tmpdir tests, coincident timestamps), and a cache keyed only on mtimes bleeds one
73
+ // root's registry into another — a specialist can then surface without its promptFile.
74
+ if (_registry && _rootDir === rootDir && _orgMtime === orgMtime && _legacyOverlayMtime === legacyOverlayMtime) {
66
75
  return _registry;
67
76
  }
68
77
 
@@ -79,6 +88,7 @@ export function loadRegistry(opts = {}) {
79
88
  }
80
89
 
81
90
  _registry = registry;
91
+ _rootDir = rootDir;
82
92
  _orgMtime = orgMtime;
83
93
  _legacyOverlayMtime = legacyOverlayMtime;
84
94
 
@@ -127,6 +137,7 @@ export function listSpecialists(teamId, opts = {}) {
127
137
 
128
138
  export function clearCache() {
129
139
  _registry = null;
140
+ _rootDir = null;
130
141
  _orgMtime = null;
131
142
  _legacyOverlayMtime = null;
132
143
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "pdf": { "engine": "pandoc", "writer": null, "pdfEngine": "typst", "extraBinaries": ["typst"] },
3
+ "docx": { "engine": "pandoc", "writer": null, "pdfEngine": null, "extraBinaries": [] },
4
+ "doc": { "engine": "pandoc", "writer": null, "pdfEngine": null, "extraBinaries": [], "via": "libreoffice", "intermediate": "docx" },
5
+ "deck": { "engine": "pandoc", "writer": null, "pdfEngine": null, "extraBinaries": [] },
6
+ "pptx": { "engine": "pptxgenjs", "writer": null, "pdfEngine": null, "extraBinaries": [] },
7
+ "html": { "engine": "pandoc", "writer": null, "pdfEngine": null, "extraBinaries": [] },
8
+ "rtf": { "engine": "pandoc", "writer": "rtf", "pdfEngine": null, "extraBinaries": [] },
9
+ "odt": { "engine": "pandoc", "writer": "odt", "pdfEngine": null, "extraBinaries": [] },
10
+ "epub": { "engine": "pandoc", "writer": "epub", "pdfEngine": null, "extraBinaries": [] },
11
+ "tex": { "engine": "pandoc", "writer": "latex", "pdfEngine": null, "extraBinaries": [] },
12
+ "txt": { "engine": "pandoc", "writer": "plain", "pdfEngine": null, "extraBinaries": [] },
13
+ "md": { "engine": "copy", "writer": null, "pdfEngine": null, "extraBinaries": [] },
14
+ "mdx": { "engine": "copy", "writer": null, "pdfEngine": null, "extraBinaries": [] }
15
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "tiers": ["agent-mcp", "thin-cli", "tui", "internal"],
3
+ "commands": {
4
+ "install": "thin-cli",
5
+ "init": "thin-cli",
6
+ "dev": "thin-cli",
7
+ "stop": "thin-cli",
8
+ "status": "thin-cli",
9
+ "doctor": "thin-cli",
10
+ "sync": "thin-cli",
11
+ "ingest": "agent-mcp",
12
+ "drop": "agent-mcp",
13
+ "distill": "agent-mcp",
14
+ "ask": "agent-mcp",
15
+ "search": "agent-mcp",
16
+ "knowledge": "agent-mcp",
17
+ "memory": "agent-mcp",
18
+ "reflect": "agent-mcp",
19
+ "intake": "tui",
20
+ "workflow": "agent-mcp",
21
+ "graph": "agent-mcp",
22
+ "capability": "agent-mcp",
23
+ "execution": "agent-mcp",
24
+ "orchestrate": "agent-mcp",
25
+ "models": "agent-mcp",
26
+ "profile": "tui",
27
+ "sandbox": "tui",
28
+ "review": "thin-cli",
29
+ "telemetry": "thin-cli",
30
+ "evals": "thin-cli",
31
+ "improvement": "thin-cli",
32
+ "ci": "thin-cli",
33
+ "docs": "thin-cli",
34
+ "export": "agent-mcp",
35
+ "diagram": "agent-mcp",
36
+ "demo": "agent-mcp",
37
+ "beads": "thin-cli",
38
+ "hook": "internal",
39
+ "lint:comments": "internal",
40
+ "lint:agents": "internal",
41
+ "registry:status": "internal",
42
+ "registry:validate": "internal",
43
+ "registry:generate-docs": "internal",
44
+ "rules": "thin-cli"
45
+ }
46
+ }
@@ -29,6 +29,7 @@ export const RETIRED_REGISTRY_PATH_ALLOWLIST = Object.freeze([
29
29
  'scripts/patch-registry-readers-v2.mjs',
30
30
  'tests/no-monolithic-registry.test.mjs',
31
31
  'tests/migration-round-trip.test.mjs',
32
+ 'tests/fixtures/mcp-tool-schemas/results/project_context.result.json',
32
33
  ]);
33
34
 
34
35
  export function isRetiredRegistryPathAllowed(relPath) {
@@ -3,6 +3,12 @@
3
3
  *
4
4
  * Declarative map from CLI command name to primary surface tier. The CLI registry
5
5
  * remains the substrate; this file governs discovery posture without removing verbs.
6
+ * LMCP-B7: the tiers/command map ship as lib/registry/manifests/surface-map.default.json
7
+ * (loaded once at module init — this file's exports stay byte-identical to the prior
8
+ * hardcoded dict) with an optional `.cx/registry/surface-map.json` project override
9
+ * merged in per-call by resolveSurfaceMap()/surfaceForCommand()/validateSurfaceMap().
10
+ * An override may add or reassign command entries and replace the tier list; it can
11
+ * never remove a default entry outright (unset keys keep resolving to the default).
6
12
  *
7
13
  * Surfaces:
8
14
  * agent-mcp — MCP tool canonical; CLI --json twin
@@ -11,64 +17,108 @@
11
17
  * internal — CI/harness only
12
18
  */
13
19
 
14
- export const SURFACE_TIERS = ['agent-mcp', 'thin-cli', 'tui', 'internal'];
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+
23
+ import { packageRoot } from '../roots.mjs';
24
+
25
+ // packageRoot (not a local import.meta.url-derived HERE) because under a
26
+ // Bun-compiled binary every bundled module's import.meta.url collapses to
27
+ // the same virtual /$bunfs/root path, losing this file's real nesting
28
+ // under lib/registry/ — packageRoot already falls back to the compiled
29
+ // binary's real on-disk directory (see lib/roots.mjs), so rebuilding the
30
+ // known relative path from it resolves correctly in every run mode.
31
+
32
+ const DEFAULT_MANIFEST_PATH = path.join(packageRoot, 'lib', 'registry', 'manifests', 'surface-map.default.json');
33
+
34
+ function readJsonIfExists(filePath) {
35
+ try {
36
+ if (!fs.existsSync(filePath)) return null;
37
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function loadDefaultManifest() {
44
+ const manifest = readJsonIfExists(DEFAULT_MANIFEST_PATH);
45
+ if (!manifest || !Array.isArray(manifest.tiers) || typeof manifest.commands !== 'object') {
46
+ throw new Error(`lib/registry/surface-map.mjs: missing or malformed default manifest at ${DEFAULT_MANIFEST_PATH}`);
47
+ }
48
+ return manifest;
49
+ }
50
+
51
+ const DEFAULT_MANIFEST = loadDefaultManifest();
52
+
53
+ // Byte-identical to the pre-manifest hardcoded exports: both are read once at
54
+ // module init from the default manifest, with no project override applied —
55
+ // a direct import of these two names sees exactly the shipped defaults.
56
+
57
+ export const SURFACE_TIERS = DEFAULT_MANIFEST.tiers;
15
58
 
16
59
  /** @type {Record<string, string>} */
17
- export const COMMAND_SURFACE = {
18
- install: 'thin-cli',
19
- init: 'thin-cli',
20
- dev: 'thin-cli',
21
- stop: 'thin-cli',
22
- status: 'thin-cli',
23
- doctor: 'thin-cli',
24
- sync: 'thin-cli',
25
- ingest: 'agent-mcp',
26
- drop: 'agent-mcp',
27
- distill: 'agent-mcp',
28
- ask: 'agent-mcp',
29
- search: 'agent-mcp',
30
- knowledge: 'agent-mcp',
31
- memory: 'agent-mcp',
32
- reflect: 'agent-mcp',
33
- intake: 'tui',
34
- workflow: 'agent-mcp',
35
- graph: 'agent-mcp',
36
- capability: 'agent-mcp',
37
- execution: 'agent-mcp',
38
- orchestrate: 'agent-mcp',
39
- models: 'agent-mcp',
40
- profile: 'tui',
41
- sandbox: 'tui',
42
- review: 'thin-cli',
43
- telemetry: 'thin-cli',
44
- evals: 'thin-cli',
45
- improvement: 'thin-cli',
46
- ci: 'thin-cli',
47
- docs: 'thin-cli',
48
- export: 'agent-mcp',
49
- diagram: 'agent-mcp',
50
- demo: 'agent-mcp',
51
- beads: 'thin-cli',
52
- hook: 'internal',
53
- 'lint:comments': 'internal',
54
- 'lint:agents': 'internal',
55
- 'registry:status': 'internal',
56
- 'registry:validate': 'internal',
57
- 'registry:generate-docs': 'internal',
58
- rules: 'thin-cli',
59
- };
60
-
61
- export function surfaceForCommand(name) {
62
- if (COMMAND_SURFACE[name]) return COMMAND_SURFACE[name];
60
+ export const COMMAND_SURFACE = DEFAULT_MANIFEST.commands;
61
+
62
+ const overrideCache = new Map();
63
+
64
+ function loadOverride(cwd) {
65
+ if (overrideCache.has(cwd)) return overrideCache.get(cwd);
66
+ const override = readJsonIfExists(path.join(cwd, '.cx', 'registry', 'surface-map.json'));
67
+ overrideCache.set(cwd, override);
68
+ return override;
69
+ }
70
+
71
+ /**
72
+ * resolveSurfaceMap({ cwd } = {})
73
+ *
74
+ * Merges the default manifest with a project's `.cx/registry/surface-map.json`
75
+ * override, when present. `commands` in the override are shallow-merged over
76
+ * the default (an override entry replaces the matching default; every other
77
+ * default entry survives untouched). `tiers` in the override replaces the
78
+ * default tier list wholesale when present.
79
+ */
80
+ export function resolveSurfaceMap({ cwd = process.cwd() } = {}) {
81
+ const override = loadOverride(cwd);
82
+ if (!override) return { tiers: SURFACE_TIERS, commands: COMMAND_SURFACE };
83
+ const tiers = Array.isArray(override.tiers) ? override.tiers : SURFACE_TIERS;
84
+ const commands = { ...COMMAND_SURFACE, ...(override.commands && typeof override.commands === 'object' ? override.commands : {}) };
85
+ return { tiers, commands };
86
+ }
87
+
88
+ export function surfaceForCommand(name, { cwd = process.cwd() } = {}) {
89
+ const { commands } = resolveSurfaceMap({ cwd });
90
+ if (commands[name]) return commands[name];
63
91
  if (name.includes(':')) return 'internal';
64
92
  return 'thin-cli';
65
93
  }
66
94
 
67
- export function commandsBySurface(commands) {
68
- const grouped = Object.fromEntries(SURFACE_TIERS.map((t) => [t, []]));
95
+ export function commandsBySurface(commands, { cwd = process.cwd() } = {}) {
96
+ const { tiers } = resolveSurfaceMap({ cwd });
97
+ const grouped = Object.fromEntries(tiers.map((t) => [t, []]));
69
98
  for (const cmd of commands) {
70
- const surface = cmd.surface ?? surfaceForCommand(cmd.name);
99
+ const surface = cmd.surface ?? surfaceForCommand(cmd.name, { cwd });
71
100
  grouped[surface]?.push(cmd.name);
72
101
  }
73
102
  return grouped;
74
103
  }
104
+
105
+ /**
106
+ * validateSurfaceMap(commandNames, { cwd } = {})
107
+ *
108
+ * Stricter than surfaceForCommand's runtime fallback: every name must have an
109
+ * explicit entry in the resolved (default + override) command map. This is an
110
+ * opt-in registry/doctor-style check, separate from the CLI's own dispatch
111
+ * path, so the default fallback behavior for unregistered commands stays
112
+ * unaffected.
113
+ */
114
+ export function validateSurfaceMap(commandNames, { cwd = process.cwd() } = {}) {
115
+ const { commands } = resolveSurfaceMap({ cwd });
116
+ const errors = commandNames
117
+ .filter((name) => !commands[name])
118
+ .map((name) => `command '${name}' has no surface-map entry (add one to .cx/registry/surface-map.json or lib/registry/manifests/surface-map.default.json)`);
119
+ return { valid: errors.length === 0, errors };
120
+ }
121
+
122
+ export function __clearSurfaceMapOverrideCache() {
123
+ overrideCache.clear();
124
+ }
@@ -86,7 +86,7 @@ export function validateCapabilityRegistry({ rootDir = REPO_ROOT, now = Date.now
86
86
 
87
87
  const hasTest = ver.functional || ver.hostEmulation || ver.untestableRationale;
88
88
  if (!hasTest && (cap.criticality === 'P0' || cap.criticality === 'P1')) {
89
- warnings.push(`${cap.id}: P0/P1 capability has no functional test or untestableRationale`);
89
+ errors.push(`${cap.id}: P0/P1 capability has no functional test or untestableRationale`);
90
90
  }
91
91
 
92
92
  const surfaces = cap.surfaces ?? {};
@@ -105,10 +105,10 @@ export function validateCapabilityRegistry({ rootDir = REPO_ROOT, now = Date.now
105
105
  if (cap.lastValidated) {
106
106
  const age = now - Date.parse(cap.lastValidated);
107
107
  if (Number.isFinite(age) && age > STALE_MS) {
108
- warnings.push(`${cap.id}: lastValidated older than 90 days (${cap.lastValidated})`);
108
+ errors.push(`${cap.id}: lastValidated older than 90 days (${cap.lastValidated})`);
109
109
  }
110
110
  } else if (cap.criticality === 'P0') {
111
- warnings.push(`${cap.id}: P0 capability never validated (lastValidated null)`);
111
+ errors.push(`${cap.id}: P0 capability never validated (lastValidated null)`);
112
112
  }
113
113
 
114
114
  for (const dep of cap.dependencies?.skills ?? []) {