@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
@@ -7,11 +7,11 @@
7
7
  * 2. MCP registration ↔ impl: every `name === '<tool>'` branch in the MCP
8
8
  * dispatch resolves to an imported function, and every exported tool
9
9
  * function in lib/mcp/tools/*.mjs is wired into the dispatch.
10
- * 3. Roles ↔ registry: every persona key in specialists/role-manifests.json
11
- * resolves to a persona or agent name in specialists/registry.json.
10
+ * 3. Roles ↔ registry: every persona key in specialists/org
11
+ * resolves to a persona or agent name in specialists/org.
12
12
  * 4. Persona promptFiles: every registry.personas[].promptFile and
13
13
  * registry.agents[].promptFile exists on disk.
14
- * 5. Contracts ↔ schemas: specialists/contracts.json passes validateContractsFile
14
+ * 5. Contracts ↔ schemas: specialists/org passes validateContractsFile
15
15
  * (cross-file refs, producer/consumer names, schema paths).
16
16
  *
17
17
  * Each violation produces an audit record. Blocking severity escalates to a
@@ -19,7 +19,8 @@
19
19
  * directory listings, no network.
20
20
  */
21
21
 
22
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
22
+ import { existsSync, readFileSync, statSync } from 'node:fs';
23
+ import { loadRegistry } from '../../registry/loader.mjs';
23
24
  import { join, dirname, resolve } from 'node:path';
24
25
  import os from 'node:os';
25
26
  import { fileURLToPath } from 'node:url';
@@ -89,18 +90,39 @@ export async function runAllChecks({ repoRoot = REPO_ROOT } = {}) {
89
90
  const hosts = checkHostSurfaces({ repoRoot });
90
91
  collect(hosts, findings, passed, 'host-surfaces');
91
92
 
93
+ const { runCredentialParityChecks } = await import('./credential-parity.mjs');
94
+ const credentials = await runCredentialParityChecks({ rootDir: repoRoot });
95
+ collect(credentials, findings, passed, 'credential-parity');
96
+
92
97
  return { findings, passed };
93
98
  }
94
99
 
100
+ // Drift categories split into two operator tiers. `actionable` findings are
101
+ // project state a user can fix and surface by default. `internal` findings are
102
+ // package/maintainer diagnostics about Construct's own registry and MCP wiring —
103
+ // never user-actionable in a consumer project (this watcher resolves REPO_ROOT to
104
+ // the installed package), so they stay behind `doctor consistency --strict`.
105
+
106
+ const CATEGORY_TIERS = {
107
+ 'mcp-drift': 'internal',
108
+ 'roles-drift': 'internal',
109
+ };
110
+
111
+ function tierFor(category) {
112
+ return CATEGORY_TIERS[category] || 'actionable';
113
+ }
114
+
95
115
  function collect(result, findings, passed, defaultCategory) {
96
116
  if (result.violations.length === 0) {
97
- passed.push({ category: defaultCategory, summary: result.summary });
117
+ passed.push({ category: defaultCategory, summary: result.summary, tier: tierFor(defaultCategory) });
98
118
  return;
99
119
  }
100
120
  for (const v of result.violations) {
121
+ const category = v.category || defaultCategory;
101
122
  findings.push({
102
- category: v.category || defaultCategory,
123
+ category,
103
124
  severity: v.severity || 'warning',
125
+ tier: v.tier || tierFor(category),
104
126
  summary: v.summary,
105
127
  target: v.target || null,
106
128
  details: v.details || null,
@@ -179,38 +201,51 @@ function checkMcpDrift({ repoRoot }) {
179
201
  }
180
202
 
181
203
  const serverSource = readFileSync(serverPath, 'utf8');
182
- const dispatchedTools = new Set();
183
- for (const m of serverSource.matchAll(/name === ['"]([a-z][a-z0-9_]*)['"]/g)) {
184
- dispatchedTools.add(m[1]);
185
- }
186
204
 
187
- const exportedTools = new Set();
188
- const entries = readdirSync(toolsDir, { withFileTypes: true });
189
- for (const entry of entries) {
190
- if (!entry.isFile() || !entry.name.endsWith('.mjs')) continue;
191
- const src = readFileSync(join(toolsDir, entry.name), 'utf8');
192
- for (const m of src.matchAll(/export (?:async )?function (\w+)/g)) {
193
- exportedTools.add(camelToSnake(m[1]));
205
+ // Candidate tool handlers are exactly the identifiers server.mjs imports from
206
+ // ./tools/*.mjs. Tool modules also export private helpers (exec, readJson) that
207
+ // are never MCP tools; scanning every `export function` swept those into the
208
+ // signal. Scoping to the server's own imports is the real contract surface.
209
+
210
+ const importedHandlers = new Set();
211
+ for (const m of serverSource.matchAll(/import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]\.\/tools\/[^'"]+['"]/g)) {
212
+ for (const ident of m[1].split(',')) {
213
+ const local = ident.trim().split(/\s+as\s+/).pop().trim();
214
+ if (local) importedHandlers.add(local);
194
215
  }
195
216
  }
196
217
 
197
- for (const tool of exportedTools) {
198
- if (KNOWN_NON_DISPATCH_TOOLS.has(tool)) continue;
199
- if (!dispatchedTools.has(tool)) {
218
+ // A handler is wired when the dispatcher invokes it, regardless of the
219
+ // registered tool name. Handlers follow xxxTool→'xxx' and construct_xxx naming
220
+ // conventions, so matching exported names against `name === '<tool>'` strings is
221
+ // unreliable; matching the invoked identifier is convention-agnostic.
222
+
223
+ const invoked = new Set();
224
+ for (const m of serverSource.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
225
+ invoked.add(m[1]);
226
+ }
227
+
228
+ for (const handler of importedHandlers) {
229
+ const snake = camelToSnake(handler);
230
+ if (KNOWN_NON_DISPATCH_TOOLS.has(handler) || KNOWN_NON_DISPATCH_TOOLS.has(snake)) continue;
231
+ if (!invoked.has(handler)) {
200
232
  violations.push({
201
233
  category: 'mcp-drift',
202
234
  severity: 'warning',
203
- target: tool,
204
- summary: `MCP tool exported but not dispatched: ${tool}`,
235
+ tier: 'internal',
236
+ target: snake,
237
+ summary: `MCP tool handler imported but never dispatched: ${handler}`,
205
238
  });
206
239
  }
207
240
  }
208
241
 
209
- return { summary: `mcp: ${dispatchedTools.size} dispatched, ${exportedTools.size} exported, ${violations.length} drift`, violations };
242
+ return { summary: `mcp: ${importedHandlers.size} handlers, ${violations.length} drift`, violations };
210
243
  }
211
244
 
245
+ // Handlers the server may import for re-export or test surface but intentionally
246
+ // never dispatches. Matched by camelCase identifier or snake_case form.
247
+
212
248
  const KNOWN_NON_DISPATCH_TOOLS = new Set([
213
- // Internal helpers exported for tests but never wired through MCP.
214
249
  'workflow_status_bound',
215
250
  'create_needs_main_input_packet',
216
251
  ]);
@@ -219,22 +254,82 @@ function camelToSnake(s) {
219
254
  return s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
220
255
  }
221
256
 
257
+ function readRegistrySnapshot(repoRoot) {
258
+ const registryPath = join(repoRoot, 'specialists', 'org');
259
+ if (!existsSync(registryPath)) return null;
260
+ const stat = statSync(registryPath);
261
+ if (stat.isDirectory()) return loadRegistry({ rootDir: repoRoot });
262
+ return JSON.parse(readFileSync(registryPath, 'utf8'));
263
+ }
264
+
265
+ function personaKeysFromRegistry(registry) {
266
+ const keys = new Set();
267
+
268
+ for (const [specId, spec] of Object.entries(registry?.specialists || {})) {
269
+ if (specId) keys.add(specId.replace(/^cx-/, ''));
270
+ if (spec?.name) keys.add(String(spec.name).replace(/^cx-/, ''));
271
+ }
272
+
273
+ if (Array.isArray(registry?.specialists)) {
274
+ for (const spec of registry.specialists) {
275
+ if (spec?.id) keys.add(String(spec.id).replace(/^cx-/, ''));
276
+ if (spec?.name) keys.add(String(spec.name).replace(/^cx-/, ''));
277
+ }
278
+ }
279
+
280
+ if (registry?.orchestrator?.id) keys.add(String(registry.orchestrator.id).replace(/^cx-/, ''));
281
+ if (registry?.orchestrator?.name) keys.add(String(registry.orchestrator.name).replace(/^cx-/, ''));
282
+
283
+ return [...keys].sort();
284
+ }
285
+
286
+ function personaOwnersFromRegistry(registry) {
287
+ const owners = new Map();
288
+
289
+ function addOwner(rawId, owner) {
290
+ if (!rawId) return;
291
+ const personaId = String(rawId).replace(/^cx-/, '');
292
+ if (!owners.has(personaId)) owners.set(personaId, new Set());
293
+ owners.get(personaId).add(owner);
294
+ }
295
+
296
+ // One registry entity owns a persona once. A specialist's id and its own name
297
+ // normalize to the same persona by design (cx-architect / architect), so they
298
+ // must share a single owner token keyed on the entity — otherwise every
299
+ // specialist self-collides and reports a spurious "ambiguous" drift. Genuine
300
+ // ambiguity (two distinct specialists, or a specialist and the orchestrator,
301
+ // normalizing to the same persona) still yields owners.size > 1.
302
+
303
+ for (const [specId, spec] of Object.entries(registry?.specialists || {})) {
304
+ addOwner(specId, `specialist:${specId}`);
305
+ addOwner(spec?.name, `specialist:${specId}`);
306
+ }
307
+
308
+ if (Array.isArray(registry?.specialists)) {
309
+ for (const spec of registry.specialists) {
310
+ const owner = `specialist:${spec?.id || spec?.name || 'unknown'}`;
311
+ addOwner(spec?.id, owner);
312
+ addOwner(spec?.name, owner);
313
+ }
314
+ }
315
+
316
+ addOwner(registry?.orchestrator?.id, 'orchestrator');
317
+ addOwner(registry?.orchestrator?.name, 'orchestrator');
318
+ return owners;
319
+ }
320
+
222
321
  // ── Check 3: roles ↔ registry ──────────────────────────────────────────────
223
322
 
224
323
  function checkRolesDrift({ repoRoot }) {
225
324
  const violations = [];
226
- const manifestsPath = join(repoRoot, 'specialists', 'role-manifests.json');
227
- const registryPath = join(repoRoot, 'specialists', 'registry.json');
228
-
229
- if (!existsSync(manifestsPath) || !existsSync(registryPath)) {
230
- return { summary: 'role manifests or registry missing', violations: [] };
325
+ if (!existsSync(join(repoRoot, 'specialists', 'org'))) {
326
+ return { summary: 'registry missing', violations: [] };
231
327
  }
232
328
 
233
- const manifests = JSON.parse(readFileSync(manifestsPath, 'utf8'));
234
- const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
329
+ const registry = readRegistrySnapshot(repoRoot);
235
330
 
236
331
  const known = new Set();
237
- for (const s of registry.specialists || []) {
332
+ for (const s of Object.values(registry.specialists || {})) {
238
333
  if (s?.name) { known.add(s.name); known.add(`cx-${s.name}`); }
239
334
  }
240
335
  if (registry.orchestrator?.name) {
@@ -242,35 +337,43 @@ function checkRolesDrift({ repoRoot }) {
242
337
  known.add(`cx-${registry.orchestrator.name}`);
243
338
  }
244
339
 
245
- const personaKeys = Object.keys(manifests.personas || manifests || {});
340
+ const personaKeys = personaKeysFromRegistry(registry);
341
+ const personaOwners = personaOwnersFromRegistry(registry);
246
342
  for (const key of personaKeys) {
247
- if (key === 'version' || key === 'description' || key === 'schemaVersion') continue;
248
343
  if (!known.has(key) && !known.has(`cx-${key}`)) {
249
344
  violations.push({
250
345
  category: 'roles-drift',
251
346
  severity: 'warning',
252
347
  target: key,
253
- summary: `role-manifests persona '${key}' does not resolve to a registry persona/agent`,
348
+ summary: `role manifest '${key}' does not resolve to a registry persona/agent`,
254
349
  });
255
350
  }
256
351
  }
257
352
 
258
- return { summary: `roles: ${personaKeys.length} manifest keys, ${violations.length} drift`, violations };
353
+ for (const [personaId, owners] of personaOwners.entries()) {
354
+ if (owners.size <= 1) continue;
355
+ violations.push({
356
+ category: 'roles-drift',
357
+ severity: 'warning',
358
+ target: personaId,
359
+ summary: `role manifest '${personaId}' is ambiguous after normalization: ${[...owners].join(', ')}`,
360
+ });
361
+ }
362
+
363
+ return { summary: `roles: ${personaKeys.length} personas, ${violations.length} drift`, violations };
259
364
  }
260
365
 
261
366
  // ── Check 4: persona promptFile existence ──────────────────────────────────
262
367
 
263
368
  function checkPersonaPrompts({ repoRoot }) {
264
369
  const violations = [];
265
- const registryPath = join(repoRoot, 'specialists', 'registry.json');
266
- if (!existsSync(registryPath)) return { summary: 'registry missing', violations: [] };
267
-
268
- const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
370
+ const registry = readRegistrySnapshot(repoRoot);
371
+ if (!registry) return { summary: 'registry missing', violations: [] };
269
372
  let checked = 0;
270
373
 
271
374
  for (const list of [
272
375
  registry.orchestrator ? [registry.orchestrator] : [],
273
- registry.specialists || [],
376
+ Object.values(registry.specialists || {}),
274
377
  ]) {
275
378
  for (const entry of list) {
276
379
  const ref = entry?.promptFile;
@@ -297,7 +400,9 @@ async function checkContractsDrift({ repoRoot }) {
297
400
  const violations = [];
298
401
  try {
299
402
  const { validateContractsFile } = await import('../../contracts/validate.mjs');
300
- const result = validateContractsFile({ repoRoot });
403
+ const legacyContractsPath = join(repoRoot, 'specialists', 'contracts.json');
404
+ const opts = existsSync(legacyContractsPath) ? { contractsPath: legacyContractsPath, repoRoot } : { repoRoot };
405
+ const result = validateContractsFile(opts);
301
406
  if (!result.ok) {
302
407
  for (const err of result.errors) {
303
408
  violations.push({
@@ -327,7 +432,7 @@ function getVSCodeSettingsDir(homeDir) {
327
432
  return join(appData, 'Code', 'User');
328
433
  }
329
434
 
330
- function checkHostSurfaces({ repoRoot }) {
435
+ function checkHostSurfaces({ repoRoot: _repoRoot }) {
331
436
  const violations = [];
332
437
  const homeDir = os.homedir();
333
438
  const surfaces = [
@@ -0,0 +1,97 @@
1
+ /**
2
+ * lib/doctor/watchers/credential-parity.mjs — advisory credential alignment checks.
3
+ *
4
+ * Surfaces when a pinned OpenRouter model or OpenCode default expects
5
+ * OPENROUTER_API_KEY but the canonical store and op-run catalog both lack it,
6
+ * or when OpenCode's openrouter provider has no auth wiring.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+
13
+ import { parseEnvFile, getUserEnvPath } from '../../env-config.mjs';
14
+ import { hasSecret } from '../../providers/secret-resolver.mjs';
15
+ import { ensureOpenRouterProviderAuth } from '../../opencode-config.mjs';
16
+ import { findOpenCodeConfigPath } from '../../opencode-config.mjs';
17
+
18
+ export const name = 'credential-parity';
19
+ export const intervalMs = 30 * 60 * 1000;
20
+
21
+ function openRouterModelId(modelId) {
22
+ return typeof modelId === 'string' && /^openrouter\//.test(modelId);
23
+ }
24
+
25
+ function openCodeNeedsOpenRouter(homeDir) {
26
+ const configPath = findOpenCodeConfigPath();
27
+ if (!fs.existsSync(configPath)) return { needs: false, configPath };
28
+ try {
29
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
30
+ const defaultModel = config.model || config.defaultModel || '';
31
+ return { needs: openRouterModelId(defaultModel), configPath, config };
32
+ } catch {
33
+ return { needs: false, configPath };
34
+ }
35
+ }
36
+
37
+ function openRouterAuthWired(config) {
38
+ const probe = { provider: { openrouter: config?.provider?.openrouter || {} } };
39
+ ensureOpenRouterProviderAuth(probe);
40
+ const provider = probe.provider.openrouter;
41
+ return Boolean(
42
+ provider.apiKey
43
+ || provider.options?.headers?.Authorization,
44
+ );
45
+ }
46
+
47
+ export async function runCredentialParityChecks({ rootDir = process.cwd(), homeDir = os.homedir(), env = process.env } = {}) {
48
+ const violations = [];
49
+ const configPath = getUserEnvPath(homeDir);
50
+ const openRouterConfigured = hasSecret('OPENROUTER_API_KEY', { env, cwd: rootDir });
51
+
52
+ const openCode = openCodeNeedsOpenRouter(homeDir);
53
+ if (openCode.needs && !openRouterConfigured) {
54
+ violations.push({
55
+ category: 'credential-parity',
56
+ severity: 'advisory',
57
+ summary: `OpenCode default model uses OpenRouter but OPENROUTER_API_KEY is not configured (${openCode.configPath})`,
58
+ });
59
+ }
60
+
61
+ if (openCode.config && openCode.needs && !openRouterAuthWired(openCode.config)) {
62
+ violations.push({
63
+ category: 'credential-parity',
64
+ severity: 'advisory',
65
+ summary: 'OpenCode provider.openrouter has no apiKey env ref — run `construct sync --global`',
66
+ });
67
+ }
68
+
69
+ const xdgEnv = parseEnvFile(configPath);
70
+ const legacyEnv = parseEnvFile(path.join(homeDir, '.construct', 'config.env'));
71
+ if (legacyEnv.CONSTRUCT_OP_ENV_FILE && !xdgEnv.CONSTRUCT_OP_ENV_FILE) {
72
+ violations.push({
73
+ category: 'credential-parity',
74
+ severity: 'advisory',
75
+ summary: 'CONSTRUCT_OP_ENV_FILE is still in ~/.construct/config.env — run `construct install --scope=user` to migrate to XDG config',
76
+ });
77
+ }
78
+
79
+ return {
80
+ summary: violations.length
81
+ ? `credential-parity: ${violations.length} advisory finding(s)`
82
+ : 'credential-parity: ok',
83
+ violations,
84
+ };
85
+ }
86
+
87
+ export async function tick({ rootDir = process.cwd(), homeDir = os.homedir(), env = process.env } = {}) {
88
+ const result = await runCredentialParityChecks({ rootDir, homeDir, env });
89
+ return {
90
+ actions: [],
91
+ escalations: [],
92
+ findings: result.violations,
93
+ passed: result.violations.length === 0
94
+ ? [{ category: 'credential-parity', summary: result.summary }]
95
+ : [],
96
+ };
97
+ }
@@ -6,7 +6,7 @@
6
6
  * `initialize`, parse the response, send `tools/list`, report tool count.
7
7
  * cass-memory v0.2.x (the original cause of construct-1qt) rejects the
8
8
  * handshake outright; this watcher flags servers that fail the handshake so
9
- * the dashboard surfaces a protocol problem rather than a port-up false
9
+ * doctor surfaces a protocol problem rather than a port-up false
10
10
  * positive.
11
11
  *
12
12
  * Severity defaults to warning. Set `CONSTRUCT_MCP_PROTOCOL_BLOCKING=1` to
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * lib/doctor/watchers/service-health.mjs — continuous service probes.
3
3
  *
4
- * Probes the services `construct dev` starts (postgres, dashboard,
5
- * cm, opencode). On 2 consecutive probe failures, attempts a restart of
4
+ * Probes the services `construct dev` starts (postgres, cm).
5
+ * On 2 consecutive probe failures, attempts a restart of
6
6
  * docker-backed services (postgres). On 2 consecutive failed
7
7
  * restarts, escalates `service.down` for cx-sre. Non-docker services log +
8
8
  * escalate after 3 consecutive failures — auto-restart needs original spawn
@@ -13,12 +13,11 @@
13
13
 
14
14
  import { spawnSync, spawn } from 'node:child_process';
15
15
  import { existsSync, readFileSync, openSync, mkdirSync } from 'node:fs';
16
- import { dirname, join } from 'node:path';
17
- import { fileURLToPath } from 'node:url';
16
+ import { join } from 'node:path';
18
17
 
19
18
  import { record } from '../audit.mjs';
20
19
  import { escalate } from '../escalate.mjs';
21
- import { configDir, stateDir, cacheDir } from '../../config/xdg.mjs';
20
+ import { configDir, cacheDir } from '../../config/xdg.mjs';
22
21
 
23
22
  export const name = 'service-health';
24
23
  export const intervalMs = 60 * 1000;
@@ -26,9 +25,6 @@ export const intervalMs = 60 * 1000;
26
25
  const failureCounts = {};
27
26
  const restartAttempts = {};
28
27
 
29
- const __dirname = dirname(fileURLToPath(import.meta.url));
30
- const REPO_ROOT = join(__dirname, '..', '..', '..');
31
-
32
28
  const services = [
33
29
  {
34
30
  id: 'postgres',
@@ -36,12 +32,6 @@ const services = [
36
32
  restart: () => dockerRestart('construct-postgres'),
37
33
  restartable: true,
38
34
  },
39
- {
40
- id: 'dashboard',
41
- probe: () => probeDashboardPort(),
42
- restart: () => respawnViaConstructUp('dashboard'),
43
- restartable: true,
44
- },
45
35
  {
46
36
  id: 'cm',
47
37
  probe: () => probeMemoryHealth(),
@@ -55,18 +45,6 @@ function probeTcp(host, port, timeoutMs = 1500) {
55
45
  return r.status === 0;
56
46
  }
57
47
 
58
- function probeDashboardPort() {
59
- const statePath = join(stateDir(), 'dashboard.json');
60
- if (!existsSync(statePath)) return true;
61
- try {
62
- const state = JSON.parse(readFileSync(statePath, 'utf8'));
63
- if (!state.port) return true;
64
- return probeTcp('127.0.0.1', state.port);
65
- } catch {
66
- return true;
67
- }
68
- }
69
-
70
48
  function probeMemoryHealth() {
71
49
  const envPath = join(configDir(), 'config.env');
72
50
  if (!existsSync(envPath)) return true;
@@ -99,12 +77,6 @@ function detachSpawn(command, args, logName) {
99
77
  }
100
78
  }
101
79
 
102
- function respawnViaConstructUp() {
103
- const bin = join(REPO_ROOT, 'bin', 'construct');
104
- if (!existsSync(bin)) return { ok: false, error: 'bin/construct not found' };
105
- return detachSpawn('node', [bin, 'up'], 'doctor-respawn-up.log');
106
- }
107
-
108
80
  function respawnCm() {
109
81
  const envPath = join(configDir(), 'config.env');
110
82
  let port = '4231';
@@ -105,6 +105,50 @@ export function pdfRenderedDiagrams(pdfPath, sourceContent) {
105
105
  }
106
106
  }
107
107
 
108
+ function rawDiagramSourcePattern() {
109
+ return /(?:flowchart\s+(?:TD|LR|BT|RL)|sequenceDiagram|classDiagram|stateDiagram|erDiagram|journey|gantt|pie\s+title|direction:\s*(?:right|down|left|up))/i;
110
+ }
111
+
112
+ function zipEntryText(zipPath, entryName, env = process.env) {
113
+ if (!whichBin('unzip', env)) return null;
114
+ const result = spawnSync('unzip', ['-p', zipPath, entryName], { encoding: 'utf8', env });
115
+ return result.status === 0 ? String(result.stdout || '') : null;
116
+ }
117
+
118
+ function zipEntryNames(zipPath, env = process.env) {
119
+ if (!whichBin('unzip', env)) return [];
120
+ const result = spawnSync('unzip', ['-Z1', zipPath], { encoding: 'utf8', env });
121
+ return result.status === 0
122
+ ? String(result.stdout || '').split('\n').map((line) => line.trim()).filter(Boolean)
123
+ : [];
124
+ }
125
+
126
+ export function docxRenderedDiagrams(docxPath, sourceContent, env = process.env) {
127
+ const expected = countDiagramFences(sourceContent);
128
+ if (expected === 0) return true;
129
+ const documentXml = zipEntryText(docxPath, 'word/document.xml', env);
130
+ if (!documentXml) return false;
131
+ if (rawDiagramSourcePattern().test(documentXml)) return false;
132
+ const rels = zipEntryText(docxPath, 'word/_rels/document.xml.rels', env) || '';
133
+ const entries = zipEntryNames(docxPath, env);
134
+ const mediaCount = entries.filter((entry) => entry.startsWith('word/media/')).length;
135
+ return mediaCount >= expected || /relationships\/image/i.test(rels);
136
+ }
137
+
138
+ export function htmlRenderedDiagrams(htmlPath, sourceContent) {
139
+ const expected = countDiagramFences(sourceContent);
140
+ if (expected === 0) return true;
141
+ let html = '';
142
+ try {
143
+ html = fs.readFileSync(htmlPath, 'utf8');
144
+ } catch {
145
+ return false;
146
+ }
147
+ if (rawDiagramSourcePattern().test(html)) return false;
148
+ const renderedCount = (html.match(/<(?:img|svg)\b/gi) || []).length;
149
+ return renderedCount >= expected || /data:image\//i.test(html);
150
+ }
151
+
108
152
  export { pdfUsesBundledBrandSans } from './brand-fonts.mjs';
109
153
 
110
154
  /** @deprecated use pdfUsesBundledBrandSans */
@@ -441,17 +485,37 @@ export function exportMarkdown({
441
485
  };
442
486
  }
443
487
 
444
- if (format === 'pdf' && figures) {
488
+ if (figures) {
445
489
  const source = fs.readFileSync(inputPath, 'utf8');
446
490
  const expectedDiagrams = countDiagramFences(source);
447
- if (expectedDiagrams > 0 && !pdfRenderedDiagrams(target, source)) {
448
- return {
449
- ok: false,
450
- format,
451
- inputPath,
452
- outputPath: target,
453
- message: `Export wrote PDF but rendered 0/${expectedDiagrams} diagram(s). Ensure d2 and mmdc are installed; mmdc needs Chrome (set PUPPETEER_EXECUTABLE_PATH or install Google Chrome). Pandoc stderr: ${(result.stderr || '').trim().slice(0, 300)}`,
454
- };
491
+ if (expectedDiagrams > 0) {
492
+ if (format === 'pdf' && !pdfRenderedDiagrams(target, source)) {
493
+ return {
494
+ ok: false,
495
+ format,
496
+ inputPath,
497
+ outputPath: target,
498
+ message: `Export wrote PDF but rendered 0/${expectedDiagrams} diagram(s). Ensure d2 and mmdc are installed; mmdc needs Chrome (set PUPPETEER_EXECUTABLE_PATH or install Google Chrome). Pandoc stderr: ${(result.stderr || '').trim().slice(0, 300)}`,
499
+ };
500
+ }
501
+ if (format === 'docx' && !docxRenderedDiagrams(target, source, effectiveEnv)) {
502
+ return {
503
+ ok: false,
504
+ format,
505
+ inputPath,
506
+ outputPath: target,
507
+ message: `Export wrote DOCX but left diagram source unresolved. Ensure d2 and mermaid-cli rendered the figures before distributing the document. Pandoc stderr: ${(result.stderr || '').trim().slice(0, 300)}`,
508
+ };
509
+ }
510
+ if ((format === 'html' || format === 'deck') && !htmlRenderedDiagrams(target, source)) {
511
+ return {
512
+ ok: false,
513
+ format,
514
+ inputPath,
515
+ outputPath: target,
516
+ message: `Export wrote ${format.toUpperCase()} but left diagram source unresolved. Ensure d2 and mermaid-cli rendered the figures before distributing the document. Pandoc stderr: ${(result.stderr || '').trim().slice(0, 300)}`,
517
+ };
518
+ }
455
519
  }
456
520
  }
457
521