@geraldmaron/construct 1.1.0 → 1.2.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 (425) hide show
  1. package/README.md +30 -18
  2. package/bin/construct +392 -91
  3. package/bin/construct-postinstall.mjs +30 -2
  4. package/examples/distribution/README.md +42 -0
  5. package/examples/distribution/manifest.json +53 -0
  6. package/examples/distribution/sources/adr.md +84 -0
  7. package/examples/distribution/sources/deck-one-pager.md +65 -0
  8. package/examples/distribution/sources/prd-platform.md +161 -0
  9. package/examples/distribution/sources/research-brief.md +80 -0
  10. package/examples/distribution/sources/rfc-platform.md +82 -0
  11. package/examples/distribution/sources/runbook.md +103 -0
  12. package/examples/distribution/sources/strategy.md +88 -0
  13. package/lib/adapters-sync.mjs +67 -0
  14. package/lib/artifact-gate-notice.mjs +38 -0
  15. package/lib/artifact-manifest.mjs +112 -0
  16. package/lib/artifact-release-gate.mjs +171 -0
  17. package/lib/artifact-reviewers.mjs +68 -0
  18. package/lib/artifact-type-from-path.mjs +79 -0
  19. package/lib/audit-skills.mjs +4 -0
  20. package/lib/audit-specialists.mjs +285 -0
  21. package/lib/audit-trail.mjs +77 -10
  22. package/lib/auto-docs.mjs +87 -18
  23. package/lib/beads/drift.mjs +27 -6
  24. package/lib/beads-client.mjs +49 -121
  25. package/lib/beads-optimistic.mjs +5 -12
  26. package/lib/brand-fonts.mjs +93 -0
  27. package/lib/brand-prose.mjs +214 -0
  28. package/lib/brand-tokens.mjs +92 -0
  29. package/lib/bridges/copilot-proxy.mjs +13 -26
  30. package/lib/capability-ledger.mjs +156 -0
  31. package/lib/certification/artifact-fixtures.mjs +132 -0
  32. package/lib/certification/artifact-gates.mjs +63 -0
  33. package/lib/certification/artifact-provenance.mjs +97 -0
  34. package/lib/certification/canonical-scenarios.mjs +78 -0
  35. package/lib/certification/cli.mjs +191 -0
  36. package/lib/certification/dashboard-api.mjs +71 -0
  37. package/lib/certification/demo-parity.mjs +116 -0
  38. package/lib/certification/document-io-fixtures.mjs +246 -0
  39. package/lib/certification/document-workflow.mjs +97 -0
  40. package/lib/certification/eval-bridge.mjs +77 -0
  41. package/lib/certification/model-routing.mjs +149 -0
  42. package/lib/certification/prompt-budget.mjs +119 -0
  43. package/lib/certification/rc-gate.mjs +206 -0
  44. package/lib/certification/real-llm-scenarios.mjs +303 -0
  45. package/lib/certification/role-cards.mjs +113 -0
  46. package/lib/certification/role-overlays.mjs +117 -0
  47. package/lib/certification/run.mjs +122 -0
  48. package/lib/certification/runner.mjs +323 -0
  49. package/lib/certification/scenarios.mjs +51 -0
  50. package/lib/certification/skill-inventory.mjs +289 -0
  51. package/lib/certification/skill-scenarios.mjs +147 -0
  52. package/lib/certification/specialist-contracts.mjs +85 -0
  53. package/lib/certification/specialist-scenarios.mjs +175 -0
  54. package/lib/certification/stale-impact.mjs +146 -0
  55. package/lib/certification/status.mjs +252 -0
  56. package/lib/certification/store.mjs +77 -0
  57. package/lib/chat/cli.mjs +333 -0
  58. package/lib/chat/command-suggest.mjs +161 -0
  59. package/lib/chat/commands.mjs +215 -0
  60. package/lib/chat/config.mjs +142 -0
  61. package/lib/chat/context-compactor.mjs +250 -0
  62. package/lib/chat/context-continuation.mjs +253 -0
  63. package/lib/chat/continuation-source.mjs +58 -0
  64. package/lib/chat/demo-guide.mjs +61 -0
  65. package/lib/chat/design-tokens.mjs +91 -0
  66. package/lib/chat/desktop-binary.mjs +79 -0
  67. package/lib/chat/desktop-build.mjs +130 -0
  68. package/lib/chat/desktop-launcher.mjs +133 -0
  69. package/lib/chat/evidence.mjs +145 -0
  70. package/lib/chat/export.mjs +74 -0
  71. package/lib/chat/harness/driver.mjs +91 -0
  72. package/lib/chat/list-picker.mjs +112 -0
  73. package/lib/chat/model-picker.mjs +356 -0
  74. package/lib/chat/openrouter-fallback.mjs +151 -0
  75. package/lib/chat/permission-prompt.mjs +33 -0
  76. package/lib/chat/picker-catalog.mjs +45 -0
  77. package/lib/chat/policy-telemetry.mjs +34 -0
  78. package/lib/chat/present.mjs +245 -0
  79. package/lib/chat/session-context.mjs +39 -0
  80. package/lib/chat/session-persist.mjs +73 -0
  81. package/lib/chat/session-restore.mjs +71 -0
  82. package/lib/chat/session-settings.mjs +53 -0
  83. package/lib/chat/system-prompt.mjs +52 -0
  84. package/lib/chat/transparency.mjs +93 -0
  85. package/lib/chat/tui/color-scheme.mjs +42 -0
  86. package/lib/chat/tui/markdown.mjs +123 -0
  87. package/lib/chat/tui/presentation.mjs +100 -0
  88. package/lib/chat/tui/render.mjs +500 -0
  89. package/lib/chat/tui/turn-block.mjs +284 -0
  90. package/lib/chat/tui/turn-present.mjs +18 -0
  91. package/lib/chat/tui/turn-state.mjs +88 -0
  92. package/lib/chat/tui/usage.mjs +122 -0
  93. package/lib/chat/web-commands.mjs +146 -0
  94. package/lib/chat/web-launcher.mjs +63 -0
  95. package/lib/chat/web-picker-keys.mjs +46 -0
  96. package/lib/chat/web-session.mjs +159 -0
  97. package/lib/cli-commands.mjs +232 -11
  98. package/lib/comment-lint.mjs +6 -4
  99. package/lib/config/schema.mjs +36 -5
  100. package/lib/contract-schemas/decision.json +50 -0
  101. package/lib/contract-schemas/implementation.json +51 -0
  102. package/lib/contract-schemas/review-report.json +32 -0
  103. package/lib/contract-schemas/test-report.json +43 -0
  104. package/lib/contracts/construct-handoff.mjs +60 -0
  105. package/lib/contracts/validate.mjs +32 -3
  106. package/lib/contracts/violation-log.mjs +58 -5
  107. package/lib/dashboard-demo.mjs +71 -0
  108. package/lib/dashboard-static.mjs +19 -5
  109. package/lib/decisions/registry.mjs +5 -3
  110. package/lib/deck-export-pptx.mjs +1152 -0
  111. package/lib/demo-recording.mjs +142 -0
  112. package/lib/demo-script.mjs +114 -0
  113. package/lib/demo-surface.mjs +249 -0
  114. package/lib/demo.mjs +703 -0
  115. package/lib/diagram-export.mjs +192 -0
  116. package/lib/diagram.mjs +302 -0
  117. package/lib/docs-verify.mjs +25 -7
  118. package/lib/doctor/index.mjs +4 -2
  119. package/lib/doctor/project-adapters.mjs +44 -0
  120. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  121. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  122. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  123. package/lib/doctor/watchers/service-health.mjs +1 -1
  124. package/lib/document-export.mjs +360 -36
  125. package/lib/document-extract.mjs +88 -0
  126. package/lib/document-ingest.mjs +32 -18
  127. package/lib/embed/cli.mjs +2 -2
  128. package/lib/embed/daemon.mjs +18 -46
  129. package/lib/embed/semantic.mjs +5 -2
  130. package/lib/embed/worker.mjs +2 -2
  131. package/lib/embedded-contract/model-resolve.mjs +10 -9
  132. package/lib/env-config.mjs +26 -0
  133. package/lib/evals/dataset.mjs +137 -0
  134. package/lib/evals/gates.mjs +175 -0
  135. package/lib/graph/build-co-change.mjs +54 -0
  136. package/lib/graph/build-from-registry.mjs +136 -0
  137. package/lib/graph/build-import-graph.mjs +153 -0
  138. package/lib/graph/cli.mjs +110 -0
  139. package/lib/graph/impact-cli.mjs +89 -0
  140. package/lib/graph/impact.mjs +108 -0
  141. package/lib/graph/staleness.mjs +44 -0
  142. package/lib/graph/store.mjs +172 -0
  143. package/lib/handoffs/cleanup.mjs +1 -1
  144. package/lib/health-check.mjs +90 -76
  145. package/lib/hooks/artifact-release-gate.mjs +43 -0
  146. package/lib/hooks/audit-trail.mjs +14 -28
  147. package/lib/hooks/brand-prose-lint.mjs +38 -0
  148. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  149. package/lib/hooks/session-optimize.mjs +84 -207
  150. package/lib/hooks/session-start.mjs +4 -5
  151. package/lib/host-disposition.mjs +7 -0
  152. package/lib/improvement/cli.mjs +189 -0
  153. package/lib/improvement/controller.mjs +137 -0
  154. package/lib/improvement/proposal.mjs +120 -0
  155. package/lib/improvement/specialist-loop.mjs +192 -0
  156. package/lib/improvement/store.mjs +89 -0
  157. package/lib/improvement/surface.mjs +219 -0
  158. package/lib/ingest-tooling.mjs +97 -0
  159. package/lib/init/doc-lanes.mjs +165 -0
  160. package/lib/init-docs.mjs +31 -178
  161. package/lib/init-unified.mjs +27 -97
  162. package/lib/init-update-guide.mjs +102 -0
  163. package/lib/init-update.mjs +32 -1
  164. package/lib/init.mjs +8 -0
  165. package/lib/install/desktop-binary-download.mjs +85 -0
  166. package/lib/install/legacy-global-cleanup.mjs +189 -0
  167. package/lib/intake/daemon.mjs +2 -0
  168. package/lib/intake/git-queue.mjs +9 -8
  169. package/lib/intake/queue.mjs +2 -1
  170. package/lib/intake/session-prelude.mjs +90 -1
  171. package/lib/libreoffice-export.mjs +97 -0
  172. package/lib/logging/rotate.mjs +9 -0
  173. package/lib/maintenance/docker-reclaim.mjs +206 -0
  174. package/lib/mcp/external-schema-cost.mjs +74 -0
  175. package/lib/mcp/server.mjs +84 -9
  176. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  177. package/lib/mcp/tool-budget.mjs +55 -4
  178. package/lib/mcp/tools/document.mjs +18 -4
  179. package/lib/mcp/tools/skills.mjs +32 -3
  180. package/lib/mcp/tools/workflow.mjs +13 -1
  181. package/lib/model-free-selector.mjs +18 -0
  182. package/lib/model-registry.mjs +13 -229
  183. package/lib/model-router.mjs +425 -93
  184. package/lib/models/behavior-matrix.mjs +289 -0
  185. package/lib/models/catalog.mjs +209 -0
  186. package/lib/models/execution-capability-profile.mjs +196 -0
  187. package/lib/models/execution-policy.mjs +307 -0
  188. package/lib/models/provider-poll.mjs +383 -0
  189. package/lib/npm-spawn-env.mjs +17 -0
  190. package/lib/ollama/installed-models.mjs +129 -0
  191. package/lib/oracle/actions.mjs +187 -0
  192. package/lib/oracle/artifact-gate.mjs +99 -0
  193. package/lib/oracle/cli.mjs +204 -0
  194. package/lib/oracle/daemon-entry.mjs +14 -0
  195. package/lib/oracle/dispatch.mjs +81 -0
  196. package/lib/oracle/execute.mjs +143 -0
  197. package/lib/oracle/gaps.mjs +76 -0
  198. package/lib/oracle/index.mjs +84 -0
  199. package/lib/oracle/issues.mjs +164 -0
  200. package/lib/oracle/org-graph.mjs +170 -0
  201. package/lib/oracle/policy.mjs +80 -0
  202. package/lib/oracle/read-model.mjs +398 -0
  203. package/lib/oracle/reconcile.mjs +191 -0
  204. package/lib/oracle/routing.mjs +89 -0
  205. package/lib/oracle/synthesize.mjs +384 -0
  206. package/lib/oracle/verdicts.mjs +51 -0
  207. package/lib/orchestration/worker.mjs +88 -27
  208. package/lib/orchestration-policy.mjs +50 -0
  209. package/lib/parity.mjs +96 -2
  210. package/lib/persona-sections.mjs +66 -0
  211. package/lib/playwright-demo.mjs +252 -0
  212. package/lib/project-init-shared.mjs +4 -1
  213. package/lib/prompt-composer.js +9 -4
  214. package/lib/prompt-validation-contract.mjs +24 -0
  215. package/lib/provider-capabilities.js +57 -11
  216. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  217. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  218. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  219. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  220. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  221. package/lib/providers/contract/contract-tests.mjs +57 -0
  222. package/lib/providers/contract/errors.mjs +48 -0
  223. package/lib/providers/contract/interface.mjs +50 -0
  224. package/lib/providers/contract/registry.mjs +102 -0
  225. package/lib/providers/copilot-auth.mjs +297 -0
  226. package/lib/providers/credential-bootstrap.mjs +178 -0
  227. package/lib/providers/credential-catalog.mjs +46 -0
  228. package/lib/providers/credential-sources.mjs +63 -0
  229. package/lib/providers/creds.mjs +5 -2
  230. package/lib/providers/op-run.mjs +59 -0
  231. package/lib/providers/secret-resolver.mjs +159 -0
  232. package/lib/publish-template.mjs +163 -0
  233. package/lib/publish-tooling.mjs +119 -0
  234. package/lib/publish.mjs +305 -0
  235. package/lib/registry/cli.mjs +82 -0
  236. package/lib/registry/consolidation.mjs +147 -0
  237. package/lib/registry/generate-docs.mjs +75 -0
  238. package/lib/registry/skill-verification.mjs +57 -0
  239. package/lib/registry/surface-map.mjs +76 -0
  240. package/lib/registry/validate.mjs +135 -0
  241. package/lib/resources/budget.mjs +82 -0
  242. package/lib/resources/process-budget.mjs +45 -0
  243. package/lib/rules-delivery.mjs +9 -2
  244. package/lib/rules-read.mjs +26 -0
  245. package/lib/runtime-env.mjs +23 -0
  246. package/lib/runtime-pressure.mjs +51 -7
  247. package/lib/schema-infer.mjs +13 -2
  248. package/lib/server/chat-loop.mjs +622 -0
  249. package/lib/server/demo-preview.mjs +63 -0
  250. package/lib/server/index.mjs +229 -8
  251. package/lib/server/langfuse-login.mjs +3 -3
  252. package/lib/service-manager.mjs +61 -9
  253. package/lib/session-store.mjs +1 -1
  254. package/lib/setup.mjs +102 -2
  255. package/lib/specialists/prompt-schema.mjs +19 -10
  256. package/lib/specialists/roster.mjs +43 -0
  257. package/lib/specialists/scaffold.mjs +56 -0
  258. package/lib/storage/backend.mjs +6 -6
  259. package/lib/storage/embeddings-local.mjs +6 -3
  260. package/lib/storage/file-lock.mjs +18 -11
  261. package/lib/storage/hybrid-query.mjs +7 -4
  262. package/lib/storage/state-source.mjs +5 -5
  263. package/lib/storage/sync.mjs +2 -3
  264. package/lib/telemetry/rule-calls.mjs +52 -0
  265. package/lib/template-registry.mjs +2 -2
  266. package/lib/templates/visual-requirements.mjs +27 -51
  267. package/lib/test-corpus-inventory.mjs +313 -0
  268. package/lib/uninstall/uninstall.mjs +19 -7
  269. package/lib/update.mjs +12 -0
  270. package/lib/upgrade.mjs +14 -0
  271. package/lib/wireframe.mjs +20 -14
  272. package/lib/worker/run.mjs +17 -4
  273. package/lib/worker/trace.mjs +11 -3
  274. package/package.json +29 -13
  275. package/personas/construct.md +13 -13
  276. package/platforms/claude/settings.template.json +36 -0
  277. package/rules/common/patterns.md +1 -1
  278. package/rules/common/release-gates.md +4 -3
  279. package/scripts/sync-specialists.mjs +187 -60
  280. package/skills/devops/data-engineering.md +1 -1
  281. package/skills/docs/adr-workflow.md +1 -0
  282. package/skills/docs/backlog-proposal-workflow.md +1 -0
  283. package/skills/docs/codebase-research-workflow.md +40 -0
  284. package/skills/docs/customer-profile-workflow.md +1 -0
  285. package/skills/docs/document-ingest-workflow.md +1 -0
  286. package/skills/docs/evidence-ingest-workflow.md +1 -0
  287. package/skills/docs/init-docs.md +2 -2
  288. package/skills/docs/init-project.md +8 -3
  289. package/skills/docs/prd-workflow.md +24 -1
  290. package/skills/docs/prfaq-workflow.md +1 -0
  291. package/skills/docs/product-intelligence-workflow.md +1 -0
  292. package/skills/docs/product-signal-workflow.md +1 -0
  293. package/skills/docs/research-workflow.md +54 -37
  294. package/skills/docs/runbook-workflow.md +1 -0
  295. package/skills/docs/strategy-workflow.md +1 -0
  296. package/skills/docs/user-research-workflow.md +40 -0
  297. package/skills/operating/orchestration-reference.md +1 -1
  298. package/skills/roles/architect.md +5 -0
  299. package/skills/roles/operator.docs.md +4 -0
  300. package/skills/routing.md +4 -2
  301. package/specialists/artifact-manifest.json +480 -0
  302. package/specialists/artifact-manifest.schema.json +53 -0
  303. package/specialists/audit-enrichments.json +454 -0
  304. package/specialists/contracts.json +37 -25
  305. package/specialists/prompts/_shared/validation-contract.md +26 -0
  306. package/specialists/prompts/cx-accessibility.md +21 -2
  307. package/specialists/prompts/cx-ai-engineer.md +24 -1
  308. package/specialists/prompts/cx-architect.md +4 -1
  309. package/specialists/prompts/cx-business-strategist.md +23 -2
  310. package/specialists/prompts/cx-data-analyst.md +22 -1
  311. package/specialists/prompts/cx-data-engineer.md +23 -2
  312. package/specialists/prompts/cx-debugger.md +21 -2
  313. package/specialists/prompts/cx-designer.md +26 -3
  314. package/specialists/prompts/cx-devil-advocate.md +19 -2
  315. package/specialists/prompts/cx-docs-keeper.md +28 -1
  316. package/specialists/prompts/cx-engineer.md +22 -1
  317. package/specialists/prompts/cx-evaluator.md +18 -1
  318. package/specialists/prompts/cx-explorer.md +21 -2
  319. package/specialists/prompts/cx-legal-compliance.md +21 -2
  320. package/specialists/prompts/cx-operations.md +21 -2
  321. package/specialists/prompts/cx-oracle.md +94 -0
  322. package/specialists/prompts/cx-orchestrator.md +20 -1
  323. package/specialists/prompts/cx-platform-engineer.md +25 -2
  324. package/specialists/prompts/cx-product-manager.md +32 -1
  325. package/specialists/prompts/cx-qa.md +24 -2
  326. package/specialists/prompts/cx-rd-lead.md +23 -2
  327. package/specialists/prompts/cx-release-manager.md +23 -2
  328. package/specialists/prompts/cx-researcher.md +22 -2
  329. package/specialists/prompts/cx-reviewer.md +18 -1
  330. package/specialists/prompts/cx-security.md +22 -2
  331. package/specialists/prompts/cx-sre.md +30 -3
  332. package/specialists/prompts/cx-test-automation.md +7 -1
  333. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  334. package/specialists/prompts/cx-ux-researcher.md +21 -2
  335. package/specialists/registry.json +52 -173
  336. package/specialists/tone-profiles.json +42 -0
  337. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  338. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  339. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  340. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  341. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  342. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  343. package/templates/demos/vhs/construct-cockpit.json +24 -0
  344. package/templates/distribution/construct-brand.typ +446 -0
  345. package/templates/distribution/construct-decision.typ +38 -0
  346. package/templates/distribution/construct-deck.html +95 -0
  347. package/templates/distribution/construct-pdf.typ +38 -0
  348. package/templates/distribution/construct-prd.typ +38 -0
  349. package/templates/distribution/construct-research.typ +38 -0
  350. package/templates/distribution/construct-web.html +92 -0
  351. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  352. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  353. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  354. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  355. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  356. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  357. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  358. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  359. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  360. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  361. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  362. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  363. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  364. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  365. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  366. package/templates/distribution/fonts/README.md +36 -0
  367. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  368. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  369. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  370. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  371. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  372. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  373. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  374. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  375. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  376. package/templates/distribution/icons/activity.svg +15 -0
  377. package/templates/distribution/icons/alert-triangle.svg +17 -0
  378. package/templates/distribution/icons/book-open.svg +16 -0
  379. package/templates/distribution/icons/bot.svg +20 -0
  380. package/templates/distribution/icons/brain.svg +22 -0
  381. package/templates/distribution/icons/circle-check.svg +16 -0
  382. package/templates/distribution/icons/clipboard-check.svg +17 -0
  383. package/templates/distribution/icons/cpu.svg +28 -0
  384. package/templates/distribution/icons/database.svg +17 -0
  385. package/templates/distribution/icons/eye.svg +16 -0
  386. package/templates/distribution/icons/file-text.svg +19 -0
  387. package/templates/distribution/icons/gauge.svg +16 -0
  388. package/templates/distribution/icons/git-branch.svg +17 -0
  389. package/templates/distribution/icons/key.svg +17 -0
  390. package/templates/distribution/icons/layers.svg +17 -0
  391. package/templates/distribution/icons/list-checks.svg +19 -0
  392. package/templates/distribution/icons/lock.svg +16 -0
  393. package/templates/distribution/icons/message-square.svg +15 -0
  394. package/templates/distribution/icons/network.svg +19 -0
  395. package/templates/distribution/icons/route.svg +17 -0
  396. package/templates/distribution/icons/scale.svg +19 -0
  397. package/templates/distribution/icons/search.svg +16 -0
  398. package/templates/distribution/icons/send.svg +16 -0
  399. package/templates/distribution/icons/server.svg +18 -0
  400. package/templates/distribution/icons/shield-check.svg +16 -0
  401. package/templates/distribution/icons/users.svg +18 -0
  402. package/templates/distribution/icons/webhook.svg +17 -0
  403. package/templates/distribution/icons/workflow.svg +17 -0
  404. package/templates/distribution/icons/wrench.svg +15 -0
  405. package/templates/distribution/run.mjs +18 -1
  406. package/templates/docs/adr.md +7 -0
  407. package/templates/docs/construct_guide.md +10 -10
  408. package/templates/docs/customer-profile.md +4 -0
  409. package/templates/docs/one-pager.md +4 -0
  410. package/templates/docs/postmortem.md +31 -0
  411. package/templates/docs/prd-platform.md +19 -0
  412. package/templates/docs/prd.md +15 -0
  413. package/templates/docs/prfaq.md +7 -0
  414. package/templates/docs/qa-strategy.md +103 -0
  415. package/templates/docs/research-brief.md +8 -0
  416. package/templates/docs/rfc-platform.md +12 -0
  417. package/templates/docs/test-plan.md +7 -0
  418. package/lib/ingest/chunker.mjs +0 -94
  419. package/lib/ingest/pipeline.mjs +0 -53
  420. package/lib/ingest/store.mjs +0 -82
  421. package/lib/mode-commands.mjs +0 -122
  422. package/lib/policy/unified-gates.mjs +0 -96
  423. package/lib/profiles/validate-custom.mjs +0 -114
  424. package/lib/services/telemetry-backend.mjs +0 -177
  425. package/lib/storage/fusion.mjs +0 -95
@@ -18,6 +18,7 @@
18
18
  import { resolveContractChain } from './specialist-contracts.mjs';
19
19
  import { verifyRoute } from './intent-classifier.mjs';
20
20
  import { ownerForEvent, ownerForDoc, evaluateWatchConditions, knownDocTypes } from './orchestration/routing-tables.mjs';
21
+ import { getArtifactEntry } from './artifact-manifest.mjs';
21
22
 
22
23
  export const EXECUTION_TRACKS = {
23
24
  immediate: 'immediate',
@@ -653,6 +654,19 @@ export function formatOverlayTrace(roleFlavors = {}, request = '') {
653
654
  return `Overlays: ${parts.join(', ')}`;
654
655
  }
655
656
 
657
+ function resolveArtifactReviewRequirements(docAuthoring) {
658
+ if (!docAuthoring?.docType) {
659
+ return { requiredReviewers: [], optionalReviewers: [], releaseGate: null };
660
+ }
661
+ const entry = getArtifactEntry(docAuthoring.docType);
662
+ const gate = entry?.releaseGate ?? {};
663
+ return {
664
+ requiredReviewers: gate.requiredReviewers ?? [],
665
+ optionalReviewers: gate.optionalReviewers ?? [],
666
+ releaseGate: gate,
667
+ };
668
+ }
669
+
656
670
  export function routeRequest(options = {}) {
657
671
  const intent = classifyIntent(options.request);
658
672
  const riskFlags = detectRiskFlags(options.request);
@@ -679,6 +693,11 @@ export function routeRequest(options = {}) {
679
693
  if (framingChallenge.required && !specialists.includes('cx-devil-advocate')) {
680
694
  specialists = ['cx-devil-advocate', ...specialists];
681
695
  }
696
+
697
+ const artifactReview = resolveArtifactReviewRequirements(docAuthoring);
698
+ for (const reviewer of artifactReview.requiredReviewers) {
699
+ if (!specialists.includes(reviewer)) specialists = [...specialists, reviewer];
700
+ }
682
701
  // Pre-dispatch specialists that surface concerns before architecture locks in.
683
702
  // Order matters: business framing → R&D validation → legal/compliance review,
684
703
  // each prepended so it runs earliest in the chain.
@@ -715,6 +734,7 @@ export function routeRequest(options = {}) {
715
734
  framingChallenge,
716
735
  externalResearch,
717
736
  docAuthoring,
737
+ artifactReview,
718
738
  });
719
739
 
720
740
  const route = {
@@ -731,6 +751,7 @@ export function routeRequest(options = {}) {
731
751
  docAuthoring,
732
752
  externalResearch,
733
753
  framingChallenge,
754
+ artifactReview,
734
755
  contractChain,
735
756
  dispatchPlan: buildDispatchPlan({ track, intent, specialists }),
736
757
  dispatchSummary: buildDispatchSummary({ specialists, reasons }),
@@ -754,3 +775,32 @@ export function routeRequestVerified(options = {}) {
754
775
  const route = routeRequest(options);
755
776
  return verifyRoute(route, { request: options.request, modelCaller: options.modelCaller });
756
777
  }
778
+
779
+ /**
780
+ * Build the construct→orchestrator contract packet (specialists/contracts.json).
781
+ * Callers must pass this object to agent_contract — not a bare goal string.
782
+ */
783
+ export function buildConstructToOrchestratorPacket(options = {}) {
784
+ const request = String(options.request || '');
785
+ const route = options.route || routeRequest({
786
+ request,
787
+ fileCount: options.fileCount ?? 0,
788
+ moduleCount: options.moduleCount ?? 0,
789
+ introducesContract: options.introducesContract ?? false,
790
+ explicitDrive: options.explicitDrive ?? false,
791
+ });
792
+ if (route.track === EXECUTION_TRACKS.immediate) return null;
793
+
794
+ const goal = String(options.goal || request || '').trim() || 'User-requested work';
795
+ const acceptanceCriteria = Array.isArray(options.acceptanceCriteria) && options.acceptanceCriteria.length
796
+ ? options.acceptanceCriteria
797
+ : ['Dispatch plan emitted with specialists in sequence', 'Acceptance criteria verified before close'];
798
+
799
+ return {
800
+ goal,
801
+ intent: route.intent,
802
+ workCategory: route.workCategory,
803
+ riskFlags: route.riskFlags || {},
804
+ acceptanceCriteria,
805
+ };
806
+ }
package/lib/parity.mjs CHANGED
@@ -104,6 +104,16 @@ function diffSets(expected, actual) {
104
104
  return { missing, extra };
105
105
  }
106
106
 
107
+ // Construct ensures its own MCP servers are present; it does not own the user's
108
+ // full editor MCP list. A non-registry server the user added (e.g. playwright) is
109
+ // not drift — only a MISSING registry server is, which is a real sync regression.
110
+ // A rename still surfaces, since the new name registers as missing. Agent surfaces
111
+ // keep extra-detection (legacy cx-* cleanup), so this applies to MCP kinds only.
112
+
113
+ function mcpStatus(missing) {
114
+ return missing.length === 0 ? 'ok' : 'drift';
115
+ }
116
+
107
117
  function checkFileSurface({ surface, kind, dir, extension, expected }) {
108
118
  if (!fs.existsSync(dir)) return { surface, kind, status: 'absent', dir };
109
119
  const actual = fs.readdirSync(dir)
@@ -245,7 +255,7 @@ function checkVSCode(registry, { homeDir = os.homedir() } = {}) {
245
255
  return {
246
256
  surface: 'vscode',
247
257
  kind: 'mcps',
248
- status: missing.length === 0 && extra.length === 0 ? 'ok' : 'drift',
258
+ status: mcpStatus(missing),
249
259
  paths,
250
260
  expectedCount: expected.length,
251
261
  actualCount: actual.length,
@@ -269,7 +279,7 @@ function checkCursor(registry, { homeDir = os.homedir() } = {}) {
269
279
  return {
270
280
  surface: 'cursor',
271
281
  kind: 'mcps',
272
- status: missing.length === 0 && extra.length === 0 ? 'ok' : 'drift',
282
+ status: mcpStatus(missing),
273
283
  file,
274
284
  expectedCount: expected.length,
275
285
  actualCount: actual.length,
@@ -278,6 +288,90 @@ function checkCursor(registry, { homeDir = os.homedir() } = {}) {
278
288
  };
279
289
  }
280
290
 
291
+ function checkProjectMcp(registry, { projectDir, surface, relPath, configKey }) {
292
+ const file = path.join(projectDir, relPath);
293
+ if (!fs.existsSync(file)) {
294
+ return { surface, scope: 'project', kind: 'mcps', status: 'absent', file };
295
+ }
296
+ let config;
297
+ try {
298
+ config = readJsoncFile(file);
299
+ } catch (err) {
300
+ return { surface, scope: 'project', kind: 'mcps', status: 'unreadable', file, error: err.message };
301
+ }
302
+ const servers = config[configKey] ?? {};
303
+ const expected = Object.keys(registry.mcpServers ?? {}).filter((id) => id !== 'memory');
304
+ const actual = Object.keys(servers).filter((id) => id !== 'memory');
305
+ const { missing, extra } = diffSets(expected, actual);
306
+ return {
307
+ surface,
308
+ scope: 'project',
309
+ kind: 'mcps',
310
+ status: mcpStatus(missing),
311
+ file,
312
+ expectedCount: expected.length,
313
+ actualCount: actual.length,
314
+ missing,
315
+ extra,
316
+ };
317
+ }
318
+
319
+ function checkProjectCursorRules({ projectDir }) {
320
+ const pointer = path.join(projectDir, '.cursor', 'rules', 'construct.mdc');
321
+ if (!fs.existsSync(pointer)) {
322
+ return { surface: 'cursor', scope: 'project', kind: 'rules', status: 'absent', file: pointer };
323
+ }
324
+ const text = fs.readFileSync(pointer, 'utf8');
325
+ const stale = !text.includes('Generated by construct sync');
326
+ return {
327
+ surface: 'cursor',
328
+ scope: 'project',
329
+ kind: 'rules',
330
+ status: stale ? 'drift' : 'ok',
331
+ file: pointer,
332
+ stale,
333
+ };
334
+ }
335
+
336
+ function checkProjectClaudeAgent({ projectDir, registry }) {
337
+ const expected = registry.orchestrator?.name || 'construct';
338
+ const file = path.join(projectDir, '.claude', 'agents', `${expected}.md`);
339
+ if (!fs.existsSync(file)) {
340
+ return { surface: 'claude', scope: 'project', kind: 'agents', status: 'absent', file, expected: [expected] };
341
+ }
342
+ return { surface: 'claude', scope: 'project', kind: 'agents', status: 'ok', file, expected: [expected], actual: [expected] };
343
+ }
344
+
345
+ /**
346
+ * Project-scoped adapter parity for Construct-managed repos (.cx/ or .construct/).
347
+ * Checks `.cursor/mcp.json`, `.vscode/mcp.json`, front-door rule, and construct agent.
348
+ */
349
+ export function checkProjectParity({ rootDir = ROOT_DIR, projectDir = rootDir } = {}) {
350
+ const cxPresent = fs.existsSync(path.join(projectDir, '.cx')) || fs.existsSync(path.join(projectDir, '.construct'));
351
+ if (!cxPresent) {
352
+ return { ok: true, skipped: true, surfaces: [], summary: ['project: not a Construct project'] };
353
+ }
354
+ const registry = loadRegistry(rootDir);
355
+ const surfaces = [
356
+ checkProjectClaudeAgent({ projectDir, registry }),
357
+ checkProjectMcp(registry, { projectDir, surface: 'cursor', relPath: '.cursor/mcp.json', configKey: 'mcpServers' }),
358
+ checkProjectMcp(registry, { projectDir, surface: 'vscode', relPath: '.vscode/mcp.json', configKey: 'servers' }),
359
+ checkProjectCursorRules({ projectDir }),
360
+ ];
361
+ const ok = surfaces.every((s) => s.status === 'ok' || s.status === 'absent');
362
+ const summary = surfaces.map((s) => {
363
+ if (s.status === 'absent') return `${s.surface} (project): missing ${s.kind}`;
364
+ if (s.status === 'unreadable') return `${s.surface} (project): unreadable (${s.error})`;
365
+ if (s.status === 'ok') return `${s.surface} (project): ok`;
366
+ if (s.stale) return `${s.surface} (project): stale construct.mdc — run \`npm run adapters\``;
367
+ const parts = [];
368
+ if (s.missing?.length) parts.push(`missing: ${s.missing.join(', ')}`);
369
+ if (s.extra?.length) parts.push(`extra: ${s.extra.join(', ')}`);
370
+ return `${s.surface} (project): drift — ${parts.join(' · ')}`;
371
+ });
372
+ return { ok, skipped: false, surfaces, summary };
373
+ }
374
+
281
375
  // Names a v1.0.10 install would have populated at user scope (claude, codex,
282
376
  // copilot) before the two-tier sync contract moved specialists to project
283
377
  // scope. Pulled from the live registry so the roster auto-updates as new
@@ -0,0 +1,66 @@
1
+ /**
2
+ * lib/persona-sections.mjs — Capability-tiered persona section rendering.
3
+ *
4
+ * A persona is authored once. Each `## ` section carries an inline `<!-- cx:prio=N -->`
5
+ * marker on its heading; the preamble before the first heading is always prio 1
6
+ * (identity + anti-fabrication). Small local models follow large multi-instruction
7
+ * prompts poorly — instruction-following degrades well before the context window fills —
8
+ * so a weak model receives only the sections it can actually comply with. Tiers:
9
+ * floor (prio 1, must-keep), mid (prio <= 2), full (all). The marker is stripped from the
10
+ * emitted text. Untagged `## ` sections default to prio 2 so authoring omissions degrade
11
+ * gracefully rather than vanishing or forcing a floor model to read everything.
12
+ */
13
+
14
+ const TIER_MAX_PRIO = Object.freeze({ floor: 1, mid: 2, full: 3 });
15
+ const PRIO_MARKER = /<!--\s*cx:prio=(\d+)\s*-->/;
16
+ const PRIO_MARKER_GLOBAL = /[ \t]*<!--\s*cx:prio=\d+\s*-->/g;
17
+
18
+ // The markers are authoring metadata; no emitted prompt (full or tiered) may carry them.
19
+ // Tiered render strips them via section parsing; full render (composePrompt) calls this.
20
+
21
+ export function stripSectionMarkers(text) {
22
+ return String(text || "").replace(PRIO_MARKER_GLOBAL, "");
23
+ }
24
+
25
+ // `## ` (exactly two hashes) is the only section boundary; `### ` subheadings stay with
26
+ // their parent section. The preamble (before the first `## `) is implicit prio 1.
27
+
28
+ export function parsePersonaSections(body) {
29
+ const lines = String(body || "").split("\n");
30
+ const sections = [];
31
+ let current = { heading: null, prio: 1, lines: [] };
32
+
33
+ const flush = () => {
34
+ const content = current.lines.join("\n").trim();
35
+ if (content || current.heading) {
36
+ sections.push({ heading: current.heading, prio: current.prio, content });
37
+ }
38
+ };
39
+
40
+ for (const line of lines) {
41
+ if (line.startsWith("## ")) {
42
+ flush();
43
+ const marker = line.match(PRIO_MARKER);
44
+ const prio = marker ? Number(marker[1]) : 2;
45
+ const heading = line.replace(PRIO_MARKER, "").trimEnd();
46
+ current = { heading, prio, lines: [heading] };
47
+ } else {
48
+ current.lines.push(line);
49
+ }
50
+ }
51
+ flush();
52
+ return sections;
53
+ }
54
+
55
+ export function renderPersonaForTier(body, tier = "full") {
56
+ const maxPrio = TIER_MAX_PRIO[tier] ?? TIER_MAX_PRIO.full;
57
+ return parsePersonaSections(body)
58
+ .filter((section) => section.prio <= maxPrio)
59
+ .map((section) => stripSectionMarkers(section.content))
60
+ .filter(Boolean)
61
+ .join("\n\n");
62
+ }
63
+
64
+ export function personaTierMaxPrio(tier) {
65
+ return TIER_MAX_PRIO[tier] ?? TIER_MAX_PRIO.full;
66
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * lib/playwright-demo.mjs — workspace-agnostic Playwright demo recording.
3
+ *
4
+ * Spawns `npx playwright test --config <config> <spec>` with env passthrough for
5
+ * artifact reveal, collects video output, and transcodes webm→mp4 when ffmpeg exists.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { spawnSync } from 'node:child_process';
11
+ import { sanitizeNpmSpawnEnv } from './npm-spawn-env.mjs';
12
+
13
+ export function resolvePlaywrightPackage(workspace, repoRoot) {
14
+ const candidates = [
15
+ path.join(workspace, 'node_modules', '@playwright', 'test'),
16
+ path.join(repoRoot, 'node_modules', '@playwright', 'test'),
17
+ ];
18
+ return candidates.find((p) => fs.existsSync(p)) || null;
19
+ }
20
+
21
+ export function resolvePlaywrightConfig(workspace) {
22
+ const candidates = ['playwright.config.mjs', 'playwright.config.ts'].map((f) => path.join(workspace, f));
23
+ return candidates.find((p) => fs.existsSync(p)) || null;
24
+ }
25
+
26
+ export function detectPlaywrightDemo({ workspace, cwd = process.cwd(), repoRoot = cwd } = {}) {
27
+ const root = repoRoot || cwd;
28
+ const resolvedWorkspace = path.resolve(root, workspace || '.');
29
+ const missing = [];
30
+ if (!fs.existsSync(path.join(resolvedWorkspace, 'package.json'))) {
31
+ missing.push(`package.json in ${workspace || '.'}`);
32
+ }
33
+ const config = resolvePlaywrightConfig(resolvedWorkspace);
34
+ if (!config) missing.push(`playwright.config.mjs in ${workspace || '.'}`);
35
+ if (!resolvePlaywrightPackage(resolvedWorkspace, root)) {
36
+ missing.push('@playwright/test (npm install in workspace)');
37
+ }
38
+ return {
39
+ ok: true,
40
+ present: missing.length === 0,
41
+ workspace: resolvedWorkspace,
42
+ config,
43
+ missing,
44
+ message: missing.length === 0
45
+ ? 'Playwright demo tooling ready'
46
+ : `Install Playwright demo deps: ${missing.join('; ')}`,
47
+ };
48
+ }
49
+
50
+ export function locateFfmpeg() {
51
+ const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['ffmpeg'], { encoding: 'utf8' });
52
+ if (result.status !== 0) return null;
53
+ return result.stdout.trim().split('\n')[0] || null;
54
+ }
55
+
56
+ export function collectVideoFiles(dirs) {
57
+ const collected = [];
58
+ function walk(dir) {
59
+ if (!dir || !fs.existsSync(dir)) return;
60
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
61
+ const p = path.join(dir, ent.name);
62
+ if (ent.isDirectory()) walk(p);
63
+ else if (/\.(webm|mp4)$/.test(ent.name)) collected.push(p);
64
+ }
65
+ }
66
+ for (const dir of dirs) walk(dir);
67
+ return collected;
68
+ }
69
+
70
+ export function newestVideo(paths) {
71
+ if (!paths.length) return null;
72
+ return paths
73
+ .map((p) => ({ p, mtime: fs.statSync(p).mtimeMs }))
74
+ .sort((a, b) => b.mtime - a.mtime)[0].p;
75
+ }
76
+
77
+ export function transcodeWebmToMp4(src, dest) {
78
+ const ffmpeg = locateFfmpeg();
79
+ if (!ffmpeg) return { ok: false, message: 'ffmpeg not on PATH' };
80
+ const result = spawnSync(ffmpeg, [
81
+ '-y', '-i', src,
82
+ '-c:v', 'libx264',
83
+ '-pix_fmt', 'yuv420p',
84
+ '-movflags', '+faststart',
85
+ dest,
86
+ ], { encoding: 'utf8', timeout: 180_000 });
87
+ if (result.status !== 0) {
88
+ return {
89
+ ok: false,
90
+ message: `ffmpeg failed (exit ${result.status}): ${(result.stderr || '').trim().slice(0, 300)}`,
91
+ };
92
+ }
93
+ return { ok: true, outputPath: dest };
94
+ }
95
+
96
+ export function finalizeDemoVideo({ sourcePath, outputPath, format = 'mp4' }) {
97
+ if (!sourcePath || !fs.existsSync(sourcePath)) {
98
+ return { ok: false, message: 'No video artifact produced' };
99
+ }
100
+ const target = outputPath || sourcePath;
101
+ fs.mkdirSync(path.dirname(target), { recursive: true });
102
+
103
+ if (format === 'mp4' && sourcePath.endsWith('.webm')) {
104
+ const transcode = transcodeWebmToMp4(sourcePath, target);
105
+ if (!transcode.ok) return transcode;
106
+ return { ok: true, outputPath: target, format: 'mp4', engine: 'playwright+ffmpeg' };
107
+ }
108
+
109
+ if (path.resolve(sourcePath) !== path.resolve(target)) {
110
+ fs.copyFileSync(sourcePath, target);
111
+ }
112
+ return {
113
+ ok: true,
114
+ outputPath: target,
115
+ format: path.extname(target).slice(1) || format,
116
+ engine: 'playwright',
117
+ };
118
+ }
119
+
120
+ export function buildArtifactRevealEnv(recording, {
121
+ repoRoot,
122
+ cwd = repoRoot,
123
+ artifactDir = null,
124
+ artifactFile = null,
125
+ env = process.env,
126
+ } = {}) {
127
+ const reveal = recording?.artifactReveal;
128
+ const out = { ...env };
129
+ if (!reveal && !artifactDir && !artifactFile) return out;
130
+
131
+ const file = artifactFile || reveal?.file || reveal?.path || '';
132
+ const staticDir = artifactDir
133
+ || (reveal?.staticDir ? path.resolve(repoRoot || cwd, reveal.staticDir) : '');
134
+
135
+ if (staticDir) out.CONSTRUCT_DEMO_ARTIFACT_DIR = staticDir;
136
+ if (file) out.DEMO_ARTIFACT_FILE = file;
137
+ if (reveal?.mode) out.DEMO_ARTIFACT_REVEAL_MODE = reveal.mode;
138
+ if (recording?.baseUrl) out.BASE_URL = recording.baseUrl;
139
+ if (recording?.webServer && !recording.skipWebServer) {
140
+ out.DEMO_WEB_SERVER_COMMAND = recording.webServer.command;
141
+ out.DEMO_WEB_SERVER_URL = recording.webServer.url;
142
+ out.DEMO_WEB_SERVER_CWD = path.resolve(repoRoot || cwd, recording.webServer.cwd || recording.workspace || '.');
143
+ out.DEMO_WEB_SERVER_TIMEOUT = String(recording.webServer.timeout || 120_000);
144
+ }
145
+ if (recording?.skipWebServer) out.SKIP_WEBSERVER = '1';
146
+ return out;
147
+ }
148
+
149
+ export function resolveRecordingPaths(recording, { repoRoot, cwd = repoRoot } = {}) {
150
+ const root = repoRoot || cwd;
151
+ const workspace = path.resolve(root, recording.workspace || '.');
152
+ const spec = path.isAbsolute(recording.spec)
153
+ ? recording.spec
154
+ : path.resolve(root, recording.spec);
155
+ const config = recording.playwrightConfig
156
+ ? (path.isAbsolute(recording.playwrightConfig)
157
+ ? recording.playwrightConfig
158
+ : path.resolve(root, recording.playwrightConfig))
159
+ : resolvePlaywrightConfig(workspace);
160
+ const outputDir = recording.output?.path
161
+ ? path.dirname(path.resolve(root, recording.output.path))
162
+ : path.join(cwd, '.cx', 'demos');
163
+ return { workspace, spec, config, outputDir };
164
+ }
165
+
166
+ export function recordPlaywrightDemo(recording, {
167
+ cwd = process.cwd(),
168
+ repoRoot,
169
+ outputDir = null,
170
+ outputPath = null,
171
+ format = null,
172
+ artifactDir = null,
173
+ artifactFile = null,
174
+ env = process.env,
175
+ timeout = null,
176
+ } = {}) {
177
+ const root = repoRoot || cwd;
178
+ const name = recording.name || path.basename(recording.spec || '', '.spec.ts');
179
+ const { workspace, spec, config, outputDir: defaultOutDir } = resolveRecordingPaths(recording, { repoRoot: root, cwd });
180
+ const detection = detectPlaywrightDemo({ workspace, repoRoot: root, cwd });
181
+ if (!detection.present) {
182
+ return { ok: false, name, missing: detection.missing, message: detection.message };
183
+ }
184
+ if (!config || !fs.existsSync(config)) {
185
+ return { ok: false, name, message: `Playwright config not found for workspace ${recording.workspace || '.'}` };
186
+ }
187
+ if (!fs.existsSync(spec)) {
188
+ return { ok: false, name, message: `Demo spec not found: ${spec}` };
189
+ }
190
+
191
+ const outDir = outputDir || defaultOutDir;
192
+ fs.mkdirSync(outDir, { recursive: true });
193
+
194
+ const outFormat = format || recording.output?.format || 'mp4';
195
+ const destPath = outputPath
196
+ || (recording.output?.path ? path.resolve(root, recording.output.path) : path.join(outDir, `${name}.${outFormat}`));
197
+
198
+ const spawnEnv = buildArtifactRevealEnv(recording, {
199
+ repoRoot: root,
200
+ cwd,
201
+ artifactDir,
202
+ artifactFile,
203
+ env: {
204
+ ...env,
205
+ DEMO_OUTPUT_DIR: outDir,
206
+ CONSTRUCT_DEMO: name,
207
+ CI: env.CI || '',
208
+ },
209
+ });
210
+
211
+ const args = ['playwright', 'test', '--config', config, spec];
212
+ if (recording.project) args.push(`--project=${recording.project}`);
213
+
214
+ const spawnTimeout = timeout || recording.timeout || 600_000;
215
+ const result = spawnSync('npx', args, {
216
+ cwd: workspace,
217
+ encoding: 'utf8',
218
+ env: sanitizeNpmSpawnEnv(spawnEnv),
219
+ timeout: spawnTimeout,
220
+ });
221
+
222
+ if (result.status !== 0) {
223
+ const detail = (result.stderr || result.stdout || result.error?.message || '').trim().slice(0, 500);
224
+ const exitLabel = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
225
+ return {
226
+ ok: false,
227
+ name,
228
+ message: `Playwright demo failed (${exitLabel})${detail ? `: ${detail}` : ''}`,
229
+ };
230
+ }
231
+
232
+ const videos = collectVideoFiles([
233
+ path.join(workspace, 'test-results'),
234
+ outDir,
235
+ ]);
236
+ const newest = newestVideo(videos);
237
+ const finalized = finalizeDemoVideo({ sourcePath: newest, outputPath: destPath, format: outFormat });
238
+ if (!finalized.ok) {
239
+ return { ok: false, name, message: finalized.message };
240
+ }
241
+
242
+ return {
243
+ ok: true,
244
+ name,
245
+ outputDir: outDir,
246
+ outputPath: finalized.outputPath,
247
+ format: finalized.format,
248
+ engine: finalized.engine,
249
+ videos: videos.map((p) => path.basename(p)),
250
+ message: `Recorded Playwright demo to ${finalized.outputPath}`,
251
+ };
252
+ }
@@ -87,7 +87,10 @@ instead of letting it turn into a historical log. Durable task status belongs in
87
87
 
88
88
  > Required project state. Keep this file current enough that a new session can resume work quickly.
89
89
 
90
- ## Active Work
90
+ ## What was in progress
91
+
92
+ ## Open issues
93
+ None
91
94
 
92
95
  ## Recent Decisions
93
96
 
@@ -24,12 +24,16 @@ import { buildContextDigest, readContextState } from './context-state.mjs';
24
24
  import {
25
25
  MODEL_OPERATING_PROFILES,
26
26
  resolveExecutionContractModelMetadata,
27
- resolveModelOperatingProfile,
28
27
  selectModelTierForWorkCategory,
29
28
  } from './model-router.mjs';
29
+ import {
30
+ resolveExecutionCapabilityProfile,
31
+ operatingProfileIdFromProfile,
32
+ } from './models/execution-capability-profile.mjs';
30
33
  import { listObservations } from './observation-store.mjs';
31
34
  import { routeRequest } from './orchestration-policy.mjs';
32
35
  import { resolvePromptEntry, resolvePromptMetadata } from './prompt-metadata.mjs';
36
+ import { stripSectionMarkers } from './persona-sections.mjs';
33
37
  import { readRoleFile } from './role-preload.mjs';
34
38
  import { estimateTokens, estimatePromptTokens, estimateTokensSync } from './token-engine.js';
35
39
  import { cxDir } from './paths.mjs';
@@ -207,12 +211,13 @@ export function composePrompt(agentName, {
207
211
 
208
212
  const metadata = resolvePromptMetadata(agentName, { rootDir, registry });
209
213
  const fragments = [];
210
- const selectedProfile = resolveModelOperatingProfile({
214
+ const executionProfile = resolveExecutionCapabilityProfile({
215
+ model: executionContractModel?.selectedModel ?? modelId,
211
216
  envValues: executionContractModel?.profile ? { CONSTRUCT_MODEL_PROFILE: executionContractModel.profile.id } : {},
212
- selectedModel: executionContractModel?.selectedModel ?? modelId,
213
217
  });
218
+ const selectedProfile = MODEL_OPERATING_PROFILES[operatingProfileIdFromProfile(executionProfile)];
214
219
 
215
- fragments.push({ type: 'core', priority: PRIORITY['core'], label: entry.name, content: readPromptBody(entry.promptFile, rootDir), tokenBudget: null });
220
+ fragments.push({ type: 'core', priority: PRIORITY['core'], label: entry.name, content: stripSectionMarkers(readPromptBody(entry.promptFile, rootDir)), tokenBudget: null });
216
221
 
217
222
  const shortName = String(agentName).replace(/^cx-/, '');
218
223
  const flavorMapping = AGENT_FLAVOR_MAP[shortName];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * lib/prompt-validation-contract.mjs — Inline shared validation contract at sync time.
3
+ *
4
+ * Prepends specialists/prompts/_shared/validation-contract.md to every cx-* prompt
5
+ * so assume-nothing discipline is present at runtime without 29 copy-paste edits.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ const MARKER = '<!-- cx:validation-contract -->';
12
+
13
+ export function readValidationContract(root) {
14
+ const p = path.join(root, 'specialists', 'prompts', '_shared', 'validation-contract.md');
15
+ if (!fs.existsSync(p)) return null;
16
+ return fs.readFileSync(p, 'utf8').trim();
17
+ }
18
+
19
+ export function inlineValidationContract(prompt, root, agentName = '(unknown)') {
20
+ if (!prompt || prompt.includes(MARKER)) return prompt;
21
+ const block = readValidationContract(root);
22
+ if (!block) return prompt;
23
+ return `${MARKER}\n\n${block}\n\n${prompt}`;
24
+ }
@@ -41,6 +41,62 @@ function resolveAdapterKey(modelId) {
41
41
  return 'generic';
42
42
  }
43
43
 
44
+ // Declared adapter defaults for sync hot paths when the 24h probe cache is cold.
45
+ // Matches the static `capabilities()` exports in provider-capabilities-*.js.
46
+
47
+ const SYNC_ADAPTER_CAPS = {
48
+ anthropic: {
49
+ cacheControl: true,
50
+ cacheMechanism: 'annotation',
51
+ cacheTTL: { '5m': 300_000, '1h': 1_200_000 },
52
+ structuredOutput: true,
53
+ maxContextWindow: 200_000,
54
+ tokenRatio: 3.5,
55
+ annotationFormat: 'anthropic',
56
+ annotationHeaders: { 'anthropic-version': '2024-10-22' },
57
+ },
58
+ google: {
59
+ cacheControl: true,
60
+ cacheMechanism: 'resource',
61
+ cacheTTL: { '5m': null, '1h': 1_200_000 },
62
+ structuredOutput: true,
63
+ maxContextWindow: 1_000_000,
64
+ tokenRatio: 4,
65
+ annotationFormat: 'google',
66
+ annotationHeaders: null,
67
+ },
68
+ openai: {
69
+ cacheControl: false,
70
+ cacheMechanism: 'automatic',
71
+ cacheTTL: null,
72
+ structuredOutput: true,
73
+ maxContextWindow: 128_000,
74
+ tokenRatio: 4,
75
+ annotationFormat: 'openai',
76
+ annotationHeaders: null,
77
+ },
78
+ deepseek: {
79
+ cacheControl: false,
80
+ cacheMechanism: 'none',
81
+ cacheTTL: null,
82
+ structuredOutput: true,
83
+ maxContextWindow: 128_000,
84
+ tokenRatio: 4,
85
+ annotationFormat: 'none',
86
+ annotationHeaders: null,
87
+ },
88
+ generic: {
89
+ cacheControl: false,
90
+ cacheMechanism: 'none',
91
+ cacheTTL: null,
92
+ structuredOutput: false,
93
+ maxContextWindow: 200_000,
94
+ tokenRatio: 4,
95
+ annotationFormat: 'none',
96
+ annotationHeaders: null,
97
+ },
98
+ };
99
+
44
100
  function readCapabilityCache() {
45
101
  try {
46
102
  if (!existsSync(CAPABILITY_CACHE_PATH)) return {};
@@ -103,17 +159,7 @@ export function resolveProviderCapabilitiesSync(modelId) {
103
159
  // Check cache first (keyed by adapter type, not full model ID)
104
160
  if (cache[adapterKey]) return cache[adapterKey];
105
161
 
106
- // Return generic synchronously (can't load adapter in sync context)
107
- return {
108
- cacheControl: false,
109
- cacheMechanism: 'none',
110
- cacheTTL: null,
111
- structuredOutput: false,
112
- maxContextWindow: 200000,
113
- tokenRatio: 4,
114
- annotationFormat: 'none',
115
- annotationHeaders: null,
116
- };
162
+ return SYNC_ADAPTER_CAPS[adapterKey] || SYNC_ADAPTER_CAPS.generic;
117
163
  }
118
164
 
119
165
  /**