@geraldmaron/construct 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (504) 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/source-checkout.mjs +3 -4
  44. package/lib/doctor/watchers/consistency.mjs +90 -22
  45. package/lib/doctor/watchers/credential-parity.mjs +97 -0
  46. package/lib/doctor/watchers/mcp-protocol.mjs +1 -1
  47. package/lib/doctor/watchers/service-health.mjs +4 -32
  48. package/lib/document-export.mjs +73 -9
  49. package/lib/document-extract.mjs +10 -198
  50. package/lib/embed/artifact.mjs +2 -3
  51. package/lib/embed/cli.mjs +1 -1
  52. package/lib/embed/config.mjs +1 -2
  53. package/lib/embed/customer-profiles.mjs +1 -1
  54. package/lib/embed/daemon.mjs +5 -6
  55. package/lib/embed/demand-fetch.mjs +114 -2
  56. package/lib/embed/docs-lifecycle.mjs +9 -5
  57. package/lib/embed/intake-metrics.mjs +2 -2
  58. package/lib/embed/notifications.mjs +2 -3
  59. package/lib/embed/recommendation-store.mjs +25 -22
  60. package/lib/embed/role-framing.mjs +4 -9
  61. package/lib/embedded-contract/capability.mjs +4 -2
  62. package/lib/embedded-contract/contract-version.mjs +1 -1
  63. package/lib/env-config.mjs +2 -2
  64. package/lib/flavors/loader.mjs +21 -19
  65. package/lib/graph/build-from-registry.mjs +31 -7
  66. package/lib/graph/staleness.mjs +1 -1
  67. package/lib/headhunt.mjs +82 -35
  68. package/lib/hooks/agent-tracker.mjs +2 -2
  69. package/lib/hooks/config-protection.mjs +1 -1
  70. package/lib/hooks/registry-sync.mjs +3 -3
  71. package/lib/host-capabilities.mjs +5 -5
  72. package/lib/improvement/controller.mjs +2 -2
  73. package/lib/init-unified.mjs +19 -21
  74. package/lib/intake/classify.mjs +26 -0
  75. package/lib/intake/daemon.mjs +2 -3
  76. package/lib/intake/prepare.mjs +10 -3
  77. package/lib/intake/session-prelude.mjs +1 -1
  78. package/lib/intake/tables/creative.mjs +9 -9
  79. package/lib/intake/tables/operations.mjs +4 -4
  80. package/lib/intake/tables/research.mjs +2 -2
  81. package/lib/integrations/intake-integrations.mjs +9 -10
  82. package/lib/knowledge/research-store.mjs +2 -2
  83. package/lib/mcp/server.mjs +104 -61
  84. package/lib/mcp/tool-budget.mjs +1 -46
  85. package/lib/mcp/tool-recovery.mjs +44 -0
  86. package/lib/mcp/tools/artifact-author.mjs +36 -0
  87. package/lib/mcp/tools/find-tool.mjs +86 -0
  88. package/lib/mcp/tools/orchestration-run.mjs +107 -69
  89. package/lib/mcp/tools/project.mjs +4 -3
  90. package/lib/mcp/tools/{profile.mjs → scope.mjs} +54 -52
  91. package/lib/mcp/tools/skills.mjs +39 -22
  92. package/lib/mcp/tools/telemetry.mjs +2 -1
  93. package/lib/mcp/tools/workflow.mjs +1 -1
  94. package/lib/mcp-platform-config.mjs +7 -5
  95. package/lib/migrations/index.mjs +4 -2
  96. package/lib/migrations/v2-unified-registry.mjs +35 -0
  97. package/lib/model-router.mjs +203 -36
  98. package/lib/models/catalog.mjs +25 -9
  99. package/lib/models/execution-capability-profile.mjs +3 -3
  100. package/lib/models/execution-policy.mjs +7 -6
  101. package/lib/models/provider-poll.mjs +18 -4
  102. package/lib/opencode-config.mjs +56 -1
  103. package/lib/opencode-runtime-plugin.mjs +13 -10
  104. package/lib/oracle/artifact-gate.mjs +14 -4
  105. package/lib/oracle/cli.mjs +2 -0
  106. package/lib/oracle/dispatch.mjs +18 -1
  107. package/lib/oracle/execute.mjs +39 -7
  108. package/lib/oracle/index.mjs +1 -1
  109. package/lib/oracle/org-graph.mjs +10 -5
  110. package/lib/oracle/policy.mjs +6 -0
  111. package/lib/oracle/read-model.mjs +103 -6
  112. package/lib/oracle/reconcile.mjs +31 -4
  113. package/lib/oracle/remediation-dispatch.mjs +53 -0
  114. package/lib/oracle/routing.mjs +5 -0
  115. package/lib/oracle/synthesize.mjs +80 -3
  116. package/lib/orchestration/routing-tables.mjs +44 -10
  117. package/lib/orchestration/runtime.mjs +2 -0
  118. package/lib/orchestration/worker.mjs +1 -1
  119. package/lib/orchestration-policy.mjs +351 -62
  120. package/lib/overrides/resolver.mjs +1 -12
  121. package/lib/parity.mjs +37 -11
  122. package/lib/policy/engine.mjs +80 -15
  123. package/lib/prompt-composer.js +51 -15
  124. package/lib/prompt-metadata.mjs +3 -2
  125. package/lib/providers/connection-probe.mjs +58 -0
  126. package/lib/providers/copilot-auth.mjs +1 -1
  127. package/lib/providers/credential-bootstrap.mjs +1 -1
  128. package/lib/providers/op-run.mjs +3 -4
  129. package/lib/providers/secret-resolver.mjs +31 -0
  130. package/lib/publish-tooling.mjs +3 -11
  131. package/lib/publish.mjs +12 -29
  132. package/lib/reconcile/mcp-entry-reconcile.mjs +14 -9
  133. package/lib/reflect.mjs +2 -2
  134. package/lib/registry/agent-manifest.mjs +190 -0
  135. package/lib/registry/assemble.mjs +133 -0
  136. package/lib/registry/catalog.mjs +171 -0
  137. package/lib/registry/cli.mjs +590 -6
  138. package/lib/registry/consolidation.mjs +5 -4
  139. package/lib/registry/docs-sync.mjs +67 -0
  140. package/lib/registry/loader.mjs +147 -0
  141. package/lib/registry/org-io.mjs +76 -0
  142. package/lib/registry/retired-paths.mjs +71 -0
  143. package/lib/registry/surface-map.mjs +5 -7
  144. package/lib/registry/validator.mjs +438 -0
  145. package/lib/research-execution-policy.mjs +173 -0
  146. package/lib/roles/catalog.mjs +4 -9
  147. package/lib/roles/fence.mjs +107 -1
  148. package/lib/roles/flavor-bindings.mjs +68 -0
  149. package/lib/roles/gateway.mjs +140 -29
  150. package/lib/roles/manifest.mjs +22 -13
  151. package/lib/roles/router.mjs +1 -1
  152. package/lib/scopes/enrich.mjs +67 -0
  153. package/lib/scopes/hooks.mjs +12 -0
  154. package/lib/{profiles → scopes}/lifecycle.mjs +70 -70
  155. package/lib/scopes/loader.mjs +104 -0
  156. package/lib/scopes/rebrand.mjs +32 -0
  157. package/lib/scopes/research-profile.mjs +16 -0
  158. package/lib/scopes/teams.mjs +98 -0
  159. package/lib/service-manager.mjs +1 -117
  160. package/lib/setup-prompts.mjs +1 -1
  161. package/lib/setup.mjs +6 -26
  162. package/lib/skills/composition-graph.mjs +98 -0
  163. package/lib/skills/router.mjs +80 -0
  164. package/lib/specialist-contracts.mjs +17 -18
  165. package/lib/specialists/postconditions.mjs +2 -2
  166. package/lib/specialists/prompt-schema.mjs +6 -5
  167. package/lib/specialists/roster.mjs +7 -12
  168. package/lib/specialists/schema.mjs +9 -6
  169. package/lib/status.mjs +25 -90
  170. package/lib/storage/file-lock.mjs +6 -3
  171. package/lib/telemetry/skill-calls.mjs +1 -1
  172. package/lib/templates/doc-presentation.mjs +63 -0
  173. package/lib/templates/visual-requirements.mjs +35 -2
  174. package/lib/term-format.mjs +107 -2
  175. package/lib/test-env-setup.mjs +21 -0
  176. package/lib/ui/components.mjs +42 -0
  177. package/lib/ui/glyphs.mjs +58 -0
  178. package/lib/ui/links.mjs +115 -0
  179. package/lib/ui/theme.mjs +108 -0
  180. package/lib/uninstall/uninstall.mjs +2 -1
  181. package/lib/validator.mjs +45 -8
  182. package/lib/validators/skill-effectiveness.mjs +174 -0
  183. package/lib/validators/skills.mjs +1 -1
  184. package/package.json +12 -8
  185. package/personas/construct.md +12 -3
  186. package/platforms/claude/CLAUDE.md +1 -1
  187. package/rules/common/beads-hygiene.md +52 -0
  188. package/rules/common/neurodivergent-output.md +4 -7
  189. package/rules/common/research.md +2 -0
  190. package/scripts/sync-specialists.mjs +260 -49
  191. package/skills/ai/prompt-optimizer.md +3 -3
  192. package/skills/brand/output-vibe.md +48 -0
  193. package/skills/docs/adr-workflow.md +3 -0
  194. package/skills/docs/backlog-proposal-workflow.md +3 -0
  195. package/skills/docs/codebase-research-workflow.md +5 -2
  196. package/skills/docs/customer-profile-workflow.md +3 -0
  197. package/skills/docs/document-ingest-workflow.md +3 -0
  198. package/skills/docs/evidence-ingest-workflow.md +4 -1
  199. package/skills/docs/init-project.md +1 -1
  200. package/skills/docs/prd-workflow.md +1 -1
  201. package/skills/docs/prfaq-workflow.md +3 -0
  202. package/skills/docs/product-intelligence-workflow.md +4 -1
  203. package/skills/docs/product-signal-workflow.md +3 -0
  204. package/skills/docs/research-workflow.md +13 -6
  205. package/skills/docs/runbook-workflow.md +3 -0
  206. package/skills/docs/strategy-workflow.md +3 -0
  207. package/skills/docs/user-research-workflow.md +6 -3
  208. package/skills/operating/orchestration-reference.md +1 -1
  209. package/skills/roles/{engineer.ai.md → ai-engineer.md} +2 -2
  210. package/skills/roles/architect.ai-systems.md +1 -1
  211. package/skills/roles/architect.data.md +1 -1
  212. package/skills/roles/architect.enterprise.md +1 -1
  213. package/skills/roles/architect.integration.md +1 -1
  214. package/skills/roles/architect.md +1 -1
  215. package/skills/roles/architect.platform.md +1 -1
  216. package/skills/roles/{product-manager.business-strategy.md → business-strategist.md} +3 -3
  217. package/skills/roles/data-analyst.experiment.md +1 -1
  218. package/skills/roles/data-analyst.md +1 -1
  219. package/skills/roles/data-analyst.product-intelligence.md +1 -1
  220. package/skills/roles/data-analyst.product.md +1 -1
  221. package/skills/roles/data-analyst.telemetry.md +1 -1
  222. package/skills/roles/{engineer.data.md → data-engineer.md} +2 -2
  223. package/skills/roles/data-engineer.pipeline.md +2 -2
  224. package/skills/roles/data-engineer.vector-retrieval.md +2 -2
  225. package/skills/roles/data-engineer.warehouse.md +2 -2
  226. package/skills/roles/debugger.md +1 -1
  227. package/skills/roles/designer.accessibility.md +1 -1
  228. package/skills/roles/designer.md +1 -1
  229. package/skills/roles/{reviewer.devil-advocate.md → devil-advocate.md} +2 -2
  230. package/skills/roles/{operator.docs.md → docs-keeper.md} +3 -3
  231. package/skills/roles/engineer.md +3 -7
  232. package/skills/roles/{reviewer.evaluator.md → evaluator.md} +2 -2
  233. package/skills/roles/{researcher.explorer.md → explorer.md} +2 -2
  234. package/skills/roles/{operator.md → operations.md} +2 -5
  235. package/skills/roles/orchestrator.md +1 -1
  236. package/skills/roles/{engineer.platform.md → platform-engineer.md} +2 -2
  237. package/skills/roles/product-manager.ai-product.md +1 -1
  238. package/skills/roles/product-manager.enterprise.md +1 -1
  239. package/skills/roles/product-manager.growth.md +1 -2
  240. package/skills/roles/product-manager.md +1 -2
  241. package/skills/roles/product-manager.platform.md +1 -1
  242. package/skills/roles/product-manager.product.md +1 -1
  243. package/skills/roles/qa.ai-eval.md +1 -1
  244. package/skills/roles/qa.api-contract.md +1 -1
  245. package/skills/roles/qa.data-pipeline.md +1 -1
  246. package/skills/roles/qa.md +1 -1
  247. package/skills/roles/qa.web-ui.md +1 -1
  248. package/skills/roles/{operator.release.md → release-manager.md} +3 -3
  249. package/skills/roles/researcher.md +1 -1
  250. package/skills/roles/reviewer.md +1 -1
  251. package/skills/roles/security.ai.md +1 -1
  252. package/skills/roles/security.appsec.md +1 -1
  253. package/skills/roles/security.cloud.md +1 -1
  254. package/skills/roles/security.legal-compliance.md +1 -1
  255. package/skills/roles/security.md +1 -1
  256. package/skills/roles/security.privacy.md +1 -1
  257. package/skills/roles/security.supply-chain.md +1 -1
  258. package/skills/roles/{operator.sre.md → sre.md} +3 -3
  259. package/skills/roles/{qa.test-automation.md → test-automation.md} +2 -2
  260. package/skills/roles/{reviewer.trace.md → trace-reviewer.md} +2 -2
  261. package/skills/roles/{researcher.ux.md → ux-researcher.md} +2 -2
  262. package/skills/routing.json +18 -0
  263. package/skills/routing.md +1 -1
  264. package/specialists/artifact-manifest.json +2 -1
  265. package/specialists/audit-enrichments.json +14 -14
  266. package/specialists/org/contracts/accessibility-to-qa.json +37 -0
  267. package/specialists/org/contracts/any-to-business-strategist.json +57 -0
  268. package/specialists/org/contracts/any-to-debugger.json +38 -0
  269. package/specialists/org/contracts/any-to-designer.json +33 -0
  270. package/specialists/org/contracts/any-to-docs-keeper.json +34 -0
  271. package/specialists/org/contracts/any-to-explorer.json +39 -0
  272. package/specialists/org/contracts/any-to-sre-incident.json +33 -0
  273. package/specialists/org/contracts/any-to-trace-reviewer.json +32 -0
  274. package/specialists/org/contracts/architect-to-ai-engineer.json +32 -0
  275. package/specialists/org/contracts/architect-to-data-engineer.json +52 -0
  276. package/specialists/org/contracts/architect-to-devil-advocate.json +38 -0
  277. package/specialists/org/contracts/architect-to-engineer.json +34 -0
  278. package/specialists/org/contracts/architect-to-evaluator.json +59 -0
  279. package/specialists/org/contracts/architect-to-legal-compliance.json +55 -0
  280. package/specialists/org/contracts/architect-to-operations.json +45 -0
  281. package/specialists/org/contracts/architect-to-platform-engineer.json +42 -0
  282. package/specialists/org/contracts/business-strategist-to-product-manager.json +39 -0
  283. package/specialists/org/contracts/construct-to-orchestrator.json +36 -0
  284. package/specialists/org/contracts/construct-to-rd-lead.json +53 -0
  285. package/specialists/org/contracts/data-analyst-to-product-manager.json +43 -0
  286. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +36 -0
  287. package/specialists/org/contracts/designer-to-accessibility.json +36 -0
  288. package/specialists/org/contracts/engineer-to-qa.json +34 -0
  289. package/specialists/org/contracts/engineer-to-reviewer.json +52 -0
  290. package/specialists/org/contracts/explorer-to-engineer.json +38 -0
  291. package/specialists/org/contracts/legal-compliance-to-release-manager.json +43 -0
  292. package/specialists/org/contracts/platform-engineer-to-engineer.json +31 -0
  293. package/specialists/org/contracts/product-manager-to-architect.json +71 -0
  294. package/specialists/org/contracts/product-manager-to-data-analyst.json +49 -0
  295. package/specialists/org/contracts/product-manager-to-ux-researcher.json +51 -0
  296. package/specialists/org/contracts/qa-to-release-manager.json +42 -0
  297. package/specialists/org/contracts/qa-to-test-automation.json +42 -0
  298. package/specialists/org/contracts/rd-lead-to-architect.json +38 -0
  299. package/specialists/org/contracts/researcher-to-architect.json +40 -0
  300. package/specialists/org/contracts/researcher-to-product-manager.json +57 -0
  301. package/specialists/org/contracts/reviewer-to-security.json +41 -0
  302. package/specialists/org/contracts/sre-to-release-manager.json +36 -0
  303. package/specialists/org/contracts/test-automation-to-engineer.json +34 -0
  304. package/specialists/org/contracts/trace-reviewer-to-sre.json +41 -0
  305. package/specialists/org/contracts/user-to-construct.json +30 -0
  306. package/specialists/org/groups/engineering-group.json +43 -0
  307. package/specialists/org/groups/governance-group.json +38 -0
  308. package/specialists/org/groups/operations-group.json +41 -0
  309. package/specialists/org/groups/product-group.json +46 -0
  310. package/specialists/org/groups/quality-group.json +42 -0
  311. package/specialists/org/groups/strategy-group.json +41 -0
  312. package/specialists/org/policies/action-approval.json +13 -0
  313. package/specialists/org/policies/agents-routing.json +13 -0
  314. package/specialists/org/policies/architecture.json +20 -0
  315. package/specialists/org/policies/bash-safety.json +13 -0
  316. package/specialists/org/policies/beads-hygiene.json +13 -0
  317. package/specialists/org/policies/bootstrap-state.json +13 -0
  318. package/specialists/org/policies/code-review.json +13 -0
  319. package/specialists/org/policies/coding-style.json +13 -0
  320. package/specialists/org/policies/comments.json +13 -0
  321. package/specialists/org/policies/commit-approval.json +13 -0
  322. package/specialists/org/policies/contract-preconditions.json +13 -0
  323. package/specialists/org/policies/deployment.json +20 -0
  324. package/specialists/org/policies/description.json +14 -0
  325. package/specialists/org/policies/design-approval.json +19 -0
  326. package/specialists/org/policies/doc-ownership.json +13 -0
  327. package/specialists/org/policies/file-path-fence.json +13 -0
  328. package/specialists/org/policies/framing.json +13 -0
  329. package/specialists/org/policies/git-workflow.json +13 -0
  330. package/specialists/org/policies/incident-response.json +15 -0
  331. package/specialists/org/policies/intake-triage.json +15 -0
  332. package/specialists/org/policies/neurodivergent-output.json +13 -0
  333. package/specialists/org/policies/no-fabrication.json +13 -0
  334. package/specialists/org/policies/patterns.json +13 -0
  335. package/specialists/org/policies/quality-gate-approval.json +17 -0
  336. package/specialists/org/policies/release-gates.json +13 -0
  337. package/specialists/org/policies/research-evidence.json +13 -0
  338. package/specialists/org/policies/review-before-change.json +13 -0
  339. package/specialists/org/policies/rollback.json +15 -0
  340. package/specialists/org/policies/scope-change.json +20 -0
  341. package/specialists/org/policies/secret-scan.json +13 -0
  342. package/specialists/org/policies/security-approval.json +17 -0
  343. package/specialists/org/policies/security.json +13 -0
  344. package/specialists/org/policies/session-efficiency.json +13 -0
  345. package/specialists/org/policies/skill-routing.json +13 -0
  346. package/specialists/org/policies/strategic-prioritization.json +17 -0
  347. package/specialists/org/policies/testing.json +13 -0
  348. package/specialists/org/policies/tool-invisibility.json +14 -0
  349. package/specialists/org/scopes/creative.json +54 -0
  350. package/specialists/org/scopes/operations.json +51 -0
  351. package/specialists/org/scopes/research.json +60 -0
  352. package/specialists/org/scopes/rnd.json +81 -0
  353. package/specialists/org/specialists/cx-accessibility.json +69 -0
  354. package/specialists/org/specialists/cx-ai-engineer.json +75 -0
  355. package/specialists/org/specialists/cx-architect.json +89 -0
  356. package/specialists/org/specialists/cx-business-strategist.json +72 -0
  357. package/specialists/org/specialists/cx-data-analyst.json +68 -0
  358. package/specialists/org/specialists/cx-data-engineer.json +71 -0
  359. package/specialists/org/specialists/cx-debugger.json +75 -0
  360. package/specialists/org/specialists/cx-designer.json +85 -0
  361. package/specialists/org/specialists/cx-devil-advocate.json +71 -0
  362. package/specialists/org/specialists/cx-docs-keeper.json +82 -0
  363. package/specialists/org/specialists/cx-engineer.json +106 -0
  364. package/specialists/org/specialists/cx-evaluator.json +69 -0
  365. package/specialists/org/specialists/cx-explorer.json +69 -0
  366. package/specialists/org/specialists/cx-legal-compliance.json +74 -0
  367. package/specialists/org/specialists/cx-operations.json +72 -0
  368. package/specialists/org/specialists/cx-oracle.json +46 -0
  369. package/specialists/org/specialists/cx-orchestrator.json +81 -0
  370. package/specialists/org/specialists/cx-platform-engineer.json +80 -0
  371. package/specialists/org/specialists/cx-product-manager.json +102 -0
  372. package/specialists/org/specialists/cx-qa.json +79 -0
  373. package/specialists/org/specialists/cx-rd-lead.json +73 -0
  374. package/specialists/org/specialists/cx-release-manager.json +77 -0
  375. package/specialists/org/specialists/cx-researcher.json +82 -0
  376. package/specialists/org/specialists/cx-reviewer.json +71 -0
  377. package/specialists/org/specialists/cx-security.json +91 -0
  378. package/specialists/org/specialists/cx-sre.json +94 -0
  379. package/specialists/org/specialists/cx-test-automation.json +69 -0
  380. package/specialists/org/specialists/cx-trace-reviewer.json +69 -0
  381. package/specialists/org/specialists/cx-ux-researcher.json +68 -0
  382. package/specialists/org/teams/accessibility-team.json +39 -0
  383. package/specialists/org/teams/design-team.json +40 -0
  384. package/specialists/org/teams/engineering-team.json +50 -0
  385. package/specialists/org/teams/governance-team.json +41 -0
  386. package/specialists/org/teams/operations-team.json +46 -0
  387. package/specialists/org/teams/product-management-team.json +45 -0
  388. package/specialists/org/teams/quality-team.json +49 -0
  389. package/specialists/org/teams/research-team.json +39 -0
  390. package/specialists/org/teams/strategy-team.json +48 -0
  391. package/specialists/org/teams/ux-research-team.json +39 -0
  392. package/specialists/prompts/_shared/validation-contract.md +1 -1
  393. package/specialists/prompts/_team-template.md +10 -0
  394. package/specialists/prompts/cx-accessibility.md +1 -0
  395. package/specialists/prompts/cx-ai-engineer.md +1 -1
  396. package/specialists/prompts/cx-business-strategist.md +1 -1
  397. package/specialists/prompts/cx-data-engineer.md +1 -1
  398. package/specialists/prompts/cx-designer.md +1 -0
  399. package/specialists/prompts/cx-devil-advocate.md +1 -1
  400. package/specialists/prompts/cx-docs-keeper.md +1 -1
  401. package/specialists/prompts/cx-evaluator.md +1 -1
  402. package/specialists/prompts/cx-explorer.md +1 -1
  403. package/specialists/prompts/cx-operations.md +1 -1
  404. package/specialists/prompts/cx-oracle.md +7 -3
  405. package/specialists/prompts/cx-orchestrator.md +17 -1
  406. package/specialists/prompts/cx-platform-engineer.md +1 -1
  407. package/specialists/prompts/cx-product-manager.md +2 -0
  408. package/specialists/prompts/cx-release-manager.md +1 -1
  409. package/specialists/prompts/cx-researcher.md +7 -4
  410. package/specialists/prompts/cx-sre.md +1 -1
  411. package/specialists/prompts/cx-test-automation.md +2 -2
  412. package/specialists/prompts/cx-trace-reviewer.md +3 -3
  413. package/specialists/prompts/cx-ux-researcher.md +2 -1
  414. package/templates/demos/scripts/architecture-review-adr.json +30 -0
  415. package/templates/demos/scripts/capability-contract.json +25 -0
  416. package/templates/demos/scripts/intake-triage.json +25 -0
  417. package/templates/demos/scripts/profile-doctor-health.json +25 -0
  418. package/templates/demos/tapes/agentic-platforms-prd.tape +2 -2
  419. package/templates/demos/tapes/architecture-review-adr.tape +44 -0
  420. package/templates/demos/tapes/capability-contract.tape +39 -0
  421. package/templates/demos/tapes/intake-triage.tape +39 -0
  422. package/templates/demos/tapes/profile-doctor-health.tape +39 -0
  423. package/templates/distribution/construct-brand.typ +23 -18
  424. package/templates/distribution/construct-decision.typ +1 -1
  425. package/templates/distribution/construct-pdf.typ +1 -1
  426. package/templates/distribution/construct-prd.typ +1 -1
  427. package/templates/distribution/construct-research.typ +1 -1
  428. package/templates/distribution/mermaid-puppeteer.json +8 -0
  429. package/templates/docs/persona-artifact.md +1 -1
  430. package/templates/docs/prd.md +6 -0
  431. package/apps/chat/engine/ai-sdk-agent.mjs +0 -183
  432. package/apps/chat/engine/loop-driver.mjs +0 -211
  433. package/apps/chat/engine/models.mjs +0 -122
  434. package/apps/chat/engine/provider-adapters.mjs +0 -171
  435. package/apps/chat/engine/tools/permission.mjs +0 -54
  436. package/apps/chat/engine/tools/primitives.mjs +0 -180
  437. package/apps/chat/engine/tools/registry.mjs +0 -122
  438. package/apps/chat/engine/turn-controls.mjs +0 -70
  439. package/lib/boundary.mjs +0 -127
  440. package/lib/certification/dashboard-api.mjs +0 -71
  441. package/lib/chat/cli.mjs +0 -333
  442. package/lib/chat/command-suggest.mjs +0 -161
  443. package/lib/chat/commands.mjs +0 -215
  444. package/lib/chat/config.mjs +0 -142
  445. package/lib/chat/context-compactor.mjs +0 -250
  446. package/lib/chat/context-continuation.mjs +0 -253
  447. package/lib/chat/continuation-source.mjs +0 -58
  448. package/lib/chat/demo-guide.mjs +0 -61
  449. package/lib/chat/design-tokens.mjs +0 -91
  450. package/lib/chat/desktop-binary.mjs +0 -81
  451. package/lib/chat/desktop-build.mjs +0 -130
  452. package/lib/chat/desktop-launcher.mjs +0 -133
  453. package/lib/chat/evidence.mjs +0 -145
  454. package/lib/chat/export.mjs +0 -74
  455. package/lib/chat/harness/driver.mjs +0 -91
  456. package/lib/chat/list-picker.mjs +0 -112
  457. package/lib/chat/model-picker.mjs +0 -356
  458. package/lib/chat/openrouter-fallback.mjs +0 -151
  459. package/lib/chat/permission-prompt.mjs +0 -33
  460. package/lib/chat/picker-catalog.mjs +0 -45
  461. package/lib/chat/policy-telemetry.mjs +0 -34
  462. package/lib/chat/present.mjs +0 -246
  463. package/lib/chat/session-context.mjs +0 -39
  464. package/lib/chat/session-persist.mjs +0 -73
  465. package/lib/chat/session-restore.mjs +0 -71
  466. package/lib/chat/session-settings.mjs +0 -53
  467. package/lib/chat/system-prompt.mjs +0 -52
  468. package/lib/chat/transparency.mjs +0 -93
  469. package/lib/chat/tui/color-scheme.mjs +0 -42
  470. package/lib/chat/tui/markdown.mjs +0 -123
  471. package/lib/chat/tui/presentation.mjs +0 -100
  472. package/lib/chat/tui/render.mjs +0 -500
  473. package/lib/chat/tui/turn-block.mjs +0 -284
  474. package/lib/chat/tui/turn-present.mjs +0 -18
  475. package/lib/chat/tui/turn-state.mjs +0 -88
  476. package/lib/chat/tui/usage.mjs +0 -122
  477. package/lib/chat/web-commands.mjs +0 -146
  478. package/lib/chat/web-launcher.mjs +0 -63
  479. package/lib/chat/web-picker-keys.mjs +0 -46
  480. package/lib/chat/web-session.mjs +0 -159
  481. package/lib/config/alias.mjs +0 -57
  482. package/lib/dashboard-demo.mjs +0 -71
  483. package/lib/dashboard-static.mjs +0 -175
  484. package/lib/install/desktop-binary-download.mjs +0 -88
  485. package/lib/profiles/loader.mjs +0 -123
  486. package/lib/profiles/rebrand.mjs +0 -46
  487. package/lib/server/auth.mjs +0 -169
  488. package/lib/server/chat-loop.mjs +0 -622
  489. package/lib/server/chat.mjs +0 -336
  490. package/lib/server/cors.mjs +0 -77
  491. package/lib/server/csrf.mjs +0 -103
  492. package/lib/server/demo-preview.mjs +0 -63
  493. package/lib/server/index.mjs +0 -2641
  494. package/lib/server/insights.mjs +0 -780
  495. package/lib/server/langfuse-login.mjs +0 -58
  496. package/lib/server/rate-limit.mjs +0 -93
  497. package/lib/server/telemetry-login.mjs +0 -100
  498. package/lib/server/webhook.mjs +0 -512
  499. package/specialists/contracts.json +0 -1032
  500. package/specialists/contracts.schema.json +0 -83
  501. package/specialists/policy-inventory.json +0 -188
  502. package/specialists/registry.json +0 -1412
  503. package/specialists/role-manifests.json +0 -217
  504. package/specialists/teams.json +0 -94
@@ -3,7 +3,7 @@
3
3
  * record (construct-6zga.1.8).
4
4
  *
5
5
  * Consolidates the four formerly-scattered capability producers into one
6
- * serializable, versioned record that chat (resolveCapabilityTier) and specialist
6
+ * serializable, versioned record that model routing (resolveCapabilityTier) and specialist
7
7
  * prompt composition (resolveModelOperatingProfile via lib/prompt-composer.js) both
8
8
  * consume. It is keyed by provider, requested/resolved model, adapter protocol, and
9
9
  * observation time. Every capability declares an evidence source
@@ -57,8 +57,8 @@ export function providerGroupForModel(model) {
57
57
  return seg || 'unknown';
58
58
  }
59
59
 
60
- // Adapter protocol mirrors the dispatch in apps/chat/engine/ai-sdk-agent.mjs:
61
- // Anthropic uses its native messages API, OpenAI and Copilot use chat completions,
60
+ // Adapter protocol mirrors provider worker dispatch:
61
+ // Anthropic uses its native messages API, OpenAI and Copilot use completions,
62
62
  // and every other provider goes through the OpenAI-compatible shim.
63
63
 
64
64
  function adapterProtocolForGroup(group) {
@@ -81,11 +81,10 @@ function oneOf(allowed, value, fallback) {
81
81
  return allowed.includes(value) ? value : fallback;
82
82
  }
83
83
 
84
- // The chat turn overlay (lib/chat/transparency.mjs) speaks the orchestration
85
- // vocabulary (INTENT_CLASSES, risk flags, externalResearch). These helpers map it
84
+ // Routing overlays speak the orchestration vocabulary (INTENT_CLASSES, risk flags,
85
+ // externalResearch). These helpers map that vocabulary
86
86
  // onto the compiler's stable input enums so the compiler stays decoupled from
87
- // routing changes. A turn that follows a failed tool result is the recovery case
88
- // the loop signals with toolFailure.
87
+ // routing changes. A turn that follows a failed tool result is the recovery case.
89
88
 
90
89
  export function normalizePolicyIntent({ intent = null, assumptionsBlocked = false, toolFailure = false } = {}) {
91
90
  if (toolFailure === true) return 'tool-failure';
@@ -240,10 +239,10 @@ export function compileExecutionPolicy({
240
239
  }
241
240
 
242
241
  /**
243
- * Compile a policy directly from a chat turn overlay (lib/chat/transparency.mjs),
242
+ * Compile a policy directly from a routing overlay,
244
243
  * normalizing its routing vocabulary into the compiler's input enums first.
245
244
  */
246
- export function compilePolicyFromOverlay({ profile = null, overlay = null, toolFailure = false } = {}) {
245
+ export function compilePolicyFromRoutingOverlay({ profile = null, overlay = null, toolFailure = false } = {}) {
247
246
  const intent = normalizePolicyIntent({
248
247
  intent: overlay?.intent,
249
248
  assumptionsBlocked: overlay?.assumptionsBlocked === true,
@@ -257,6 +256,8 @@ export function compilePolicyFromOverlay({ profile = null, overlay = null, toolF
257
256
  return compileExecutionPolicy({ profile, intent, risk, evidenceRequirement });
258
257
  }
259
258
 
259
+ export const compilePolicyFromOverlay = compilePolicyFromRoutingOverlay;
260
+
260
261
  /**
261
262
  * Hand-rolled validator (no ajv — Construct stays dependency-free at startup).
262
263
  * Returns { valid, errors } against schemas/execution-policy.schema.json.
@@ -71,8 +71,16 @@ function openRouterCapabilities(model) {
71
71
  };
72
72
  }
73
73
 
74
+ function resolveSecretOrNull(varNames, opts) {
75
+ try {
76
+ return resolveFirstSecret(varNames, opts);
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
74
82
  export async function pollOpenRouter({ env = process.env } = {}) {
75
- const apiKey = resolveFirstSecret(['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'], { env });
83
+ const apiKey = resolveSecretOrNull(['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'], { env, allowAmbient: env === process.env });
76
84
  const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
77
85
  const data = await fetchJson('https://openrouter.ai/api/v1/models', { headers });
78
86
  if (!data) return null;
@@ -80,10 +88,15 @@ export async function pollOpenRouter({ env = process.env } = {}) {
80
88
  const models = [];
81
89
  for (const m of list) {
82
90
  if (!m?.id) continue;
91
+ const outputs = m.architecture?.output_modalities;
92
+ if (Array.isArray(outputs) && outputs.length && !outputs.includes('text')) continue;
93
+ if (/(embed|rerank|whisper|tts|dall-e|moderation|transcribe|sora)/i.test(m.id)) continue;
83
94
  const free = isFreePricing(m.pricing);
84
95
  const input = Number(m.pricing?.prompt);
85
96
  const output = Number(m.pricing?.completion);
86
97
  const caps = openRouterCapabilities(m);
98
+ const context = Number(m.context_length) || null;
99
+ if (!free && context && context < 8000) continue;
87
100
  models.push({
88
101
  id: `openrouter/${m.id}`,
89
102
  label: m.name || m.id,
@@ -92,11 +105,12 @@ export async function pollOpenRouter({ env = process.env } = {}) {
92
105
  pricing: Number.isFinite(input) && Number.isFinite(output)
93
106
  ? { input: input * 1_000_000, output: output * 1_000_000 }
94
107
  : null,
95
- context: Number(m.context_length) || null,
108
+ context,
96
109
  reasoning: caps.reasoning,
97
110
  tools: caps.tools,
98
111
  toolsKnown: true,
99
112
  vision: caps.vision,
113
+ architecture: m.architecture || null,
100
114
  source: 'live',
101
115
  });
102
116
  }
@@ -110,7 +124,7 @@ export async function pollOpenRouter({ env = process.env } = {}) {
110
124
  const OPENAI_NON_CHAT = /(embedding|whisper|tts|audio|realtime|image|dall-e|moderation|search|babbage|davinci|transcribe|sora)/i;
111
125
 
112
126
  export async function pollOpenAI({ env = process.env } = {}) {
113
- const apiKey = resolveFirstSecret(['OPENAI_API_KEY'], { env });
127
+ const apiKey = resolveSecretOrNull(['OPENAI_API_KEY'], { env, allowAmbient: env === process.env });
114
128
  if (!apiKey) return null;
115
129
  const data = await fetchJson('https://api.openai.com/v1/models', {
116
130
  headers: { Authorization: `Bearer ${apiKey}` },
@@ -141,7 +155,7 @@ export async function pollOpenAI({ env = process.env } = {}) {
141
155
  }
142
156
 
143
157
  export async function pollAnthropic({ env = process.env } = {}) {
144
- const apiKey = resolveFirstSecret(['ANTHROPIC_API_KEY'], { env });
158
+ const apiKey = resolveSecretOrNull(['ANTHROPIC_API_KEY'], { env, allowAmbient: env === process.env });
145
159
  if (!apiKey) return null;
146
160
  const data = await fetchJson('https://api.anthropic.com/v1/models?limit=100', {
147
161
  headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
@@ -48,6 +48,43 @@ export function sanitizeOpenCodeConfig(config) {
48
48
  return sanitized;
49
49
  }
50
50
 
51
+ // op run resolves only the launch env-file, never values baked into config
52
+ // JSON. A literal `op://` reference (or a build-time __PLACEHOLDER__) left in a
53
+ // credential field is therefore sent to OpenRouter verbatim, which 401s. Only
54
+ // `{env:VAR}` (resolved by OpenCode) and concrete keys count as real auth.
55
+
56
+ function isUnresolvedSecretRef(value) {
57
+ return typeof value === 'string' && (value.includes('op://') || value.includes('__OPENROUTER_API_KEY__'));
58
+ }
59
+
60
+ function openRouterHasAuth(provider = {}) {
61
+ const apiKey = provider.apiKey;
62
+ if (typeof apiKey === 'string' && apiKey.length > 0 && !isUnresolvedSecretRef(apiKey)) {
63
+ return true;
64
+ }
65
+ const auth = provider.options?.headers?.Authorization;
66
+ return typeof auth === 'string' && auth.length > 0 && !isUnresolvedSecretRef(auth);
67
+ }
68
+
69
+ export function ensureOpenRouterProviderAuth(config) {
70
+ if (!config?.provider?.openrouter) return config;
71
+ const provider = config.provider.openrouter;
72
+
73
+ // A broken Authorization header overrides the working apiKey at request time,
74
+ // so strip it before deciding whether the provider still needs an env-ref key.
75
+
76
+ const authHeader = provider.options?.headers?.Authorization;
77
+ if (isUnresolvedSecretRef(authHeader)) {
78
+ delete provider.options.headers.Authorization;
79
+ }
80
+ if (openRouterHasAuth(provider)) return config;
81
+ config.provider.openrouter = {
82
+ ...provider,
83
+ apiKey: '{env:OPENROUTER_API_KEY}',
84
+ };
85
+ return config;
86
+ }
87
+
51
88
  /**
52
89
  * Maps Construct internal tiers (fast, standard, reasoning) to OpenCode model configurations.
53
90
  */
@@ -124,7 +161,7 @@ export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
124
161
  if (config.models.some(m => m.provider === 'openrouter')) {
125
162
  config.provider.openrouter = config.provider.openrouter || {
126
163
  baseUrl: "https://openrouter.ai/api/v1",
127
- apiKey: "__OPENROUTER_API_KEY__"
164
+ apiKey: "{env:OPENROUTER_API_KEY}"
128
165
  };
129
166
  }
130
167
  if (config.models.some(m => m.provider === 'ollama')) {
@@ -204,6 +241,24 @@ export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
204
241
  // The relevant models should only be defined under provider.<name>.models
205
242
  delete config.models;
206
243
 
244
+ ensureOpenRouterProviderAuth(config);
245
+
246
+ // The plugin array is hand- and CLI-editable, so it accumulates junk: stale
247
+ // toolkit paths from moved checkouts and stray CLI tokens (e.g. a literal
248
+ // "list"). A non-existent path entry can break plugin loading outright. Keep
249
+ // existing file-path plugins and plausible package specifiers, deduped.
250
+
251
+ if (Array.isArray(config.plugin)) {
252
+ const seen = new Set();
253
+ config.plugin = config.plugin.filter((entry) => {
254
+ if (typeof entry !== "string" || !entry.trim() || seen.has(entry)) return false;
255
+ seen.add(entry);
256
+ const isPath = /^([./~]|[A-Za-z]:[\\/])/.test(entry) || /\.[cm]?js$/.test(entry);
257
+ if (isPath) return fs.existsSync(entry);
258
+ return entry.length > 2 && (/^@/.test(entry) || /[-/]/.test(entry));
259
+ });
260
+ }
261
+
207
262
  fs.writeFileSync(target, `${JSON.stringify(sanitizeOpenCodeConfig(config), null, 2)}\n`, "utf8");
208
263
  return target;
209
264
  }
@@ -5,6 +5,7 @@
5
5
  * records each as a telemetry trace, and applies model fallback on rate-limit errors.
6
6
  */
7
7
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
+ import { loadRegistry } from './registry/loader.mjs';
8
9
  import { homedir } from "node:os";
9
10
  import { dirname, join, resolve } from "node:path";
10
11
  import { spawnSync } from "node:child_process";
@@ -416,9 +417,7 @@ function buildHostConstraints() {
416
417
 
417
418
  function readRegistryModels(rootDir) {
418
419
  try {
419
- const registryPath = join(rootDir, "specialists", "registry.json");
420
- if (!existsSync(registryPath)) return {};
421
- const registry = JSON.parse(readFileSync(registryPath, "utf8"));
420
+ const registry = loadRegistry({ rootDir, skipValidation: true });
422
421
  return registry?.models ?? {};
423
422
  } catch {
424
423
  return {};
@@ -759,13 +758,17 @@ async function emitRuntimeTrace(event, { client, env = process.env }) {
759
758
  await ingest.flush();
760
759
  }
761
760
  } catch {
762
- await client?.app?.log?.({
763
- body: {
764
- service: "construct",
765
- level: "warn",
766
- message: "OpenCode runtime telemetry failed before delivery.",
767
- },
768
- });
761
+ try {
762
+ await client?.app?.log?.({
763
+ body: {
764
+ service: "construct",
765
+ level: "warn",
766
+ message: "OpenCode runtime telemetry failed before delivery.",
767
+ },
768
+ });
769
+ } catch {
770
+ // best effort: telemetry failure must not block fallback handling
771
+ }
769
772
  }
770
773
  }
771
774
 
@@ -49,6 +49,10 @@ function collectBypassArtifacts(projectDir) {
49
49
  }
50
50
 
51
51
  function collectReviewerGaps(projectDir) {
52
+ const agentLog = path.join(projectDir, '.cx', 'agent-log.jsonl');
53
+ if (!fs.existsSync(agentLog)) {
54
+ return { gaps: [], armed: false };
55
+ }
52
56
  const roots = [
53
57
  path.join(projectDir, 'docs', 'specs', 'prd'),
54
58
  path.join(projectDir, 'docs', 'prd'),
@@ -70,12 +74,12 @@ function collectReviewerGaps(projectDir) {
70
74
  }
71
75
  }
72
76
  }
73
- return gaps.slice(0, 20);
77
+ return { gaps: gaps.slice(0, 20), armed: true };
74
78
  }
75
79
 
76
80
  export function collectArtifactGateSignals({ rootDir, projectDir }) {
77
81
  const bypassed = collectBypassArtifacts(projectDir);
78
- const reviewerGaps = collectReviewerGaps(projectDir);
82
+ const { gaps: reviewerGaps, armed: reviewerGateArmed } = collectReviewerGaps(projectDir);
79
83
 
80
84
  let specialistAudit = { present: false, pass: true, crossCheckCount: 0 };
81
85
  if (isConstructPackageRepo(projectDir)) {
@@ -87,8 +91,13 @@ export function collectArtifactGateSignals({ rootDir, projectDir }) {
87
91
  crossCheckCount: audit.crossCheckIssues?.length ?? 0,
88
92
  crossCheckIssues: (audit.crossCheckIssues ?? []).slice(0, 5),
89
93
  };
90
- } catch {
91
- specialistAudit = { present: true, pass: false, crossCheckCount: -1 };
94
+ } catch (err) {
95
+ specialistAudit = {
96
+ present: true,
97
+ pass: false,
98
+ crossCheckCount: 0,
99
+ error: err?.message ?? String(err),
100
+ };
92
101
  }
93
102
  }
94
103
 
@@ -97,6 +106,7 @@ export function collectArtifactGateSignals({ rootDir, projectDir }) {
97
106
  bypassCount: bypassed.length,
98
107
  reviewerGaps,
99
108
  reviewerGapCount: reviewerGaps.length,
109
+ reviewerGateArmed,
100
110
  specialistAudit,
101
111
  };
102
112
  }
@@ -21,6 +21,7 @@ import { runOracleTick, listPending, approvePending, defaultRootDir } from './ac
21
21
  import { collectOracleGaps, formatOracleGapsReport } from './gaps.mjs';
22
22
  import { reconcileOracleHygieneBeads } from './reconcile.mjs';
23
23
  import { doctorRoot } from '../config/xdg.mjs';
24
+ import { writeLastTick } from './index.mjs';
24
25
 
25
26
  const MODULE_FILE = fileURLToPath(import.meta.url);
26
27
 
@@ -87,6 +88,7 @@ async function cmdReview(args, { rootDir, projectDir, homeDir }) {
87
88
  const dryRun = args.includes('--dry-run');
88
89
  const json = args.includes('--json');
89
90
  const result = await runOracleTick({ rootDir, projectDir, homeDir, dryRun });
91
+ if (!dryRun) writeLastTick(result.tick, homeDir);
90
92
  if (json) {
91
93
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
92
94
  return 0;
@@ -9,6 +9,7 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
 
11
11
  import { routeGap, routeAction } from './routing.mjs';
12
+ import { resolveRemediationDispatch } from './remediation-dispatch.mjs';
12
13
 
13
14
  export function routingDir(projectDir) {
14
15
  return path.join(projectDir, '.cx', 'oracle', 'routing');
@@ -28,9 +29,16 @@ export function buildRoutingArtifact({ synthesis, readModel, tickId, route }) {
28
29
 
29
30
  for (const gap of synthesis.gaps ?? []) {
30
31
  const r = routeGap(gap);
32
+ const dispatch = gap.remediationRoute?.mode
33
+ ? gap.remediationRoute
34
+ : resolveRemediationDispatch(gap);
31
35
  lines.push(` [${gap.severity}] ${gap.id} — ${gap.detail}`);
32
36
  lines.push(` signal: ${gap.signal ?? 'unknown'}`);
33
37
  lines.push(` route: ${r.primary}${r.secondary ? ` (secondary: ${r.secondary})` : ''}`);
38
+ lines.push(` dispatch: ${dispatch.mode}${dispatch.teamRouting?.involvedTeams?.length ? ` (teams: ${dispatch.teamRouting.involvedTeams.join(', ')})` : ''}`);
39
+ if (dispatch.mode === 'swarm' && dispatch.specialists?.length) {
40
+ lines.push(` swarm: ${dispatch.specialists.join(', ')}`);
41
+ }
34
42
  if (r.gateType) lines.push(` gate: ${r.gateType}`);
35
43
  }
36
44
 
@@ -50,6 +58,15 @@ export function buildRoutingArtifact({ synthesis, readModel, tickId, route }) {
50
58
  if (route?.primary) {
51
59
  lines.push('', `DISPATCH TARGET: ${route.primary}`);
52
60
  }
61
+ if (route?.mode) {
62
+ lines.push(`DISPATCH MODE: ${route.mode}`);
63
+ if (route.mode === 'swarm' && route.specialists?.length) {
64
+ lines.push(`SWARM SPECIALISTS: ${route.specialists.join(', ')}`);
65
+ }
66
+ if (route.teamRouting?.involvedTeams?.length) {
67
+ lines.push(`INVOLVED TEAMS: ${route.teamRouting.involvedTeams.join(', ')}`);
68
+ }
69
+ }
53
70
 
54
71
  lines.push('', 'BLOCKED:', ' none (deterministic routing artifact — LLM dispatch is approve-gated)');
55
72
  return lines.join('\n');
@@ -76,6 +93,6 @@ export function maybeWriteHighSeverityRouting({ projectDir, tick, synthesis, rea
76
93
  if (!hasHigh) return null;
77
94
  const tickId = tick.at ?? new Date().toISOString();
78
95
  const primaryGap = synthesis.gaps.find((g) => g.severity === 'high');
79
- const route = routeGap(primaryGap);
96
+ const route = primaryGap?.remediationRoute ?? routeGap(primaryGap);
80
97
  return writeRoutingArtifact({ projectDir, tickId, synthesis, readModel, route });
81
98
  }
@@ -16,6 +16,7 @@ import { recordAndMaybeInvoke } from '../roles/gateway.mjs';
16
16
  import { validateCapabilityRegistry } from '../registry/validate.mjs';
17
17
  import { writeRoutingArtifact } from './dispatch.mjs';
18
18
  import { routeAction } from './routing.mjs';
19
+ import { resolveRemediationDispatch } from './remediation-dispatch.mjs';
19
20
  import { doctorRoot } from '../config/xdg.mjs';
20
21
 
21
22
  function rolePendingPath(homeDir = homedir()) {
@@ -30,14 +31,14 @@ function appendRolePending(entry, homeDir = homedir()) {
30
31
  fs.appendFileSync(file, JSON.stringify(entry) + '\n', 'utf8');
31
32
  }
32
33
 
33
- async function dispatchSpecialist({ action, projectDir, homeDir, personaId, eventType }) {
34
+ async function dispatchSpecialist({ action, projectDir, homeDir, personaId, eventType, dispatch = null }) {
34
35
  const route = routeAction(action.kind);
35
36
  const artifactPath = writeRoutingArtifact({
36
37
  projectDir,
37
38
  tickId: action.id,
38
39
  synthesis: { verdict: 'approved', gaps: [], recommendedActions: [action] },
39
40
  readModel: null,
40
- route,
41
+ route: dispatch ?? route,
41
42
  });
42
43
 
43
44
  const prevRolesRoot = process.env.CONSTRUCT_ROLES_ROOT;
@@ -53,6 +54,8 @@ async function dispatchSpecialist({ action, projectDir, homeDir, personaId, even
53
54
  artifactPath,
54
55
  gateType: route.gateType,
55
56
  requiredApprover: route.primary,
57
+ dispatchMode: dispatch?.mode ?? 'static',
58
+ involvedTeams: dispatch?.teamRouting?.involvedTeams ?? [],
56
59
  },
57
60
  };
58
61
 
@@ -69,11 +72,12 @@ async function dispatchSpecialist({ action, projectDir, homeDir, personaId, even
69
72
  personaId: personaId.replace(/^cx-/, ''),
70
73
  cxId: personaId,
71
74
  bdIssueId: gatewayResult?.bdIssueId ?? null,
72
- fingerprint: `oracle-${action.id}`,
75
+ fingerprint: `oracle-${action.id}-${personaId}`,
73
76
  eventType,
74
77
  summary: action.summary,
75
78
  source: 'oracle-execute',
76
79
  artifactPath,
80
+ dispatchMode: dispatch?.mode ?? 'static',
77
81
  }, homeDir);
78
82
  }
79
83
 
@@ -88,6 +92,34 @@ async function dispatchSpecialist({ action, projectDir, homeDir, personaId, even
88
92
  };
89
93
  }
90
94
 
95
+ async function dispatchRemediation({ action, projectDir, homeDir, eventType, personaId = null }) {
96
+ const dispatch = resolveRemediationDispatch(action, { projectDir });
97
+ if (dispatch.mode === 'swarm') {
98
+ const results = [];
99
+ for (const specialistId of dispatch.specialists) {
100
+ results.push(await dispatchSpecialist({
101
+ action, projectDir, homeDir, personaId: specialistId, eventType, dispatch,
102
+ }));
103
+ }
104
+ return {
105
+ ok: true,
106
+ mode: 'swarm',
107
+ specialists: dispatch.specialists,
108
+ teamRouting: dispatch.teamRouting,
109
+ results,
110
+ };
111
+ }
112
+ const result = await dispatchSpecialist({
113
+ action,
114
+ projectDir,
115
+ homeDir,
116
+ personaId: personaId ?? dispatch.primary,
117
+ eventType,
118
+ dispatch,
119
+ });
120
+ return { ...result, mode: 'static', teamRouting: dispatch.teamRouting };
121
+ }
122
+
91
123
  /**
92
124
  * Execute an approved Oracle pending action.
93
125
  *
@@ -122,20 +154,20 @@ export async function executeApprovedAction(action, { rootDir, projectDir, homeD
122
154
  return { ok: result.status === 0, kind, exitCode: result.status };
123
155
  }
124
156
  case 'specialist-review':
125
- return dispatchSpecialist({
157
+ return dispatchRemediation({
126
158
  action, projectDir, homeDir, personaId: 'cx-reviewer', eventType: 'contract.violation',
127
159
  });
128
160
  case 'trace-review':
129
- return dispatchSpecialist({
161
+ return dispatchRemediation({
130
162
  action, projectDir, homeDir, personaId: 'cx-trace-reviewer', eventType: 'outcomes.degraded',
131
163
  });
132
164
  case 'doctor-followup':
133
- return dispatchSpecialist({
165
+ return dispatchRemediation({
134
166
  action, projectDir, homeDir, personaId: 'cx-sre', eventType: 'service.down',
135
167
  });
136
168
  case 'executive-signoff-required':
137
169
  case 'structure-cleanup-proposal':
138
- return dispatchSpecialist({
170
+ return dispatchRemediation({
139
171
  action, projectDir, homeDir, personaId: routeAction(kind).primary, eventType: 'oracle.approval',
140
172
  });
141
173
  default:
@@ -29,7 +29,7 @@ export function lastTickPath(homeDir = homedir()) {
29
29
  return path.join(runtimeDir(homeDir), 'last-tick.json');
30
30
  }
31
31
 
32
- function writeLastTick(tick, homeDir) {
32
+ export function writeLastTick(tick, homeDir = homedir()) {
33
33
  const dir = runtimeDir(homeDir);
34
34
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
35
35
  fs.writeFileSync(lastTickPath(homeDir), JSON.stringify(tick, null, 2));
@@ -12,6 +12,7 @@ import { fileURLToPath } from 'node:url';
12
12
  import { alignmentFindings } from '../workflow-state.mjs';
13
13
  import { loadCapabilityRegistry } from '../registry/validate.mjs';
14
14
  import { isConstructPackageRepo } from '../host-disposition.mjs';
15
+ import { loadRegistry } from '../registry/loader.mjs';
15
16
 
16
17
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
17
18
  const PACKAGE_ROOT = path.resolve(MODULE_DIR, '../..');
@@ -25,10 +26,14 @@ function readJsonSafe(filePath) {
25
26
  }
26
27
 
27
28
  function collectPolicyInventory(rootDir) {
28
- const file = path.join(rootDir, 'specialists', 'policy-inventory.json');
29
- const data = readJsonSafe(file);
30
- if (!data) return { present: false, count: 0, policies: [] };
31
- const policies = data.policies ?? [];
29
+ let policies = [];
30
+ try {
31
+ const registry = loadRegistry({ rootDir, skipValidation: true });
32
+ const raw = registry.policies || {};
33
+ policies = Array.isArray(raw) ? raw : Object.values(raw);
34
+ } catch {
35
+ return { present: false, count: 0, policies: [] };
36
+ }
32
37
  const orphans = [];
33
38
  for (const p of policies) {
34
39
  if (p.source && p.source !== 'built-in' && !p.source.startsWith('specialists/')) {
@@ -79,7 +84,7 @@ function collectCapabilityGates(rootDir) {
79
84
  }
80
85
 
81
86
  function collectPropagationSignals({ rootDir, projectDir, parity, registryValidate }) {
82
- const registryPath = path.join(rootDir, 'specialists', 'registry.json');
87
+ const registryPath = path.join(rootDir, 'specialists', 'org');
83
88
  let registryMtime = null;
84
89
  try {
85
90
  registryMtime = fs.statSync(registryPath).mtimeMs;
@@ -30,6 +30,12 @@ export const VERDICT_ONLY_GAP_IDS = new Set([
30
30
  'propagation-stale',
31
31
  'census-stale',
32
32
  'outcomes-missing',
33
+ // Team governance signals require specialist review and cross-team discussion.
34
+ // They are not auto-actionable and surface in doctor/prelude as informational gaps.
35
+ 'team-understaffed',
36
+ 'escalation-path-broken',
37
+ 'team-decision-violation',
38
+ 'cross-team-handoff-blocked',
33
39
  ]);
34
40
 
35
41
  const DENY_ACTIONS = new Set([
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import fs from 'node:fs';
12
+ import { loadRegistry } from '../registry/loader.mjs';
12
13
  import path from 'node:path';
13
14
  import { spawnSync } from 'node:child_process';
14
15
 
@@ -20,6 +21,7 @@ import { collectOrgGraph } from './org-graph.mjs';
20
21
  import { collectArtifactGateSignals } from './artifact-gate.mjs';
21
22
  import { loadGraph, nodesByType, dependentsOf } from '../graph/store.mjs';
22
23
  import { hashFiles } from '../graph/build-from-registry.mjs';
24
+ import { GRAPH_SEED_FILES } from '../graph/staleness.mjs';
23
25
  import { readViolationSupersedeCutoff } from '../contracts/violation-log.mjs';
24
26
  import { doctorRoot } from '../config/xdg.mjs';
25
27
 
@@ -235,12 +237,6 @@ function collectStructureSprawl(projectDir) {
235
237
  }
236
238
  }
237
239
 
238
- const GRAPH_SEED_FILES = [
239
- 'registry/capabilities.json',
240
- 'specialists/contracts.json',
241
- 'lib/embedded-contract/workflow-defs.mjs',
242
- ];
243
-
244
240
  // The living dependency matrix: file↔capability↔workflow↔test graph at
245
241
  // .cx/graph/. Surfaces three oversight signals — staleness (seeds changed
246
242
  // since last build), coverage (capabilities/workflows with no impl or test
@@ -314,6 +310,105 @@ function collectDeadCodeSync(rootDir, projectDir) {
314
310
  return { present: false, pendingAsync: true, deadCount: 0, regressions: [] };
315
311
  }
316
312
 
313
+ function specialistTeamId(specialist) {
314
+ return specialist.teamId ?? specialist.team ?? null;
315
+ }
316
+
317
+ function specialistsForTeamEntity(teamId, team, registry) {
318
+ const all = Object.values(registry.specialists ?? {});
319
+ if (team.kind === 'group' && Array.isArray(team.squads) && team.squads.length > 0) {
320
+ const squadIds = new Set(team.squads);
321
+ return all.filter((s) => squadIds.has(specialistTeamId(s)));
322
+ }
323
+ return all.filter((s) => specialistTeamId(s) === teamId);
324
+ }
325
+
326
+ function isTeamStaffed(teamId, team, registry) {
327
+ if (specialistsForTeamEntity(teamId, team, registry).length > 0) return true;
328
+ if (team.kind === 'squad' && Array.isArray(team.specialists) && team.specialists.length > 0) {
329
+ return true;
330
+ }
331
+ return false;
332
+ }
333
+
334
+ function ownerExistsForTeam(teamId, team, registry) {
335
+ const ownerRole = team.owner;
336
+ if (!ownerRole) return true;
337
+ return specialistsForTeamEntity(teamId, team, registry).some((s) => s.role === ownerRole);
338
+ }
339
+
340
+ function collectTeamGovernance(rootDir) {
341
+ const registry = loadRegistry({ rootDir });
342
+ if (!registry?.teams) {
343
+ return { present: false, teams: {} };
344
+ }
345
+
346
+ const teams = {};
347
+ for (const [teamId, team] of Object.entries(registry.teams)) {
348
+ const roles = team.roles ?? [];
349
+ const escalationPath = team.escalationPath ?? [];
350
+ const decisionRights = team.decisionRights ?? [];
351
+ const forbiddenDecisions = team.forbiddenDecisions ?? [];
352
+ const teamSpecialists = specialistsForTeamEntity(teamId, team, registry);
353
+
354
+ // Check if escalation path is complete (all roles exist in registry)
355
+ const escalationPathBroken = escalationPath.some((role) => {
356
+ const specialist = Object.values(registry.specialists ?? {}).some((s) => s.role === role);
357
+ return !specialist;
358
+ });
359
+
360
+ teams[teamId] = {
361
+ id: teamId,
362
+ name: team.name,
363
+ kind: team.kind ?? null,
364
+ owner: team.owner,
365
+ roleCount: roles.length,
366
+ specialistCount: teamSpecialists.length,
367
+ decisionRightsCount: decisionRights.length,
368
+ forbiddenDecisionsCount: forbiddenDecisions.length,
369
+ escalationPathLength: escalationPath.length,
370
+ escalationPathBroken,
371
+ ownerExists: ownerExistsForTeam(teamId, team, registry),
372
+ understaffed: roles.length > 0 && !isTeamStaffed(teamId, team, registry),
373
+ };
374
+ }
375
+
376
+ // Cross-team handoff health: a crossing contract whose required approver team
377
+ // is missing or unstaffed can never be approved, so the handoff is blocked.
378
+ const contracts = Array.isArray(registry.contracts)
379
+ ? registry.contracts
380
+ : Object.values(registry.contracts ?? {});
381
+ const crossTeamHandoffsBlocked = [];
382
+ for (const contract of contracts) {
383
+ const boundary = contract.teamBoundary;
384
+ if (!boundary?.crosses
385
+ || !Array.isArray(boundary.requiresApprovalFrom)
386
+ || boundary.requiresApprovalFrom.length === 0) {
387
+ continue;
388
+ }
389
+ const unstaffedApprovers = boundary.requiresApprovalFrom.filter((approverId) => {
390
+ const team = registry.teams?.[approverId];
391
+ if (!team) return true;
392
+ return !isTeamStaffed(approverId, team, registry);
393
+ });
394
+ if (unstaffedApprovers.length > 0) {
395
+ crossTeamHandoffsBlocked.push({
396
+ contract: contract.id,
397
+ producerTeam: boundary.producerTeam ?? null,
398
+ consumerTeam: boundary.consumerTeam ?? null,
399
+ blockedBy: unstaffedApprovers,
400
+ });
401
+ }
402
+ }
403
+
404
+ return {
405
+ present: true,
406
+ teamCount: Object.keys(teams).length,
407
+ teams,
408
+ crossTeamHandoffsBlocked,
409
+ };
410
+ }
411
+
317
412
  /**
318
413
  * Collect the Oracle read model from durable Construct signals.
319
414
  *
@@ -334,6 +429,7 @@ export function collectReadModel({ rootDir, projectDir, homeDir }) {
334
429
  const registryValidate = collectRegistryValidate(rootDir);
335
430
  const orgGraph = collectOrgGraph({ rootDir, projectDir, parity, registryValidate });
336
431
  const artifactGate = collectArtifactGateSignals({ rootDir, projectDir });
432
+ const teamGovernance = collectTeamGovernance(rootDir);
337
433
 
338
434
  return {
339
435
  collectedAt,
@@ -349,6 +445,7 @@ export function collectReadModel({ rootDir, projectDir, homeDir }) {
349
445
  parity,
350
446
  orgGraph,
351
447
  artifactGate,
448
+ teamGovernance,
352
449
  beads: collectBeadsOpenCount(projectDir),
353
450
  hookFailures: collectHookFailures(homeDir),
354
451
  structure: collectStructureSprawl(projectDir),