@geraldmaron/construct 1.2.3 → 1.3.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 (505) hide show
  1. package/README.md +7 -10
  2. package/bin/construct +235 -209
  3. package/bin/construct-postinstall.mjs +0 -21
  4. package/commands/work/optimize-prompts.md +1 -1
  5. package/examples/distribution/sources/adr.md +2 -2
  6. package/examples/seed-observations/decisions.md +1 -1
  7. package/lib/artifact-loop-core.mjs +412 -0
  8. package/lib/artifact-manifest.mjs +6 -0
  9. package/lib/artifact-release-gate.mjs +115 -49
  10. package/lib/audit-rules.mjs +2 -4
  11. package/lib/audit-skills.mjs +32 -20
  12. package/lib/audit-specialists.mjs +44 -26
  13. package/lib/auto-docs.mjs +6 -5
  14. package/lib/brand-prose.mjs +3 -3
  15. package/lib/brand-tokens.mjs +2 -2
  16. package/lib/certification/artifact-fixtures.mjs +4 -1
  17. package/lib/certification/demo-parity.mjs +6 -32
  18. package/lib/certification/real-llm-scenarios.mjs +58 -13
  19. package/lib/certification/role-cards.mjs +7 -6
  20. package/lib/certification/role-overlays.mjs +4 -4
  21. package/lib/certification/runner.mjs +9 -2
  22. package/lib/certification/skill-inventory.mjs +34 -22
  23. package/lib/certification/skill-scenarios.mjs +21 -8
  24. package/lib/certification/specialist-contracts.mjs +4 -3
  25. package/lib/certification/specialist-scenarios.mjs +7 -6
  26. package/lib/certification/status.mjs +5 -4
  27. package/lib/cli-commands.mjs +55 -54
  28. package/lib/comment-lint.mjs +1 -5
  29. package/lib/completions.mjs +20 -2
  30. package/lib/config/legacy-config-migration.mjs +90 -24
  31. package/lib/config/schema.mjs +5 -5
  32. package/lib/config/source-targets.mjs +80 -0
  33. package/lib/contracts/validate.mjs +119 -51
  34. package/lib/decisions/golden.mjs +3 -2
  35. package/lib/decisions/registry.mjs +11 -6
  36. package/lib/demo-project.mjs +188 -0
  37. package/lib/demo-recording.mjs +9 -34
  38. package/lib/demo-script.mjs +2 -2
  39. package/lib/demo-surface.mjs +13 -131
  40. package/lib/demo-tour-renderer.mjs +92 -0
  41. package/lib/demo.mjs +69 -54
  42. package/lib/diagram-export.mjs +63 -14
  43. package/lib/doctor/cli.mjs +12 -2
  44. package/lib/doctor/source-checkout.mjs +3 -4
  45. package/lib/doctor/watchers/consistency.mjs +148 -43
  46. package/lib/doctor/watchers/credential-parity.mjs +97 -0
  47. package/lib/doctor/watchers/mcp-protocol.mjs +1 -1
  48. package/lib/doctor/watchers/service-health.mjs +4 -32
  49. package/lib/document-export.mjs +73 -9
  50. package/lib/document-extract.mjs +10 -198
  51. package/lib/embed/artifact.mjs +2 -3
  52. package/lib/embed/cli.mjs +1 -1
  53. package/lib/embed/config.mjs +1 -2
  54. package/lib/embed/customer-profiles.mjs +1 -1
  55. package/lib/embed/daemon.mjs +5 -6
  56. package/lib/embed/demand-fetch.mjs +114 -2
  57. package/lib/embed/docs-lifecycle.mjs +9 -5
  58. package/lib/embed/intake-metrics.mjs +2 -2
  59. package/lib/embed/notifications.mjs +2 -3
  60. package/lib/embed/recommendation-store.mjs +25 -22
  61. package/lib/embed/role-framing.mjs +4 -9
  62. package/lib/embedded-contract/capability.mjs +4 -2
  63. package/lib/embedded-contract/contract-version.mjs +1 -1
  64. package/lib/env-config.mjs +2 -2
  65. package/lib/flavors/loader.mjs +21 -19
  66. package/lib/graph/build-from-registry.mjs +31 -7
  67. package/lib/graph/staleness.mjs +1 -1
  68. package/lib/headhunt.mjs +82 -35
  69. package/lib/hooks/agent-tracker.mjs +2 -2
  70. package/lib/hooks/config-protection.mjs +1 -1
  71. package/lib/hooks/registry-sync.mjs +3 -3
  72. package/lib/host-capabilities.mjs +5 -5
  73. package/lib/improvement/controller.mjs +2 -2
  74. package/lib/init-unified.mjs +19 -21
  75. package/lib/intake/classify.mjs +26 -0
  76. package/lib/intake/daemon.mjs +2 -3
  77. package/lib/intake/prepare.mjs +10 -3
  78. package/lib/intake/session-prelude.mjs +1 -1
  79. package/lib/intake/tables/creative.mjs +9 -9
  80. package/lib/intake/tables/operations.mjs +4 -4
  81. package/lib/intake/tables/research.mjs +2 -2
  82. package/lib/integrations/intake-integrations.mjs +9 -10
  83. package/lib/knowledge/research-store.mjs +2 -2
  84. package/lib/mcp/server.mjs +104 -61
  85. package/lib/mcp/tool-budget.mjs +1 -46
  86. package/lib/mcp/tool-recovery.mjs +44 -0
  87. package/lib/mcp/tools/artifact-author.mjs +36 -0
  88. package/lib/mcp/tools/find-tool.mjs +86 -0
  89. package/lib/mcp/tools/orchestration-run.mjs +107 -69
  90. package/lib/mcp/tools/project.mjs +4 -3
  91. package/lib/mcp/tools/{profile.mjs → scope.mjs} +54 -52
  92. package/lib/mcp/tools/skills.mjs +39 -22
  93. package/lib/mcp/tools/telemetry.mjs +2 -1
  94. package/lib/mcp/tools/workflow.mjs +1 -1
  95. package/lib/mcp-platform-config.mjs +7 -5
  96. package/lib/migrations/index.mjs +4 -2
  97. package/lib/migrations/v2-unified-registry.mjs +35 -0
  98. package/lib/model-router.mjs +203 -36
  99. package/lib/models/catalog.mjs +25 -9
  100. package/lib/models/execution-capability-profile.mjs +3 -3
  101. package/lib/models/execution-policy.mjs +7 -6
  102. package/lib/models/provider-poll.mjs +18 -4
  103. package/lib/opencode-config.mjs +56 -1
  104. package/lib/opencode-runtime-plugin.mjs +13 -10
  105. package/lib/oracle/artifact-gate.mjs +14 -4
  106. package/lib/oracle/cli.mjs +2 -0
  107. package/lib/oracle/dispatch.mjs +18 -1
  108. package/lib/oracle/execute.mjs +39 -7
  109. package/lib/oracle/index.mjs +1 -1
  110. package/lib/oracle/org-graph.mjs +10 -5
  111. package/lib/oracle/policy.mjs +6 -0
  112. package/lib/oracle/read-model.mjs +103 -6
  113. package/lib/oracle/reconcile.mjs +31 -4
  114. package/lib/oracle/remediation-dispatch.mjs +53 -0
  115. package/lib/oracle/routing.mjs +5 -0
  116. package/lib/oracle/synthesize.mjs +80 -3
  117. package/lib/orchestration/routing-tables.mjs +44 -10
  118. package/lib/orchestration/runtime.mjs +2 -0
  119. package/lib/orchestration/worker.mjs +1 -1
  120. package/lib/orchestration-policy.mjs +351 -62
  121. package/lib/overrides/resolver.mjs +1 -12
  122. package/lib/parity.mjs +37 -11
  123. package/lib/policy/engine.mjs +80 -15
  124. package/lib/prompt-composer.js +51 -15
  125. package/lib/prompt-metadata.mjs +3 -2
  126. package/lib/providers/connection-probe.mjs +58 -0
  127. package/lib/providers/copilot-auth.mjs +1 -1
  128. package/lib/providers/credential-bootstrap.mjs +1 -1
  129. package/lib/providers/op-run.mjs +3 -4
  130. package/lib/providers/secret-resolver.mjs +31 -0
  131. package/lib/publish-tooling.mjs +3 -11
  132. package/lib/publish.mjs +12 -29
  133. package/lib/reconcile/mcp-entry-reconcile.mjs +14 -9
  134. package/lib/reflect.mjs +2 -2
  135. package/lib/registry/agent-manifest.mjs +190 -0
  136. package/lib/registry/assemble.mjs +133 -0
  137. package/lib/registry/catalog.mjs +171 -0
  138. package/lib/registry/cli.mjs +590 -6
  139. package/lib/registry/consolidation.mjs +5 -4
  140. package/lib/registry/docs-sync.mjs +67 -0
  141. package/lib/registry/loader.mjs +147 -0
  142. package/lib/registry/org-io.mjs +76 -0
  143. package/lib/registry/retired-paths.mjs +71 -0
  144. package/lib/registry/surface-map.mjs +5 -7
  145. package/lib/registry/validator.mjs +438 -0
  146. package/lib/research-execution-policy.mjs +173 -0
  147. package/lib/roles/catalog.mjs +4 -9
  148. package/lib/roles/fence.mjs +107 -1
  149. package/lib/roles/flavor-bindings.mjs +68 -0
  150. package/lib/roles/gateway.mjs +140 -29
  151. package/lib/roles/manifest.mjs +22 -13
  152. package/lib/roles/router.mjs +1 -1
  153. package/lib/scopes/enrich.mjs +67 -0
  154. package/lib/scopes/hooks.mjs +12 -0
  155. package/lib/{profiles → scopes}/lifecycle.mjs +70 -70
  156. package/lib/scopes/loader.mjs +104 -0
  157. package/lib/scopes/rebrand.mjs +32 -0
  158. package/lib/scopes/research-profile.mjs +16 -0
  159. package/lib/scopes/teams.mjs +98 -0
  160. package/lib/service-manager.mjs +1 -117
  161. package/lib/setup-prompts.mjs +1 -1
  162. package/lib/setup.mjs +6 -26
  163. package/lib/skills/composition-graph.mjs +98 -0
  164. package/lib/skills/router.mjs +80 -0
  165. package/lib/specialist-contracts.mjs +17 -18
  166. package/lib/specialists/postconditions.mjs +2 -2
  167. package/lib/specialists/prompt-schema.mjs +6 -5
  168. package/lib/specialists/roster.mjs +7 -12
  169. package/lib/specialists/schema.mjs +9 -6
  170. package/lib/status.mjs +25 -90
  171. package/lib/storage/file-lock.mjs +6 -3
  172. package/lib/telemetry/skill-calls.mjs +1 -1
  173. package/lib/templates/doc-presentation.mjs +63 -0
  174. package/lib/templates/visual-requirements.mjs +35 -2
  175. package/lib/term-format.mjs +107 -2
  176. package/lib/test-env-setup.mjs +21 -0
  177. package/lib/ui/components.mjs +42 -0
  178. package/lib/ui/glyphs.mjs +58 -0
  179. package/lib/ui/links.mjs +115 -0
  180. package/lib/ui/theme.mjs +108 -0
  181. package/lib/uninstall/uninstall.mjs +2 -1
  182. package/lib/validator.mjs +45 -8
  183. package/lib/validators/skill-effectiveness.mjs +174 -0
  184. package/lib/validators/skills.mjs +1 -1
  185. package/package.json +12 -8
  186. package/personas/construct.md +12 -3
  187. package/platforms/claude/CLAUDE.md +1 -1
  188. package/rules/common/beads-hygiene.md +52 -0
  189. package/rules/common/neurodivergent-output.md +4 -7
  190. package/rules/common/research.md +2 -0
  191. package/scripts/sync-specialists.mjs +269 -51
  192. package/skills/ai/prompt-optimizer.md +3 -3
  193. package/skills/brand/output-vibe.md +48 -0
  194. package/skills/docs/adr-workflow.md +3 -0
  195. package/skills/docs/backlog-proposal-workflow.md +3 -0
  196. package/skills/docs/codebase-research-workflow.md +5 -2
  197. package/skills/docs/customer-profile-workflow.md +3 -0
  198. package/skills/docs/document-ingest-workflow.md +3 -0
  199. package/skills/docs/evidence-ingest-workflow.md +4 -1
  200. package/skills/docs/init-project.md +1 -1
  201. package/skills/docs/prd-workflow.md +1 -1
  202. package/skills/docs/prfaq-workflow.md +3 -0
  203. package/skills/docs/product-intelligence-workflow.md +4 -1
  204. package/skills/docs/product-signal-workflow.md +3 -0
  205. package/skills/docs/research-workflow.md +13 -6
  206. package/skills/docs/runbook-workflow.md +3 -0
  207. package/skills/docs/strategy-workflow.md +3 -0
  208. package/skills/docs/user-research-workflow.md +6 -3
  209. package/skills/operating/orchestration-reference.md +1 -1
  210. package/skills/roles/{engineer.ai.md → ai-engineer.md} +2 -2
  211. package/skills/roles/architect.ai-systems.md +1 -1
  212. package/skills/roles/architect.data.md +1 -1
  213. package/skills/roles/architect.enterprise.md +1 -1
  214. package/skills/roles/architect.integration.md +1 -1
  215. package/skills/roles/architect.md +1 -1
  216. package/skills/roles/architect.platform.md +1 -1
  217. package/skills/roles/{product-manager.business-strategy.md → business-strategist.md} +3 -3
  218. package/skills/roles/data-analyst.experiment.md +1 -1
  219. package/skills/roles/data-analyst.md +1 -1
  220. package/skills/roles/data-analyst.product-intelligence.md +1 -1
  221. package/skills/roles/data-analyst.product.md +1 -1
  222. package/skills/roles/data-analyst.telemetry.md +1 -1
  223. package/skills/roles/{engineer.data.md → data-engineer.md} +2 -2
  224. package/skills/roles/data-engineer.pipeline.md +2 -2
  225. package/skills/roles/data-engineer.vector-retrieval.md +2 -2
  226. package/skills/roles/data-engineer.warehouse.md +2 -2
  227. package/skills/roles/debugger.md +1 -1
  228. package/skills/roles/designer.accessibility.md +1 -1
  229. package/skills/roles/designer.md +1 -1
  230. package/skills/roles/{reviewer.devil-advocate.md → devil-advocate.md} +2 -2
  231. package/skills/roles/{operator.docs.md → docs-keeper.md} +3 -3
  232. package/skills/roles/engineer.md +3 -7
  233. package/skills/roles/{reviewer.evaluator.md → evaluator.md} +2 -2
  234. package/skills/roles/{researcher.explorer.md → explorer.md} +2 -2
  235. package/skills/roles/{operator.md → operations.md} +2 -5
  236. package/skills/roles/orchestrator.md +1 -1
  237. package/skills/roles/{engineer.platform.md → platform-engineer.md} +2 -2
  238. package/skills/roles/product-manager.ai-product.md +1 -1
  239. package/skills/roles/product-manager.enterprise.md +1 -1
  240. package/skills/roles/product-manager.growth.md +1 -2
  241. package/skills/roles/product-manager.md +1 -2
  242. package/skills/roles/product-manager.platform.md +1 -1
  243. package/skills/roles/product-manager.product.md +1 -1
  244. package/skills/roles/qa.ai-eval.md +1 -1
  245. package/skills/roles/qa.api-contract.md +1 -1
  246. package/skills/roles/qa.data-pipeline.md +1 -1
  247. package/skills/roles/qa.md +1 -1
  248. package/skills/roles/qa.web-ui.md +1 -1
  249. package/skills/roles/{operator.release.md → release-manager.md} +3 -3
  250. package/skills/roles/researcher.md +1 -1
  251. package/skills/roles/reviewer.md +1 -1
  252. package/skills/roles/security.ai.md +1 -1
  253. package/skills/roles/security.appsec.md +1 -1
  254. package/skills/roles/security.cloud.md +1 -1
  255. package/skills/roles/security.legal-compliance.md +1 -1
  256. package/skills/roles/security.md +1 -1
  257. package/skills/roles/security.privacy.md +1 -1
  258. package/skills/roles/security.supply-chain.md +1 -1
  259. package/skills/roles/{operator.sre.md → sre.md} +3 -3
  260. package/skills/roles/{qa.test-automation.md → test-automation.md} +2 -2
  261. package/skills/roles/{reviewer.trace.md → trace-reviewer.md} +2 -2
  262. package/skills/roles/{researcher.ux.md → ux-researcher.md} +2 -2
  263. package/skills/routing.json +18 -0
  264. package/skills/routing.md +1 -1
  265. package/specialists/artifact-manifest.json +2 -1
  266. package/specialists/audit-enrichments.json +14 -14
  267. package/specialists/org/contracts/accessibility-to-qa.json +37 -0
  268. package/specialists/org/contracts/any-to-business-strategist.json +57 -0
  269. package/specialists/org/contracts/any-to-debugger.json +38 -0
  270. package/specialists/org/contracts/any-to-designer.json +33 -0
  271. package/specialists/org/contracts/any-to-docs-keeper.json +34 -0
  272. package/specialists/org/contracts/any-to-explorer.json +39 -0
  273. package/specialists/org/contracts/any-to-sre-incident.json +33 -0
  274. package/specialists/org/contracts/any-to-trace-reviewer.json +32 -0
  275. package/specialists/org/contracts/architect-to-ai-engineer.json +32 -0
  276. package/specialists/org/contracts/architect-to-data-engineer.json +52 -0
  277. package/specialists/org/contracts/architect-to-devil-advocate.json +38 -0
  278. package/specialists/org/contracts/architect-to-engineer.json +34 -0
  279. package/specialists/org/contracts/architect-to-evaluator.json +59 -0
  280. package/specialists/org/contracts/architect-to-legal-compliance.json +55 -0
  281. package/specialists/org/contracts/architect-to-operations.json +45 -0
  282. package/specialists/org/contracts/architect-to-platform-engineer.json +42 -0
  283. package/specialists/org/contracts/business-strategist-to-product-manager.json +39 -0
  284. package/specialists/org/contracts/construct-to-orchestrator.json +36 -0
  285. package/specialists/org/contracts/construct-to-rd-lead.json +53 -0
  286. package/specialists/org/contracts/data-analyst-to-product-manager.json +43 -0
  287. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +36 -0
  288. package/specialists/org/contracts/designer-to-accessibility.json +36 -0
  289. package/specialists/org/contracts/engineer-to-qa.json +34 -0
  290. package/specialists/org/contracts/engineer-to-reviewer.json +52 -0
  291. package/specialists/org/contracts/explorer-to-engineer.json +38 -0
  292. package/specialists/org/contracts/legal-compliance-to-release-manager.json +43 -0
  293. package/specialists/org/contracts/platform-engineer-to-engineer.json +31 -0
  294. package/specialists/org/contracts/product-manager-to-architect.json +71 -0
  295. package/specialists/org/contracts/product-manager-to-data-analyst.json +49 -0
  296. package/specialists/org/contracts/product-manager-to-ux-researcher.json +51 -0
  297. package/specialists/org/contracts/qa-to-release-manager.json +42 -0
  298. package/specialists/org/contracts/qa-to-test-automation.json +42 -0
  299. package/specialists/org/contracts/rd-lead-to-architect.json +38 -0
  300. package/specialists/org/contracts/researcher-to-architect.json +40 -0
  301. package/specialists/org/contracts/researcher-to-product-manager.json +57 -0
  302. package/specialists/org/contracts/reviewer-to-security.json +41 -0
  303. package/specialists/org/contracts/sre-to-release-manager.json +36 -0
  304. package/specialists/org/contracts/test-automation-to-engineer.json +34 -0
  305. package/specialists/org/contracts/trace-reviewer-to-sre.json +41 -0
  306. package/specialists/org/contracts/user-to-construct.json +30 -0
  307. package/specialists/org/groups/engineering-group.json +43 -0
  308. package/specialists/org/groups/governance-group.json +38 -0
  309. package/specialists/org/groups/operations-group.json +41 -0
  310. package/specialists/org/groups/product-group.json +46 -0
  311. package/specialists/org/groups/quality-group.json +42 -0
  312. package/specialists/org/groups/strategy-group.json +41 -0
  313. package/specialists/org/policies/action-approval.json +13 -0
  314. package/specialists/org/policies/agents-routing.json +13 -0
  315. package/specialists/org/policies/architecture.json +20 -0
  316. package/specialists/org/policies/bash-safety.json +13 -0
  317. package/specialists/org/policies/beads-hygiene.json +13 -0
  318. package/specialists/org/policies/bootstrap-state.json +13 -0
  319. package/specialists/org/policies/code-review.json +13 -0
  320. package/specialists/org/policies/coding-style.json +13 -0
  321. package/specialists/org/policies/comments.json +13 -0
  322. package/specialists/org/policies/commit-approval.json +13 -0
  323. package/specialists/org/policies/contract-preconditions.json +13 -0
  324. package/specialists/org/policies/deployment.json +20 -0
  325. package/specialists/org/policies/description.json +14 -0
  326. package/specialists/org/policies/design-approval.json +19 -0
  327. package/specialists/org/policies/doc-ownership.json +13 -0
  328. package/specialists/org/policies/file-path-fence.json +13 -0
  329. package/specialists/org/policies/framing.json +13 -0
  330. package/specialists/org/policies/git-workflow.json +13 -0
  331. package/specialists/org/policies/incident-response.json +15 -0
  332. package/specialists/org/policies/intake-triage.json +15 -0
  333. package/specialists/org/policies/neurodivergent-output.json +13 -0
  334. package/specialists/org/policies/no-fabrication.json +13 -0
  335. package/specialists/org/policies/patterns.json +13 -0
  336. package/specialists/org/policies/quality-gate-approval.json +17 -0
  337. package/specialists/org/policies/release-gates.json +13 -0
  338. package/specialists/org/policies/research-evidence.json +13 -0
  339. package/specialists/org/policies/review-before-change.json +13 -0
  340. package/specialists/org/policies/rollback.json +15 -0
  341. package/specialists/org/policies/scope-change.json +20 -0
  342. package/specialists/org/policies/secret-scan.json +13 -0
  343. package/specialists/org/policies/security-approval.json +17 -0
  344. package/specialists/org/policies/security.json +13 -0
  345. package/specialists/org/policies/session-efficiency.json +13 -0
  346. package/specialists/org/policies/skill-routing.json +13 -0
  347. package/specialists/org/policies/strategic-prioritization.json +17 -0
  348. package/specialists/org/policies/testing.json +13 -0
  349. package/specialists/org/policies/tool-invisibility.json +14 -0
  350. package/specialists/org/scopes/creative.json +54 -0
  351. package/specialists/org/scopes/operations.json +51 -0
  352. package/specialists/org/scopes/research.json +60 -0
  353. package/specialists/org/scopes/rnd.json +81 -0
  354. package/specialists/org/specialists/cx-accessibility.json +69 -0
  355. package/specialists/org/specialists/cx-ai-engineer.json +75 -0
  356. package/specialists/org/specialists/cx-architect.json +89 -0
  357. package/specialists/org/specialists/cx-business-strategist.json +72 -0
  358. package/specialists/org/specialists/cx-data-analyst.json +68 -0
  359. package/specialists/org/specialists/cx-data-engineer.json +71 -0
  360. package/specialists/org/specialists/cx-debugger.json +75 -0
  361. package/specialists/org/specialists/cx-designer.json +85 -0
  362. package/specialists/org/specialists/cx-devil-advocate.json +71 -0
  363. package/specialists/org/specialists/cx-docs-keeper.json +82 -0
  364. package/specialists/org/specialists/cx-engineer.json +106 -0
  365. package/specialists/org/specialists/cx-evaluator.json +69 -0
  366. package/specialists/org/specialists/cx-explorer.json +69 -0
  367. package/specialists/org/specialists/cx-legal-compliance.json +74 -0
  368. package/specialists/org/specialists/cx-operations.json +72 -0
  369. package/specialists/org/specialists/cx-oracle.json +46 -0
  370. package/specialists/org/specialists/cx-orchestrator.json +81 -0
  371. package/specialists/org/specialists/cx-platform-engineer.json +80 -0
  372. package/specialists/org/specialists/cx-product-manager.json +102 -0
  373. package/specialists/org/specialists/cx-qa.json +79 -0
  374. package/specialists/org/specialists/cx-rd-lead.json +73 -0
  375. package/specialists/org/specialists/cx-release-manager.json +77 -0
  376. package/specialists/org/specialists/cx-researcher.json +82 -0
  377. package/specialists/org/specialists/cx-reviewer.json +71 -0
  378. package/specialists/org/specialists/cx-security.json +91 -0
  379. package/specialists/org/specialists/cx-sre.json +94 -0
  380. package/specialists/org/specialists/cx-test-automation.json +69 -0
  381. package/specialists/org/specialists/cx-trace-reviewer.json +69 -0
  382. package/specialists/org/specialists/cx-ux-researcher.json +68 -0
  383. package/specialists/org/teams/accessibility-team.json +39 -0
  384. package/specialists/org/teams/design-team.json +40 -0
  385. package/specialists/org/teams/engineering-team.json +50 -0
  386. package/specialists/org/teams/governance-team.json +41 -0
  387. package/specialists/org/teams/operations-team.json +46 -0
  388. package/specialists/org/teams/product-management-team.json +45 -0
  389. package/specialists/org/teams/quality-team.json +49 -0
  390. package/specialists/org/teams/research-team.json +39 -0
  391. package/specialists/org/teams/strategy-team.json +48 -0
  392. package/specialists/org/teams/ux-research-team.json +39 -0
  393. package/specialists/prompts/_shared/validation-contract.md +1 -1
  394. package/specialists/prompts/_team-template.md +10 -0
  395. package/specialists/prompts/cx-accessibility.md +1 -0
  396. package/specialists/prompts/cx-ai-engineer.md +1 -1
  397. package/specialists/prompts/cx-business-strategist.md +1 -1
  398. package/specialists/prompts/cx-data-engineer.md +1 -1
  399. package/specialists/prompts/cx-designer.md +1 -0
  400. package/specialists/prompts/cx-devil-advocate.md +1 -1
  401. package/specialists/prompts/cx-docs-keeper.md +1 -1
  402. package/specialists/prompts/cx-evaluator.md +1 -1
  403. package/specialists/prompts/cx-explorer.md +1 -1
  404. package/specialists/prompts/cx-operations.md +1 -1
  405. package/specialists/prompts/cx-oracle.md +7 -3
  406. package/specialists/prompts/cx-orchestrator.md +17 -1
  407. package/specialists/prompts/cx-platform-engineer.md +1 -1
  408. package/specialists/prompts/cx-product-manager.md +2 -0
  409. package/specialists/prompts/cx-release-manager.md +1 -1
  410. package/specialists/prompts/cx-researcher.md +7 -4
  411. package/specialists/prompts/cx-sre.md +1 -1
  412. package/specialists/prompts/cx-test-automation.md +2 -2
  413. package/specialists/prompts/cx-trace-reviewer.md +3 -3
  414. package/specialists/prompts/cx-ux-researcher.md +2 -1
  415. package/templates/demos/scripts/architecture-review-adr.json +30 -0
  416. package/templates/demos/scripts/capability-contract.json +25 -0
  417. package/templates/demos/scripts/intake-triage.json +25 -0
  418. package/templates/demos/scripts/profile-doctor-health.json +25 -0
  419. package/templates/demos/tapes/agentic-platforms-prd.tape +2 -2
  420. package/templates/demos/tapes/architecture-review-adr.tape +44 -0
  421. package/templates/demos/tapes/capability-contract.tape +39 -0
  422. package/templates/demos/tapes/intake-triage.tape +39 -0
  423. package/templates/demos/tapes/profile-doctor-health.tape +39 -0
  424. package/templates/distribution/construct-brand.typ +23 -18
  425. package/templates/distribution/construct-decision.typ +1 -1
  426. package/templates/distribution/construct-pdf.typ +1 -1
  427. package/templates/distribution/construct-prd.typ +1 -1
  428. package/templates/distribution/construct-research.typ +1 -1
  429. package/templates/distribution/mermaid-puppeteer.json +8 -0
  430. package/templates/docs/persona-artifact.md +1 -1
  431. package/templates/docs/prd.md +6 -0
  432. package/apps/chat/engine/ai-sdk-agent.mjs +0 -183
  433. package/apps/chat/engine/loop-driver.mjs +0 -211
  434. package/apps/chat/engine/models.mjs +0 -122
  435. package/apps/chat/engine/provider-adapters.mjs +0 -171
  436. package/apps/chat/engine/tools/permission.mjs +0 -54
  437. package/apps/chat/engine/tools/primitives.mjs +0 -180
  438. package/apps/chat/engine/tools/registry.mjs +0 -122
  439. package/apps/chat/engine/turn-controls.mjs +0 -70
  440. package/lib/boundary.mjs +0 -127
  441. package/lib/certification/dashboard-api.mjs +0 -71
  442. package/lib/chat/cli.mjs +0 -333
  443. package/lib/chat/command-suggest.mjs +0 -161
  444. package/lib/chat/commands.mjs +0 -215
  445. package/lib/chat/config.mjs +0 -142
  446. package/lib/chat/context-compactor.mjs +0 -250
  447. package/lib/chat/context-continuation.mjs +0 -253
  448. package/lib/chat/continuation-source.mjs +0 -58
  449. package/lib/chat/demo-guide.mjs +0 -61
  450. package/lib/chat/design-tokens.mjs +0 -91
  451. package/lib/chat/desktop-binary.mjs +0 -81
  452. package/lib/chat/desktop-build.mjs +0 -130
  453. package/lib/chat/desktop-launcher.mjs +0 -133
  454. package/lib/chat/evidence.mjs +0 -145
  455. package/lib/chat/export.mjs +0 -74
  456. package/lib/chat/harness/driver.mjs +0 -91
  457. package/lib/chat/list-picker.mjs +0 -112
  458. package/lib/chat/model-picker.mjs +0 -356
  459. package/lib/chat/openrouter-fallback.mjs +0 -151
  460. package/lib/chat/permission-prompt.mjs +0 -33
  461. package/lib/chat/picker-catalog.mjs +0 -45
  462. package/lib/chat/policy-telemetry.mjs +0 -34
  463. package/lib/chat/present.mjs +0 -246
  464. package/lib/chat/session-context.mjs +0 -39
  465. package/lib/chat/session-persist.mjs +0 -73
  466. package/lib/chat/session-restore.mjs +0 -71
  467. package/lib/chat/session-settings.mjs +0 -53
  468. package/lib/chat/system-prompt.mjs +0 -52
  469. package/lib/chat/transparency.mjs +0 -93
  470. package/lib/chat/tui/color-scheme.mjs +0 -42
  471. package/lib/chat/tui/markdown.mjs +0 -123
  472. package/lib/chat/tui/presentation.mjs +0 -100
  473. package/lib/chat/tui/render.mjs +0 -500
  474. package/lib/chat/tui/turn-block.mjs +0 -284
  475. package/lib/chat/tui/turn-present.mjs +0 -18
  476. package/lib/chat/tui/turn-state.mjs +0 -88
  477. package/lib/chat/tui/usage.mjs +0 -122
  478. package/lib/chat/web-commands.mjs +0 -146
  479. package/lib/chat/web-launcher.mjs +0 -63
  480. package/lib/chat/web-picker-keys.mjs +0 -46
  481. package/lib/chat/web-session.mjs +0 -159
  482. package/lib/config/alias.mjs +0 -57
  483. package/lib/dashboard-demo.mjs +0 -71
  484. package/lib/dashboard-static.mjs +0 -175
  485. package/lib/install/desktop-binary-download.mjs +0 -88
  486. package/lib/profiles/loader.mjs +0 -123
  487. package/lib/profiles/rebrand.mjs +0 -46
  488. package/lib/server/auth.mjs +0 -169
  489. package/lib/server/chat-loop.mjs +0 -622
  490. package/lib/server/chat.mjs +0 -336
  491. package/lib/server/cors.mjs +0 -77
  492. package/lib/server/csrf.mjs +0 -103
  493. package/lib/server/demo-preview.mjs +0 -63
  494. package/lib/server/index.mjs +0 -2641
  495. package/lib/server/insights.mjs +0 -780
  496. package/lib/server/langfuse-login.mjs +0 -58
  497. package/lib/server/rate-limit.mjs +0 -93
  498. package/lib/server/telemetry-login.mjs +0 -100
  499. package/lib/server/webhook.mjs +0 -512
  500. package/specialists/contracts.json +0 -1032
  501. package/specialists/contracts.schema.json +0 -83
  502. package/specialists/policy-inventory.json +0 -188
  503. package/specialists/registry.json +0 -1412
  504. package/specialists/role-manifests.json +0 -217
  505. package/specialists/teams.json +0 -94
@@ -11,7 +11,7 @@
11
11
  * concern; here they mark the rule as intentionally scoped, not an orphan.
12
12
  * 2. REFERENCED — the canonical `rules/<dir>/<name>` path token (with or without
13
13
  * the .md) appears somewhere in the active surface (personas, CLAUDE.md,
14
- * skills, all of lib/ except the minified dashboard bundles, registry/
14
+ * skills, all of lib/, registry/
15
15
  * contracts, other rules, scripts, the CLI).
16
16
  * A rule that is neither glob-activated nor referenced is a true orphan — a
17
17
  * pruning candidate. A rule's own file never counts as referencing itself.
@@ -30,9 +30,7 @@ const SURFACE_DIRS = ['personas', 'skills', 'rules', 'lib', 'specialists', 'scri
30
30
  const SURFACE_FILES = ['CLAUDE.md', 'AGENTS.md', 'bin/construct', 'claude/settings.template.json'];
31
31
  const TEXT_EXT = new Set(['.md', '.mjs', '.js', '.json', '.txt', '.mdx']);
32
32
 
33
- // The minified Next.js dashboard bundles contain unrelated "rules/" tokens (the
34
- // markdown lexer's own grammar) that would read as false references.
35
- const SURFACE_SKIP = ['lib/server/static'];
33
+ const SURFACE_SKIP = [];
36
34
 
37
35
  function isSkipped(full, root) {
38
36
  const rel = path.relative(root, full);
@@ -6,18 +6,19 @@
6
6
  * Called by 'construct audit skills' and incorporated into 'construct doctor'.
7
7
  *
8
8
  * A `roles/<base>[.<flavor>]` skill is owned when `<base>` is a specialist OR a role
9
- * named in any profile (profiles/*.json) — a role skill loads when its specialist or
9
+ * named in any curated scope (specialists/org/scopes) — a role skill loads when its specialist or
10
10
  * profile role runs, even without a direct registry `skills:` binding. Counting only
11
11
  * registry bindings over-reported orphans (it missed profile roles like `operator`
12
12
  * from operations.json and conditional specialist flavors); see bead construct-ksfa.
13
13
  */
14
14
  import fs from 'node:fs';
15
+ import { loadRegistry } from './registry/loader.mjs';
15
16
  import path from 'node:path';
16
17
 
17
18
  function findConstructRoot(startPath = process.cwd()) {
18
19
  let current = path.resolve(startPath);
19
20
  while (true) {
20
- if (fs.existsSync(path.join(current, 'specialists', 'registry.json'))) return current;
21
+ if (fs.existsSync(path.join(current, 'specialists', 'org'))) return current;
21
22
  const parent = path.dirname(current);
22
23
  if (parent === current) return path.resolve(startPath);
23
24
  current = parent;
@@ -40,23 +41,18 @@ function collectSkillFiles(skillsDir) {
40
41
  return results;
41
42
  }
42
43
 
43
- // Collect every role name a profile declares — top-level `roles` and nested
44
- // `departments[].roles` — so a role skill counts as owned when a profile uses its
45
- // base even if no specialist declares the skill path directly.
44
+ import { collectScopeRoleIds } from './scopes/enrich.mjs';
45
+
46
46
  function collectProfileRoles(root) {
47
- const dir = path.join(root, 'profiles');
47
+ const dir = path.join(root, 'specialists', 'org', 'scopes');
48
48
  const roles = new Set();
49
- const scan = (node) => {
50
- if (Array.isArray(node)) { node.forEach(scan); return; }
51
- if (node && typeof node === 'object') {
52
- if (Array.isArray(node.roles)) for (const r of node.roles) if (typeof r === 'string') roles.add(r);
53
- for (const v of Object.values(node)) scan(v);
54
- }
55
- };
56
49
  try {
57
50
  for (const f of fs.readdirSync(dir)) {
58
51
  if (!f.endsWith('.json')) continue;
59
- try { scan(JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))); } catch { /* skip malformed */ }
52
+ try {
53
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
54
+ for (const r of collectScopeRoleIds(raw, { rootDir: root })) roles.add(r);
55
+ } catch { /* skip malformed */ }
60
56
  }
61
57
  } catch { /* no profiles dir */ }
62
58
  return roles;
@@ -70,17 +66,16 @@ function roleBase(skill) {
70
66
 
71
67
  export function auditSkills({ rootDir, silent = false } = {}) {
72
68
  const root = rootDir ?? findConstructRoot();
73
- const registryPath = path.join(root, 'specialists', 'registry.json');
74
69
  const skillsDir = path.join(root, 'skills');
75
70
 
76
- const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
71
+ const registry = loadRegistry({ rootDir: root });
77
72
  const allSkillFiles = collectSkillFiles(skillsDir);
78
73
 
79
74
  const declaredSkills = new Set();
80
75
  const agentsWithNoSkills = [];
81
76
  const missingSkillFiles = [];
82
77
 
83
- for (const agent of registry.specialists ?? []) {
78
+ for (const agent of Object.values(registry.specialists ?? {})) {
84
79
  const skills = agent.skills ?? [];
85
80
  if (skills.length === 0) {
86
81
  agentsWithNoSkills.push(agent.name);
@@ -93,12 +88,17 @@ export function auditSkills({ rootDir, silent = false } = {}) {
93
88
  }
94
89
  }
95
90
 
96
- // Owning role bases = specialist names (registry) profile role names. A role
97
- // skill is owned when its base is in this set, even without a direct binding.
91
+ // Owning role bases = specialist names, scope/org roles, and role-skill bases declared on specialists.
98
92
  const ownerBases = new Set([
99
- ...(registry.specialists ?? []).map((a) => a.name.replace(/^cx-/, '')),
93
+ ...Object.values(registry.specialists ?? {}).map((a) => a.name.replace(/^cx-/, '')),
100
94
  ...collectProfileRoles(root),
101
95
  ]);
96
+ for (const agent of Object.values(registry.specialists ?? {})) {
97
+ for (const skill of agent.skills ?? []) {
98
+ const base = roleBase(skill);
99
+ if (base) ownerBases.add(base);
100
+ }
101
+ }
102
102
  const isOwned = (s) => declaredSkills.has(s) || (ownerBases.has(roleBase(s)));
103
103
 
104
104
  const orphanSkills = [...allSkillFiles].filter((s) => !isOwned(s));
@@ -159,6 +159,18 @@ export async function runAuditSkillsCli(args = []) {
159
159
  const { runSkillInventoryAuditCli } = await import('./certification/skill-inventory.mjs');
160
160
  return runSkillInventoryAuditCli(args.filter((a) => a !== '--inventory'), { rootDir });
161
161
  }
162
+ if (args.includes('--composition')) {
163
+ const { auditSkillComposition } = await import('./skills/composition-graph.mjs');
164
+ const result = auditSkillComposition({ rootDir });
165
+ if (args.includes('--json')) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
166
+ else {
167
+ process.stdout.write(`Skill composition: ${result.stats.onDisk} on disk, ${result.blocking.length} blocking\n`);
168
+ for (const b of result.blocking) process.stderr.write(` ✗ ${b}\n`);
169
+ process.stdout.write(result.pass ? ' Result: PASS\n' : ' Result: FAIL\n');
170
+ }
171
+ if (!result.pass) process.exit(1);
172
+ return result;
173
+ }
162
174
  const result = auditSkills({ rootDir });
163
175
  if (!result.pass) process.exit(1);
164
176
  }
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import fs from 'node:fs';
10
+ import { loadRegistry } from './registry/loader.mjs';
10
11
  import path from 'node:path';
11
12
  import { auditSkills } from './audit-skills.mjs';
12
13
  import { loadArtifactManifest } from './artifact-manifest.mjs';
@@ -17,14 +18,14 @@ import { validateAllRoleOverlays } from './certification/role-overlays.mjs';
17
18
  import { ROLE_DIRECTIVE_RE } from './role-preload.mjs';
18
19
 
19
20
  function findConstructRoot(startPath = process.cwd()) {
20
- let current = path.resolve(startPath);
21
- while (true) {
22
- if (fs.existsSync(path.join(current, 'specialists', 'registry.json'))) return current;
23
- const parent = path.dirname(current);
24
- if (parent === current) return path.resolve(startPath);
25
- current = parent;
26
- }
27
- }
21
+ let current = path.resolve(startPath);
22
+ while (true) {
23
+ if (fs.existsSync(path.join(current, 'specialists', 'org'))) return current;
24
+ const parent = path.dirname(current);
25
+ if (parent === current) return path.resolve(startPath);
26
+ current = parent;
27
+ }
28
+ }
28
29
 
29
30
  function readJson(p) {
30
31
  return JSON.parse(fs.readFileSync(p, 'utf8'));
@@ -85,7 +86,7 @@ function crossChecks({ registry, manifest, skillAudit, allSkills, contractDocTyp
85
86
  const issues = [];
86
87
  const manifestTypes = new Set(Object.keys(manifest.artifacts ?? {}));
87
88
 
88
- for (const agent of registry.specialists ?? []) {
89
+ for (const agent of Object.values(registry.specialists ?? {})) {
89
90
  for (const skill of agent.skills ?? []) {
90
91
  if (!allSkills.includes(skill)) {
91
92
  issues.push({ kind: 'missing-skill-file', agent: agent.name, skill });
@@ -114,24 +115,41 @@ function crossChecks({ registry, manifest, skillAudit, allSkills, contractDocTyp
114
115
  }
115
116
 
116
117
  export function auditSpecialists({ rootDir, silent = false } = {}) {
117
- const root = rootDir ?? findConstructRoot();
118
- const registry = readJson(path.join(root, 'specialists', 'registry.json'));
119
- const contractsDoc = readJson(path.join(root, 'specialists', 'contracts.json'));
120
- const enrichments = readJson(path.join(root, 'specialists', 'audit-enrichments.json'));
121
- const manifest = loadArtifactManifest({ rootDir: root });
122
- const skillAudit = auditSkills({ rootDir: root, silent: true });
123
- const allSkills = collectSkillFiles(path.join(root, 'skills'));
124
- const contractDocTypes = docTypesFromContracts(contractsDoc.contracts ?? []);
125
- const crossCheckIssues = crossChecks({ registry, manifest, skillAudit, allSkills, contractDocTypes });
118
+ const root = rootDir ?? findConstructRoot();
119
+
120
+ const unifiedRegistry = loadRegistry({ rootDir: root });
121
+
122
+ // Convert unified registry to legacy registry shape for compatibility
123
+ const registry = {
124
+ specialists: Object.entries(unifiedRegistry.specialists || {}).map(([id, spec]) => ({
125
+ name: spec.name,
126
+ displayName: spec.displayName,
127
+ description: spec.description,
128
+ team: spec.team,
129
+ role: spec.role,
130
+ skills: spec.skills,
131
+ promptFile: spec.promptFile,
132
+ docArtifacts: spec.docArtifacts,
133
+ ...spec
134
+ }))
135
+ };
136
+
137
+ const contractsDoc = { contracts: Object.values(unifiedRegistry.contracts || {}) };
138
+ const enrichments = readJson(path.join(root, 'specialists', 'audit-enrichments.json'));
139
+ const manifest = loadArtifactManifest({ rootDir: root });
140
+ const skillAudit = auditSkills({ rootDir: root, silent: true });
141
+ const allSkills = collectSkillFiles(path.join(root, 'skills'));
142
+ const contractDocTypes = docTypesFromContracts(contractsDoc.contracts ?? []);
143
+ const crossCheckIssues = crossChecks({ registry, manifest, skillAudit, allSkills, contractDocTypes });
126
144
 
127
- const specialists = (registry.specialists ?? []).map((agent) => {
128
- const promptPath = agent.promptFile
129
- ? path.join(root, agent.promptFile)
130
- : null;
131
- const meta = enrichments.specialists?.[agent.name] ?? {};
132
- const perspective = resolvePerspectiveFromPrompt(agent.name, { rootDir: root, registry });
133
- return {
134
- name: agent.name,
145
+ const specialists = Object.values(registry.specialists ?? {}).map((agent) => {
146
+ const promptPath = agent.promptFile
147
+ ? path.join(root, agent.promptFile)
148
+ : null;
149
+ const meta = enrichments.specialists?.[agent.name] ?? {};
150
+ const perspective = resolvePerspectiveFromPrompt(agent.name, { rootDir: root, registry });
151
+ return {
152
+ name: agent.name,
135
153
  id: `cx-${agent.name}`,
136
154
  humanEquivalent: meta.humanEquivalent ?? 'unknown',
137
155
  primaryOutputs: agent.docArtifacts ?? [],
package/lib/auto-docs.mjs CHANGED
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import fs from 'node:fs';
18
+ import { loadRegistry } from './registry/loader.mjs';
18
19
  import path from 'node:path';
19
20
  import { execSync } from 'node:child_process';
20
21
 
@@ -90,7 +91,7 @@ function buildCoreDocsContract() {
90
91
  }
91
92
 
92
93
  const DIR_DESCRIPTIONS = {
93
- apps: 'User-facing apps shipped from this repo (chat, dashboard, docs)',
94
+ apps: 'User-facing apps shipped from this repo (dashboard, docs)',
94
95
  bin: 'CLI entrypoint (`construct`)',
95
96
  commands: 'Command prompt assets',
96
97
  config: 'Repo-wide controlled vocabulary (tag-vocabulary.json)',
@@ -165,13 +166,13 @@ function buildHooksTable(rootDir) {
165
166
  }
166
167
 
167
168
  function buildAgentsTable(rootDir) {
168
- const registryPath = path.join(rootDir, 'specialists', 'registry.json');
169
+ const registryPath = path.join(rootDir, 'specialists', 'org');
169
170
  let registry;
170
171
  try { registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')); } catch { return ''; }
171
172
 
172
173
  const agents = [
173
174
  ...(registry.orchestrator ? [{ ...registry.orchestrator, name: registry.orchestrator.name }] : []),
174
- ...(registry.specialists ?? []),
175
+ ...Object.values(registry.specialists ?? {}),
175
176
  ];
176
177
  if (!agents.length) return '';
177
178
 
@@ -410,10 +411,10 @@ function renderAgentsPage(rootDir) {
410
411
  return [
411
412
  '---',
412
413
  'title: Specialists',
413
- 'description: The 28 specialists behind the construct persona. Generated from specialists/registry.json.',
414
+ 'description: The 28 specialists behind the construct persona. Generated from specialists/org.',
414
415
  '---',
415
416
  '',
416
- '> Generated from `specialists/registry.json`. Re-run `construct docs:site` to refresh.',
417
+ '> Generated from `specialists/org`. Re-run `construct docs:site` to refresh.',
417
418
  '',
418
419
  'Construct ships one persona (`construct`) and 28 specialists behind it. You address `@construct` for all everyday work; it routes to specialists internally. Each specialist has a role, model tier, and prompt file that defines its decision authority.',
419
420
  '',
@@ -15,10 +15,10 @@ export const RETIRED_FONT_RE =
15
15
  /Plus\s+Jakarta|PlusJakarta|Geist(?:Mono)?|IBM\s+Plex|Libertinus|font-family:\s*["']?Inter["']?/i;
16
16
 
17
17
  const CLI_SUBCOMMANDS =
18
- 'doctor|init|sync|dev|stop|status|intake|chat|install|drop|search|prune|upgrade|config|list|graph|beads|export|publish|artifact|demo|oracle|ci|acp|profile|models|uninstall|reconcile|embed|ingest|classify|workflow|capability';
18
+ 'doctor|init|sync|dev|stop|status|intake|install|drop|search|prune|upgrade|config|list|graph|beads|export|publish|artifact|demo|oracle|ci|acp|profile|models|uninstall|reconcile|embed|ingest|classify|workflow|capability';
19
19
 
20
20
  const PRODUCT_NOUN_PHRASES =
21
- /\bConstruct (?:install|MCP server|MCP tools?|workflow|chat|package|repo|surfaces?|integration|orchestrator|daemon|specialists?|persona|brand|tooling|standards|config|adapters?|platform|version|release|hooks?|memory|loop|Dashboard|Chat|capability)\b/i;
21
+ /\bConstruct (?:install|MCP server|MCP tools?|workflow|package|repo|surfaces?|integration|orchestrator|daemon|specialists?|persona|brand|tooling|standards|config|adapters?|platform|version|release|hooks?|memory|loop|Dashboard|capability)\b/i;
22
22
 
23
23
  const UNBACKTICKED_CLI_RE = new RegExp(
24
24
  `(?<![\`~/])construct\\s+(${CLI_SUBCOMMANDS})(?:\\s+[\\w:-]+)*\\b`,
@@ -40,7 +40,7 @@ const DOC_EXEMPT =
40
40
  /(?:^|\/)(docs\/STYLE\.md|docs\/guides\/reference\/branding\.md|templates\/distribution\/fonts\/README\.md)$/;
41
41
 
42
42
  const HOOK_SCOPED =
43
- /^(docs\/|skills\/|specialists\/|personas\/|templates\/|rules\/|apps\/(dashboard|docs|chat)\/)/;
43
+ /^(docs\/|skills\/|specialists\/|personas\/|templates\/|rules\/|apps\/(dashboard|docs)\/)/;
44
44
 
45
45
  export function isBrandHookPath(relPath) {
46
46
  if (!relPath || EXCLUDE_PATH.test(relPath) || DOC_EXEMPT.test(relPath)) return false;
@@ -2,8 +2,8 @@
2
2
  * lib/brand-tokens.mjs — Construct distribution brand primitives.
3
3
  *
4
4
  * Monochrome ink ramp from templates/distribution/construct-brand.typ plus Space
5
- * Grotesk / JetBrains Mono typography. INK/FONTS/STATUS feed chat surfaces;
6
- * BRAND_TOKENS feed publish exports, deck HTML, and PPTX.
5
+ * Grotesk / JetBrains Mono typography. BRAND_TOKENS feed publish exports,
6
+ * deck HTML, and PPTX.
7
7
  */
8
8
 
9
9
  export const INK = Object.freeze({
@@ -73,7 +73,10 @@ export function buildGoldenFixtureMarkdown(type, { rootDir } = {}) {
73
73
 
74
74
  for (const section of entry.structureRequirements ?? []) {
75
75
  lines.push(`## ${section}`, '');
76
- if (proseAdded < proseMinimum) {
76
+ if (section === 'Phases' && type === 'prd') {
77
+ lines.push('### Phase 1: Fixture delivery', '', '- **Goal**: Ship an isolated billing ledger per tenant.', '- **Status**: not started', '', '**Functional**', '', '- **FR-1.1**: Each tenant invoice derives only from that tenant ledger events.', ' - *Acceptance*: Reconciliation test passes without cross-tenant reads.', '');
78
+ proseAdded += 1;
79
+ } else if (proseAdded < proseMinimum) {
77
80
  lines.push(proseBlock(section));
78
81
  proseAdded += 1;
79
82
  } else {
@@ -1,9 +1,9 @@
1
1
  /**
2
- * lib/certification/demo-parity.mjs — cross-surface demo parity report generator.
2
+ * lib/certification/demo-parity.mjs — demo terminal-harness report generator.
3
3
  *
4
- * Compares Tauri, web cockpit, and VHS terminal harness markers for each canonical
5
- * demo. Mismatches are recorded for follow-up beads; acceptable divergences cite
6
- * platform constraints with evidence paths.
4
+ * Verifies each canonical demo ships a VHS terminal tape. Terminal is the sole
5
+ * guided demo surface after web-deprecation retired the desktop window and web cockpit;
6
+ * acceptable divergences cite platform constraints with evidence paths.
7
7
  */
8
8
 
9
9
  import fs from 'node:fs';
@@ -12,26 +12,6 @@ import path from 'node:path';
12
12
  import { loadCanonicalScenarios } from './canonical-scenarios.mjs';
13
13
 
14
14
  const SURFACE_MARKERS = Object.freeze({
15
- tauri: {
16
- label: 'Tauri desktop',
17
- probe: (root, demo) => {
18
- const launcher = path.join(root, 'tests/functional/chat-desktop-launcher.functional.test.mjs');
19
- if (!fs.existsSync(launcher)) return { ok: false, detail: 'missing desktop launcher test' };
20
- const src = fs.readFileSync(launcher, 'utf8');
21
- return { ok: /surface=desktop/.test(src), detail: launcher };
22
- },
23
- },
24
- web: {
25
- label: 'Web cockpit',
26
- probe: (root, demo) => {
27
- const routeStrip = path.join(root, 'apps/chat/web/components/route-strip.tsx');
28
- const tokens = path.join(root, 'apps/chat/web/theme/tokens.css');
29
- if (!fs.existsSync(routeStrip) || !fs.existsSync(tokens)) {
30
- return { ok: false, detail: 'missing web cockpit markers' };
31
- }
32
- return { ok: true, detail: routeStrip };
33
- },
34
- },
35
15
  terminal: {
36
16
  label: 'VHS terminal',
37
17
  probe: (root, demo) => {
@@ -45,15 +25,9 @@ const SURFACE_MARKERS = Object.freeze({
45
25
 
46
26
  const ACCEPTABLE_DIVERGENCES = Object.freeze([
47
27
  {
48
- id: 'tauri-native-chrome',
49
- surface: 'tauri',
50
- reason: 'Desktop shell uses native window chrome; web uses browser viewport.',
51
- evidence: 'apps/chat/desktop/',
52
- },
53
- {
54
- id: 'vhs-terminal-only',
28
+ id: 'vhs-tape-direct',
55
29
  surface: 'terminal',
56
- reason: 'VHS tapes assert terminal UX; Tauri/web use GUI interaction models.',
30
+ reason: 'VHS tapes assert the scripted terminal recording directly.',
57
31
  evidence: 'templates/demos/tapes/',
58
32
  },
59
33
  ]);
@@ -34,9 +34,10 @@ export function realLlmOptInEnabled(env = process.env) {
34
34
  }
35
35
 
36
36
  export function realLlmCredentialsPresent(env = process.env) {
37
- return hasSecret('OPENROUTER_API_KEY', { env })
38
- || hasSecret('ANTHROPIC_API_KEY', { env })
39
- || hasSecret('OPENAI_API_KEY', { env })
37
+ const allowAmbient = env === process.env;
38
+ return hasSecret('OPENROUTER_API_KEY', { env, allowAmbient })
39
+ || hasSecret('ANTHROPIC_API_KEY', { env, allowAmbient })
40
+ || hasSecret('OPENAI_API_KEY', { env, allowAmbient })
40
41
  || hasStoredCredential();
41
42
  }
42
43
 
@@ -51,25 +52,26 @@ export function realLlmSkipReason(env = process.env) {
51
52
  }
52
53
 
53
54
  export function resolveRealLlmProvider(env = process.env) {
55
+ const allowAmbient = env === process.env;
54
56
  const requested = (env[REAL_LLM_PROVIDER_ENV] || DEFAULT_REAL_LLM_PROVIDER).trim().toLowerCase();
55
57
  const modelOverride = env[REAL_LLM_MODEL_ENV]?.trim() || null;
56
58
 
57
59
  if (requested === 'openrouter') {
58
- if (!hasSecret('OPENROUTER_API_KEY', { env })) {
60
+ if (!hasSecret('OPENROUTER_API_KEY', { env, allowAmbient })) {
59
61
  return { skip: 'OPENROUTER_API_KEY required for OpenRouter real-LLM scenarios' };
60
62
  }
61
63
  return { provider: 'openrouter', model: modelOverride || DEFAULT_REAL_LLM_MODEL };
62
64
  }
63
65
 
64
66
  if (requested === 'anthropic') {
65
- if (!hasSecret('ANTHROPIC_API_KEY', { env })) {
67
+ if (!hasSecret('ANTHROPIC_API_KEY', { env, allowAmbient })) {
66
68
  return { skip: 'ANTHROPIC_API_KEY required for Anthropic real-LLM scenarios' };
67
69
  }
68
70
  return { provider: 'anthropic', model: modelOverride || 'claude-sonnet-4-20250514' };
69
71
  }
70
72
 
71
73
  if (requested === 'openai') {
72
- if (!hasSecret('OPENAI_API_KEY', { env })) {
74
+ if (!hasSecret('OPENAI_API_KEY', { env, allowAmbient })) {
73
75
  return { skip: 'OPENAI_API_KEY required for OpenAI real-LLM scenarios' };
74
76
  }
75
77
  return { provider: 'openai', model: modelOverride || 'gpt-4o-mini' };
@@ -125,7 +127,9 @@ export function buildPrdPolishMessages({ requestSummary = S3_REQUEST, specialist
125
127
  const system = [
126
128
  'You are cx-product-manager synthesizing a multi-specialist orchestration run into one release-quality PRD markdown file.',
127
129
  'Output markdown only — no preamble.',
128
- 'Required ## sections (exact headings): Problem, Goals, Success metrics, Risks and mitigations.',
130
+ 'Required ## sections (exact headings): Problem, Goals, Success metrics, Phases, Risks and mitigations.',
131
+ 'Under Phases include at least one phase with **FR-<phase>.<n>:** functional requirements and *Acceptance:* criteria per requirement.',
132
+ 'You may instead use ## Requirements (or Functional requirements) and ## Acceptance criteria when requirements are not phased inline.',
129
133
  'Under Problem and Goals write multi-sentence paragraphs (not bullet-only outlines).',
130
134
  'Include a ```mermaid flowchart``` diagram for the user flow.',
131
135
  'Include a markdown table with columns: Metric | Baseline | Target (header row required).',
@@ -140,6 +144,50 @@ function resolvePolishModel(env = process.env) {
140
144
  return env[REAL_LLM_POLISH_MODEL_ENV]?.trim() || DEFAULT_REAL_LLM_POLISH_MODEL;
141
145
  }
142
146
 
147
+ export const S3_QUALITY_THRESHOLDS = Object.freeze({ minProse: 2, minCitations: 1 });
148
+
149
+ export function formatArtifactQualityVerdictDetail(
150
+ verdict,
151
+ { minProse = S3_QUALITY_THRESHOLDS.minProse, minCitations = S3_QUALITY_THRESHOLDS.minCitations } = {},
152
+ ) {
153
+ if (!verdict || typeof verdict !== 'object') {
154
+ return 'quality gate not met after polish';
155
+ }
156
+ const structurePart = verdict.structure?.ok
157
+ ? 'structure: ok'
158
+ : `structure: fail (${(verdict.structure?.errors ?? []).join('; ') || 'unknown'})`;
159
+ const prosePart = `prose: ${verdict.prose?.paragraphs ?? 0}/${minProse} (${verdict.prose?.ok ? 'ok' : 'fail'})`;
160
+ const researchPart = `research: citations=${verdict.research?.citations ?? 0}/${minCitations} unverified=${Boolean(verdict.research?.unverifiedDiscipline)} (${verdict.research?.ok ? 'ok' : 'fail'})`;
161
+ return `quality gate not met after polish; ${structurePart}; ${prosePart}; ${researchPart}`;
162
+ }
163
+
164
+ export function buildS3QualityGateFailure({
165
+ verdict,
166
+ polishModel = null,
167
+ rawOutputLength = null,
168
+ thresholds = S3_QUALITY_THRESHOLDS,
169
+ } = {}) {
170
+ return {
171
+ scenarioId: 'real-llm.s3',
172
+ status: 'inconclusive',
173
+ detail: formatArtifactQualityVerdictDetail(verdict, thresholds),
174
+ verdict,
175
+ qualityThresholds: thresholds,
176
+ polishModel,
177
+ rawOutputLength,
178
+ };
179
+ }
180
+
181
+ export function redactRealLlmGateEvidence(result) {
182
+ if (!result?.verdict) return null;
183
+ return JSON.stringify({
184
+ artifactQuality: result.verdict,
185
+ qualityThresholds: result.qualityThresholds ?? S3_QUALITY_THRESHOLDS,
186
+ polishModel: result.polishModel ?? null,
187
+ rawOutputLength: result.rawOutputLength ?? null,
188
+ });
189
+ }
190
+
143
191
  async function polishSpecialistOutputsToPrd({ specialistOutputs, requestSummary, env, fetchImpl = globalThis.fetch }) {
144
192
  if (!hasSecret('OPENROUTER_API_KEY', { env })) {
145
193
  return { skip: 'OPENROUTER_API_KEY required for PRD polish synthesis' };
@@ -230,16 +278,13 @@ export async function runRealLlmS3({ env = process.env, cleanup = true } = {}) {
230
278
 
231
279
  const prdPath = join(cwd, 'prd-output.md');
232
280
  writeFileSync(prdPath, polished.text);
233
- const verdict = assessArtifactQuality(prdPath, 'prd', { minProse: 2, minCitations: 1 });
281
+ const verdict = assessArtifactQuality(prdPath, 'prd', S3_QUALITY_THRESHOLDS);
234
282
  if (!verdict.ok) {
235
- return {
236
- scenarioId: 'real-llm.s3',
237
- status: 'inconclusive',
238
- detail: 'quality gate not met after polish',
283
+ return buildS3QualityGateFailure({
239
284
  verdict,
240
285
  polishModel: polished.polishModel,
241
286
  rawOutputLength: rawLongest.length,
242
- };
287
+ });
243
288
  }
244
289
  return {
245
290
  scenarioId: 'real-llm.s3',
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import fs from 'node:fs';
10
+ import { loadRegistry } from '../registry/loader.mjs';
10
11
  import path from 'node:path';
11
12
 
12
13
  import { splitFrontmatter } from '../specialists/prompt-schema.mjs';
@@ -14,7 +15,7 @@ import { splitFrontmatter } from '../specialists/prompt-schema.mjs';
14
15
  function findConstructRoot(startPath = process.cwd()) {
15
16
  let current = path.resolve(startPath);
16
17
  while (true) {
17
- if (fs.existsSync(path.join(current, 'specialists', 'registry.json'))) return current;
18
+ if (fs.existsSync(path.join(current, 'specialists', 'org'))) return current;
18
19
  const parent = path.dirname(current);
19
20
  if (parent === current) return path.resolve(startPath);
20
21
  current = parent;
@@ -60,7 +61,7 @@ export function buildRoleCard(agent, { rootDir } = {}) {
60
61
  refusalBoundaries,
61
62
  modelTier: agent.modelTier ?? null,
62
63
  sources: {
63
- registry: 'specialists/registry.json',
64
+ registry: 'specialists/org',
64
65
  promptFile: agent.promptFile ?? null,
65
66
  },
66
67
  };
@@ -68,11 +69,11 @@ export function buildRoleCard(agent, { rootDir } = {}) {
68
69
 
69
70
  export function writeRoleCards({ rootDir } = {}) {
70
71
  const root = findConstructRoot(rootDir);
71
- const registry = JSON.parse(fs.readFileSync(path.join(root, 'specialists', 'registry.json'), 'utf8'));
72
+ const registry = loadRegistry({ rootDir: root });
72
73
  const dir = roleCardsDir(root);
73
74
  fs.mkdirSync(dir, { recursive: true });
74
75
  const written = [];
75
- for (const agent of registry.specialists ?? []) {
76
+ for (const agent of Object.values(registry.specialists ?? {})) {
76
77
  const card = buildRoleCard(agent, { rootDir: root });
77
78
  const file = path.join(dir, `${card.specialistId}.role-card.json`);
78
79
  fs.writeFileSync(file, `${JSON.stringify(card, null, 2)}\n`);
@@ -83,10 +84,10 @@ export function writeRoleCards({ rootDir } = {}) {
83
84
 
84
85
  export function validateRoleCards({ rootDir } = {}) {
85
86
  const root = findConstructRoot(rootDir);
86
- const registry = JSON.parse(fs.readFileSync(path.join(root, 'specialists', 'registry.json'), 'utf8'));
87
+ const registry = loadRegistry({ rootDir: root });
87
88
  const errors = [];
88
89
  const cards = [];
89
- for (const agent of registry.specialists ?? []) {
90
+ for (const agent of Object.values(registry.specialists ?? {})) {
90
91
  const specialistId = `cx-${agent.name}`;
91
92
  const file = path.join(roleCardsDir(root), `${specialistId}.role-card.json`);
92
93
  if (!fs.existsSync(file)) {
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import fs from 'node:fs';
9
+ import { loadRegistry } from '../registry/loader.mjs';
9
10
  import path from 'node:path';
10
11
 
11
12
  import { splitFrontmatter } from '../specialists/prompt-schema.mjs';
@@ -21,7 +22,7 @@ const OVERLAY_CLASSES = Object.freeze({
21
22
  function findConstructRoot(startPath = process.cwd()) {
22
23
  let current = path.resolve(startPath);
23
24
  while (true) {
24
- if (fs.existsSync(path.join(current, 'specialists', 'registry.json'))) return current;
25
+ if (fs.existsSync(path.join(current, 'specialists', 'org'))) return current;
25
26
  const parent = path.dirname(current);
26
27
  if (parent === current) return path.resolve(startPath);
27
28
  current = parent;
@@ -29,12 +30,11 @@ function findConstructRoot(startPath = process.cwd()) {
29
30
  }
30
31
 
31
32
  function readRegistry(rootDir) {
32
- const file = path.join(findConstructRoot(rootDir), 'specialists', 'registry.json');
33
- return JSON.parse(fs.readFileSync(file, 'utf8'));
33
+ return loadRegistry({ rootDir: findConstructRoot(rootDir) });
34
34
  }
35
35
 
36
36
  function specialistIds(registry) {
37
- return new Set((registry.specialists ?? []).map((s) => `cx-${s.name}`));
37
+ return new Set(Object.values(registry.specialists ?? {}).map((s) => `cx-${s.name}`));
38
38
  }
39
39
 
40
40
  function countAntiPatterns(body) {
@@ -22,7 +22,7 @@ import { measurePromptBudgetChains } from './prompt-budget.mjs';
22
22
  import { validateAllArtifactProvenance } from './artifact-provenance.mjs';
23
23
  import { validateDocumentWorkflowCertification } from './document-workflow.mjs';
24
24
  import { buildDemoParityReport } from './demo-parity.mjs';
25
- import { runRealLlmS3, runRealLlmS8 } from './real-llm-scenarios.mjs';
25
+ import { redactRealLlmGateEvidence, runRealLlmS3, runRealLlmS8 } from './real-llm-scenarios.mjs';
26
26
  import { decidePromotion } from '../evals/gates.mjs';
27
27
  import { buildScenarioEvaluationReport, runScenarioEvalGates } from './eval-bridge.mjs';
28
28
  import {
@@ -106,7 +106,14 @@ async function runGate(gate, { root, scenario }) {
106
106
  const runner = gate.scenario === 's8' ? runRealLlmS8 : runRealLlmS3;
107
107
  const result = await runner({ env: process.env });
108
108
  if (result.status === 'inconclusive') {
109
- return { id: gate.id, pass: false, detail: result.skip ?? result.detail ?? 'inconclusive', inconclusive: true };
109
+ const evidence = redactRealLlmGateEvidence(result);
110
+ return {
111
+ id: gate.id,
112
+ pass: false,
113
+ detail: result.skip ?? result.detail ?? 'inconclusive',
114
+ ...(evidence ? { evidence } : {}),
115
+ inconclusive: true,
116
+ };
110
117
  }
111
118
  return { id: gate.id, pass: result.status === 'pass', detail: result.detail ?? result.status };
112
119
  }