@geraldmaron/construct 1.1.1 → 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 (408) hide show
  1. package/README.md +29 -19
  2. package/bin/construct +311 -81
  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 +1 -1
  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 +129 -15
  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 +598 -140
  115. package/lib/diagram-export.mjs +192 -0
  116. package/lib/diagram.mjs +13 -11
  117. package/lib/docs-verify.mjs +25 -7
  118. package/lib/doctor/index.mjs +3 -1
  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/document-export.mjs +360 -36
  124. package/lib/document-extract.mjs +44 -4
  125. package/lib/document-ingest.mjs +29 -20
  126. package/lib/embed/cli.mjs +2 -2
  127. package/lib/embed/daemon.mjs +18 -46
  128. package/lib/embed/worker.mjs +2 -2
  129. package/lib/embedded-contract/model-resolve.mjs +10 -9
  130. package/lib/env-config.mjs +26 -0
  131. package/lib/evals/dataset.mjs +137 -0
  132. package/lib/evals/gates.mjs +175 -0
  133. package/lib/graph/build-co-change.mjs +54 -0
  134. package/lib/graph/build-from-registry.mjs +136 -0
  135. package/lib/graph/build-import-graph.mjs +153 -0
  136. package/lib/graph/cli.mjs +110 -0
  137. package/lib/graph/impact-cli.mjs +89 -0
  138. package/lib/graph/impact.mjs +108 -0
  139. package/lib/graph/staleness.mjs +44 -0
  140. package/lib/graph/store.mjs +172 -0
  141. package/lib/health-check.mjs +90 -76
  142. package/lib/hooks/artifact-release-gate.mjs +43 -0
  143. package/lib/hooks/brand-prose-lint.mjs +38 -0
  144. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  145. package/lib/hooks/session-optimize.mjs +84 -207
  146. package/lib/hooks/session-start.mjs +4 -5
  147. package/lib/host-disposition.mjs +7 -0
  148. package/lib/improvement/cli.mjs +189 -0
  149. package/lib/improvement/controller.mjs +137 -0
  150. package/lib/improvement/proposal.mjs +120 -0
  151. package/lib/improvement/specialist-loop.mjs +192 -0
  152. package/lib/improvement/store.mjs +89 -0
  153. package/lib/improvement/surface.mjs +219 -0
  154. package/lib/ingest-tooling.mjs +97 -0
  155. package/lib/init/doc-lanes.mjs +165 -0
  156. package/lib/init-docs.mjs +31 -178
  157. package/lib/init-unified.mjs +27 -97
  158. package/lib/init-update-guide.mjs +102 -0
  159. package/lib/init-update.mjs +32 -1
  160. package/lib/init.mjs +8 -0
  161. package/lib/install/desktop-binary-download.mjs +85 -0
  162. package/lib/install/legacy-global-cleanup.mjs +189 -0
  163. package/lib/intake/daemon.mjs +2 -0
  164. package/lib/intake/git-queue.mjs +9 -8
  165. package/lib/intake/queue.mjs +2 -1
  166. package/lib/intake/session-prelude.mjs +90 -1
  167. package/lib/libreoffice-export.mjs +97 -0
  168. package/lib/logging/rotate.mjs +9 -0
  169. package/lib/maintenance/docker-reclaim.mjs +206 -0
  170. package/lib/mcp/external-schema-cost.mjs +74 -0
  171. package/lib/mcp/server.mjs +84 -9
  172. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  173. package/lib/mcp/tool-budget.mjs +55 -4
  174. package/lib/mcp/tools/document.mjs +18 -4
  175. package/lib/mcp/tools/skills.mjs +32 -3
  176. package/lib/mcp/tools/workflow.mjs +13 -1
  177. package/lib/model-free-selector.mjs +18 -0
  178. package/lib/model-registry.mjs +13 -229
  179. package/lib/model-router.mjs +373 -92
  180. package/lib/models/behavior-matrix.mjs +289 -0
  181. package/lib/models/catalog.mjs +209 -0
  182. package/lib/models/execution-capability-profile.mjs +196 -0
  183. package/lib/models/execution-policy.mjs +307 -0
  184. package/lib/models/provider-poll.mjs +383 -0
  185. package/lib/npm-spawn-env.mjs +17 -0
  186. package/lib/ollama/installed-models.mjs +129 -0
  187. package/lib/oracle/actions.mjs +187 -0
  188. package/lib/oracle/artifact-gate.mjs +99 -0
  189. package/lib/oracle/cli.mjs +204 -0
  190. package/lib/oracle/daemon-entry.mjs +14 -0
  191. package/lib/oracle/dispatch.mjs +81 -0
  192. package/lib/oracle/execute.mjs +143 -0
  193. package/lib/oracle/gaps.mjs +76 -0
  194. package/lib/oracle/index.mjs +84 -0
  195. package/lib/oracle/issues.mjs +164 -0
  196. package/lib/oracle/org-graph.mjs +170 -0
  197. package/lib/oracle/policy.mjs +80 -0
  198. package/lib/oracle/read-model.mjs +398 -0
  199. package/lib/oracle/reconcile.mjs +191 -0
  200. package/lib/oracle/routing.mjs +89 -0
  201. package/lib/oracle/synthesize.mjs +384 -0
  202. package/lib/oracle/verdicts.mjs +51 -0
  203. package/lib/orchestration/worker.mjs +88 -27
  204. package/lib/orchestration-policy.mjs +50 -0
  205. package/lib/parity.mjs +96 -2
  206. package/lib/playwright-demo.mjs +252 -0
  207. package/lib/project-init-shared.mjs +4 -1
  208. package/lib/prompt-composer.js +7 -3
  209. package/lib/prompt-validation-contract.mjs +24 -0
  210. package/lib/provider-capabilities.js +57 -11
  211. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  212. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  213. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  214. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  215. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  216. package/lib/providers/contract/contract-tests.mjs +57 -0
  217. package/lib/providers/contract/errors.mjs +48 -0
  218. package/lib/providers/contract/interface.mjs +50 -0
  219. package/lib/providers/contract/registry.mjs +102 -0
  220. package/lib/providers/copilot-auth.mjs +297 -0
  221. package/lib/providers/credential-bootstrap.mjs +178 -0
  222. package/lib/providers/credential-catalog.mjs +46 -0
  223. package/lib/providers/credential-sources.mjs +63 -0
  224. package/lib/providers/creds.mjs +5 -2
  225. package/lib/providers/op-run.mjs +59 -0
  226. package/lib/providers/secret-resolver.mjs +159 -0
  227. package/lib/publish-template.mjs +163 -0
  228. package/lib/publish-tooling.mjs +119 -0
  229. package/lib/publish.mjs +305 -0
  230. package/lib/registry/cli.mjs +82 -0
  231. package/lib/registry/consolidation.mjs +147 -0
  232. package/lib/registry/generate-docs.mjs +75 -0
  233. package/lib/registry/skill-verification.mjs +57 -0
  234. package/lib/registry/surface-map.mjs +76 -0
  235. package/lib/registry/validate.mjs +135 -0
  236. package/lib/resources/budget.mjs +82 -0
  237. package/lib/resources/process-budget.mjs +45 -0
  238. package/lib/rules-delivery.mjs +9 -2
  239. package/lib/rules-read.mjs +26 -0
  240. package/lib/runtime-env.mjs +23 -0
  241. package/lib/runtime-pressure.mjs +51 -7
  242. package/lib/schema-infer.mjs +13 -2
  243. package/lib/server/chat-loop.mjs +622 -0
  244. package/lib/server/demo-preview.mjs +63 -0
  245. package/lib/server/index.mjs +228 -7
  246. package/lib/service-manager.mjs +60 -8
  247. package/lib/setup.mjs +70 -7
  248. package/lib/specialists/prompt-schema.mjs +19 -10
  249. package/lib/specialists/roster.mjs +43 -0
  250. package/lib/specialists/scaffold.mjs +56 -0
  251. package/lib/storage/backend.mjs +6 -6
  252. package/lib/storage/hybrid-query.mjs +7 -4
  253. package/lib/storage/state-source.mjs +5 -5
  254. package/lib/storage/sync.mjs +2 -3
  255. package/lib/telemetry/rule-calls.mjs +52 -0
  256. package/lib/template-registry.mjs +2 -2
  257. package/lib/templates/visual-requirements.mjs +27 -51
  258. package/lib/test-corpus-inventory.mjs +313 -0
  259. package/lib/uninstall/uninstall.mjs +19 -7
  260. package/lib/update.mjs +12 -0
  261. package/lib/upgrade.mjs +14 -0
  262. package/lib/wireframe.mjs +20 -14
  263. package/lib/worker/run.mjs +17 -4
  264. package/lib/worker/trace.mjs +11 -3
  265. package/package.json +28 -14
  266. package/personas/construct.md +2 -2
  267. package/platforms/claude/settings.template.json +36 -0
  268. package/rules/common/patterns.md +1 -1
  269. package/rules/common/release-gates.md +4 -3
  270. package/scripts/sync-specialists.mjs +41 -28
  271. package/skills/devops/data-engineering.md +1 -1
  272. package/skills/docs/adr-workflow.md +1 -0
  273. package/skills/docs/backlog-proposal-workflow.md +1 -0
  274. package/skills/docs/codebase-research-workflow.md +40 -0
  275. package/skills/docs/customer-profile-workflow.md +1 -0
  276. package/skills/docs/document-ingest-workflow.md +1 -0
  277. package/skills/docs/evidence-ingest-workflow.md +1 -0
  278. package/skills/docs/init-docs.md +2 -2
  279. package/skills/docs/init-project.md +7 -2
  280. package/skills/docs/prd-workflow.md +24 -1
  281. package/skills/docs/prfaq-workflow.md +1 -0
  282. package/skills/docs/product-intelligence-workflow.md +1 -0
  283. package/skills/docs/product-signal-workflow.md +1 -0
  284. package/skills/docs/research-workflow.md +54 -37
  285. package/skills/docs/runbook-workflow.md +1 -0
  286. package/skills/docs/strategy-workflow.md +1 -0
  287. package/skills/docs/user-research-workflow.md +40 -0
  288. package/skills/operating/orchestration-reference.md +1 -1
  289. package/skills/roles/architect.md +5 -0
  290. package/skills/roles/operator.docs.md +4 -0
  291. package/skills/routing.md +4 -2
  292. package/specialists/artifact-manifest.json +480 -0
  293. package/specialists/artifact-manifest.schema.json +53 -0
  294. package/specialists/audit-enrichments.json +454 -0
  295. package/specialists/contracts.json +37 -25
  296. package/specialists/prompts/_shared/validation-contract.md +26 -0
  297. package/specialists/prompts/cx-accessibility.md +21 -2
  298. package/specialists/prompts/cx-ai-engineer.md +24 -1
  299. package/specialists/prompts/cx-architect.md +4 -1
  300. package/specialists/prompts/cx-business-strategist.md +23 -2
  301. package/specialists/prompts/cx-data-analyst.md +22 -1
  302. package/specialists/prompts/cx-data-engineer.md +23 -2
  303. package/specialists/prompts/cx-debugger.md +21 -2
  304. package/specialists/prompts/cx-designer.md +26 -3
  305. package/specialists/prompts/cx-devil-advocate.md +19 -2
  306. package/specialists/prompts/cx-docs-keeper.md +28 -1
  307. package/specialists/prompts/cx-engineer.md +22 -1
  308. package/specialists/prompts/cx-evaluator.md +18 -1
  309. package/specialists/prompts/cx-explorer.md +21 -2
  310. package/specialists/prompts/cx-legal-compliance.md +21 -2
  311. package/specialists/prompts/cx-operations.md +21 -2
  312. package/specialists/prompts/cx-oracle.md +94 -0
  313. package/specialists/prompts/cx-orchestrator.md +20 -1
  314. package/specialists/prompts/cx-platform-engineer.md +25 -2
  315. package/specialists/prompts/cx-product-manager.md +32 -1
  316. package/specialists/prompts/cx-qa.md +24 -2
  317. package/specialists/prompts/cx-rd-lead.md +23 -2
  318. package/specialists/prompts/cx-release-manager.md +23 -2
  319. package/specialists/prompts/cx-researcher.md +22 -2
  320. package/specialists/prompts/cx-reviewer.md +18 -1
  321. package/specialists/prompts/cx-security.md +22 -2
  322. package/specialists/prompts/cx-sre.md +30 -3
  323. package/specialists/prompts/cx-test-automation.md +7 -1
  324. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  325. package/specialists/prompts/cx-ux-researcher.md +21 -2
  326. package/specialists/registry.json +52 -173
  327. package/specialists/tone-profiles.json +42 -0
  328. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  329. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  330. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  331. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  332. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  333. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  334. package/templates/demos/vhs/construct-cockpit.json +24 -0
  335. package/templates/distribution/construct-brand.typ +446 -0
  336. package/templates/distribution/construct-decision.typ +38 -0
  337. package/templates/distribution/construct-deck.html +95 -0
  338. package/templates/distribution/construct-pdf.typ +38 -0
  339. package/templates/distribution/construct-prd.typ +38 -0
  340. package/templates/distribution/construct-research.typ +38 -0
  341. package/templates/distribution/construct-web.html +92 -0
  342. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  343. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  344. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  345. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  346. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  347. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  348. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  349. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  350. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  351. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  352. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  353. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  354. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  355. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  356. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  357. package/templates/distribution/fonts/README.md +36 -0
  358. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  359. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  360. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  361. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  362. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  363. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  364. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  365. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  366. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  367. package/templates/distribution/icons/activity.svg +15 -0
  368. package/templates/distribution/icons/alert-triangle.svg +17 -0
  369. package/templates/distribution/icons/book-open.svg +16 -0
  370. package/templates/distribution/icons/bot.svg +20 -0
  371. package/templates/distribution/icons/brain.svg +22 -0
  372. package/templates/distribution/icons/circle-check.svg +16 -0
  373. package/templates/distribution/icons/clipboard-check.svg +17 -0
  374. package/templates/distribution/icons/cpu.svg +28 -0
  375. package/templates/distribution/icons/database.svg +17 -0
  376. package/templates/distribution/icons/eye.svg +16 -0
  377. package/templates/distribution/icons/file-text.svg +19 -0
  378. package/templates/distribution/icons/gauge.svg +16 -0
  379. package/templates/distribution/icons/git-branch.svg +17 -0
  380. package/templates/distribution/icons/key.svg +17 -0
  381. package/templates/distribution/icons/layers.svg +17 -0
  382. package/templates/distribution/icons/list-checks.svg +19 -0
  383. package/templates/distribution/icons/lock.svg +16 -0
  384. package/templates/distribution/icons/message-square.svg +15 -0
  385. package/templates/distribution/icons/network.svg +19 -0
  386. package/templates/distribution/icons/route.svg +17 -0
  387. package/templates/distribution/icons/scale.svg +19 -0
  388. package/templates/distribution/icons/search.svg +16 -0
  389. package/templates/distribution/icons/send.svg +16 -0
  390. package/templates/distribution/icons/server.svg +18 -0
  391. package/templates/distribution/icons/shield-check.svg +16 -0
  392. package/templates/distribution/icons/users.svg +18 -0
  393. package/templates/distribution/icons/webhook.svg +17 -0
  394. package/templates/distribution/icons/workflow.svg +17 -0
  395. package/templates/distribution/icons/wrench.svg +15 -0
  396. package/templates/distribution/run.mjs +18 -1
  397. package/templates/docs/adr.md +7 -0
  398. package/templates/docs/construct_guide.md +8 -8
  399. package/templates/docs/customer-profile.md +4 -0
  400. package/templates/docs/one-pager.md +4 -0
  401. package/templates/docs/postmortem.md +31 -0
  402. package/templates/docs/prd-platform.md +19 -0
  403. package/templates/docs/prd.md +15 -0
  404. package/templates/docs/prfaq.md +7 -0
  405. package/templates/docs/qa-strategy.md +103 -0
  406. package/templates/docs/research-brief.md +8 -0
  407. package/templates/docs/rfc-platform.md +12 -0
  408. package/templates/docs/test-plan.md +7 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * lib/artifact-type-from-path.mjs — Infer manifest doc type from artifact file paths.
3
+ *
4
+ * Resolves a manifest doc type when --type is omitted.
5
+ * Resolution order: YAML frontmatter cx_doc_type → directory heuristics → filename hints.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { artifactTypes } from './artifact-manifest.mjs';
11
+
12
+ const KNOWN = new Set(artifactTypes());
13
+
14
+ function readFrontmatterType(filePath) {
15
+ try {
16
+ const head = fs.readFileSync(filePath, 'utf8').slice(0, 4096);
17
+ const match = head.match(/^---\n([\s\S]*?)\n---/);
18
+ if (!match) return null;
19
+ for (const line of match[1].split('\n')) {
20
+ const m = line.match(/^(?:cx_doc_type|artifactType|doc_type)\s*:\s*(.+)$/);
21
+ if (m) {
22
+ const value = m[1].trim().replace(/^['"]|['"]$/g, '');
23
+ return KNOWN.has(value) ? value : null;
24
+ }
25
+ }
26
+ } catch { /* unreadable */ }
27
+ return null;
28
+ }
29
+
30
+ function inferPrdSubtype(rel, base) {
31
+ const blob = `${rel} ${base}`.toLowerCase();
32
+ if (/meta[\s-]?prd/.test(blob)) return 'meta-prd';
33
+ if (/platform/.test(blob)) return 'prd-platform';
34
+ if (/business/.test(blob)) return 'prd-business';
35
+ return 'prd';
36
+ }
37
+
38
+ function inferRfcSubtype(rel, base) {
39
+ const blob = `${rel} ${base}`.toLowerCase();
40
+ if (/platform/.test(blob)) return 'rfc-platform';
41
+ return 'rfc';
42
+ }
43
+
44
+ const PREFIX_RULES = [
45
+ [/^docs\/prd\//, inferPrdSubtype],
46
+ [/^docs\/adr\//, () => 'adr'],
47
+ [/^docs\/rfc\//, inferRfcSubtype],
48
+ [/^docs\/research\//, () => 'research-brief'],
49
+ [/^docs\/runbooks?\//, () => 'runbook'],
50
+ [/^\.cx\/research\//, () => 'research-brief'],
51
+ ];
52
+
53
+ export function isArtifactGatePath(relPath) {
54
+ const rel = relPath.replace(/\\/g, '/');
55
+ if (!rel.endsWith('.md')) return false;
56
+ if (rel.startsWith('templates/docs/')) return false;
57
+ if (/^docs\//.test(rel) || /^\.cx\/research\//.test(rel)) return true;
58
+ return false;
59
+ }
60
+
61
+ export function inferArtifactTypeFromPath(filePath, { rootDir = process.cwd() } = {}) {
62
+ const rel = path.relative(rootDir, filePath).replace(/\\/g, '/');
63
+ if (!rel.endsWith('.md')) return null;
64
+
65
+ const fromFm = readFrontmatterType(filePath);
66
+ if (fromFm) return fromFm;
67
+
68
+ if (!isArtifactGatePath(rel)) return null;
69
+
70
+ const base = path.basename(filePath, path.extname(filePath));
71
+ for (const [re, resolver] of PREFIX_RULES) {
72
+ if (re.test(rel)) {
73
+ const type = typeof resolver === 'function' ? resolver(rel, base) : resolver;
74
+ return KNOWN.has(type) ? type : null;
75
+ }
76
+ }
77
+
78
+ return null;
79
+ }
@@ -155,6 +155,10 @@ export function auditSkills({ rootDir, silent = false } = {}) {
155
155
 
156
156
  export async function runAuditSkillsCli(args = []) {
157
157
  const rootDir = args.find((a) => a.startsWith('--root='))?.split('=')[1] ?? undefined;
158
+ if (args.includes('--inventory')) {
159
+ const { runSkillInventoryAuditCli } = await import('./certification/skill-inventory.mjs');
160
+ return runSkillInventoryAuditCli(args.filter((a) => a !== '--inventory'), { rootDir });
161
+ }
158
162
  const result = auditSkills({ rootDir });
159
163
  if (!result.pass) process.exit(1);
160
164
  }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * lib/audit-specialists.mjs — Specialist/skill corpus audit matrix.
3
+ *
4
+ * Merges live registry, contracts, skills, prompts, and audit-enrichments into
5
+ * one machine-readable matrix. Powers `construct audit specialists` and the
6
+ * human-readable docs/concepts/specialist-skill-audit.md companion.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { auditSkills } from './audit-skills.mjs';
12
+ import { loadArtifactManifest } from './artifact-manifest.mjs';
13
+ import { resolvePerspectiveFromPrompt } from './specialists/prompt-schema.mjs';
14
+ import { validateRoleCards } from './certification/role-cards.mjs';
15
+ import { auditSpecialistContracts } from './certification/specialist-contracts.mjs';
16
+ import { validateAllRoleOverlays } from './certification/role-overlays.mjs';
17
+ import { ROLE_DIRECTIVE_RE } from './role-preload.mjs';
18
+
19
+ function findConstructRoot(startPath = process.cwd()) {
20
+ let current = path.resolve(startPath);
21
+ while (true) {
22
+ if (fs.existsSync(path.join(current, 'specialists', 'registry.json'))) return current;
23
+ const parent = path.dirname(current);
24
+ if (parent === current) return path.resolve(startPath);
25
+ current = parent;
26
+ }
27
+ }
28
+
29
+ function readJson(p) {
30
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
31
+ }
32
+
33
+ function collectSkillFiles(skillsDir) {
34
+ const results = [];
35
+ function walk(dir, prefix = '') {
36
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
37
+ if (entry.isDirectory()) {
38
+ walk(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
39
+ } else if (entry.name.endsWith('.md') && entry.name !== 'routing.md') {
40
+ results.push(prefix ? `${prefix}/${entry.name.replace(/\.md$/, '')}` : entry.name.replace(/\.md$/, ''));
41
+ }
42
+ }
43
+ }
44
+ try { walk(skillsDir); } catch { /* missing */ }
45
+ return results.sort();
46
+ }
47
+
48
+ function parseRoleOverlay(promptPath) {
49
+ if (!fs.existsSync(promptPath)) return null;
50
+ const text = fs.readFileSync(promptPath, 'utf8');
51
+ const m = text.match(ROLE_DIRECTIVE_RE);
52
+ return m ? `roles/${m[1]}` : null;
53
+ }
54
+
55
+ function contractsForSpecialist(contracts, name) {
56
+ const id = name.startsWith('cx-') ? name : `cx-${name}`;
57
+ const related = [];
58
+ for (const c of contracts) {
59
+ if (c.producer === id || c.consumer === id) related.push(c.id);
60
+ }
61
+ return related;
62
+ }
63
+
64
+ function docTypesFromContracts(contracts) {
65
+ const types = new Set();
66
+ for (const c of contracts) {
67
+ const dt = c.trigger?.docAuthoring?.docType;
68
+ if (Array.isArray(dt)) dt.forEach((t) => types.add(t));
69
+ else if (typeof dt === 'string') types.add(dt);
70
+ }
71
+ return [...types];
72
+ }
73
+
74
+ function enforcementForAgent(agent, contracts) {
75
+ const id = `cx-${agent.name}`;
76
+ const items = ['no-fabrication.md', 'neurodivergent-output.md'];
77
+ const related = contracts.filter((c) => c.producer === id || c.consumer === id);
78
+ if (related.some((c) => c.postconditions?.length)) items.push('contracts.json postconditions');
79
+ if (agent.docArtifacts?.length) items.push('artifact structure lint', 'comment-lint on artifact paths');
80
+ if (agent.watchConditions?.length) items.push('watchConditions routing');
81
+ return items;
82
+ }
83
+
84
+ function crossChecks({ registry, manifest, skillAudit, allSkills, contractDocTypes }) {
85
+ const issues = [];
86
+ const manifestTypes = new Set(Object.keys(manifest.artifacts ?? {}));
87
+
88
+ for (const agent of registry.specialists ?? []) {
89
+ for (const skill of agent.skills ?? []) {
90
+ if (!allSkills.includes(skill)) {
91
+ issues.push({ kind: 'missing-skill-file', agent: agent.name, skill });
92
+ }
93
+ }
94
+ for (const doc of agent.docArtifacts ?? []) {
95
+ if (!manifestTypes.has(doc)) {
96
+ issues.push({ kind: 'doc-artifact-no-manifest', agent: agent.name, docType: doc });
97
+ }
98
+ }
99
+ }
100
+
101
+ for (const dt of contractDocTypes) {
102
+ if (!manifestTypes.has(dt)) {
103
+ issues.push({ kind: 'contract-doc-no-manifest', docType: dt });
104
+ }
105
+ }
106
+
107
+ if (skillAudit.missingSkillFiles.length) {
108
+ for (const { agent, skill } of skillAudit.missingSkillFiles) {
109
+ issues.push({ kind: 'registry-missing-skill', agent, skill });
110
+ }
111
+ }
112
+
113
+ return issues;
114
+ }
115
+
116
+ export function auditSpecialists({ rootDir, silent = false } = {}) {
117
+ const root = rootDir ?? findConstructRoot();
118
+ const registry = readJson(path.join(root, 'specialists', 'registry.json'));
119
+ const contractsDoc = readJson(path.join(root, 'specialists', 'contracts.json'));
120
+ const enrichments = readJson(path.join(root, 'specialists', 'audit-enrichments.json'));
121
+ const manifest = loadArtifactManifest({ rootDir: root });
122
+ const skillAudit = auditSkills({ rootDir: root, silent: true });
123
+ const allSkills = collectSkillFiles(path.join(root, 'skills'));
124
+ const contractDocTypes = docTypesFromContracts(contractsDoc.contracts ?? []);
125
+ const crossCheckIssues = crossChecks({ registry, manifest, skillAudit, allSkills, contractDocTypes });
126
+
127
+ const specialists = (registry.specialists ?? []).map((agent) => {
128
+ const promptPath = agent.promptFile
129
+ ? path.join(root, agent.promptFile)
130
+ : null;
131
+ const meta = enrichments.specialists?.[agent.name] ?? {};
132
+ const perspective = resolvePerspectiveFromPrompt(agent.name, { rootDir: root, registry });
133
+ return {
134
+ name: agent.name,
135
+ id: `cx-${agent.name}`,
136
+ humanEquivalent: meta.humanEquivalent ?? 'unknown',
137
+ primaryOutputs: agent.docArtifacts ?? [],
138
+ skillsBound: agent.skills ?? [],
139
+ roleOverlay: promptPath ? parseRoleOverlay(promptPath) : null,
140
+ researchProfile: meta.researchProfile ?? null,
141
+ toneProfile: meta.toneProfile ?? 'direct',
142
+ challengeModel: {
143
+ tension: perspective?.tension ?? null,
144
+ failureMode: perspective?.failureMode ?? null,
145
+ watchConditions: agent.watchConditions ?? [],
146
+ },
147
+ enforcementToday: enforcementForAgent(agent, contractsDoc.contracts ?? []),
148
+ contracts: contractsForSpecialist(contractsDoc.contracts ?? [], agent.name),
149
+ grade: meta.grade ?? 'adequate',
150
+ gap: meta.gap ?? 'Not assessed',
151
+ beadId: meta.beadId ?? 'construct-hcr9',
152
+ skillCount: (agent.skills ?? []).length,
153
+ };
154
+ });
155
+
156
+ const roleOverlays = [];
157
+ const rolesDir = path.join(root, 'skills', 'roles');
158
+ if (fs.existsSync(rolesDir)) {
159
+ for (const f of fs.readdirSync(rolesDir).filter((n) => n.endsWith('.md')).sort()) {
160
+ const key = f.replace(/\.md$/, '');
161
+ const meta = enrichments.roleOverlays?.[key] ?? {};
162
+ roleOverlays.push({
163
+ path: `roles/${key}`,
164
+ grade: meta.grade ?? 'adequate',
165
+ gap: meta.gap ?? 'Not assessed',
166
+ });
167
+ }
168
+ }
169
+
170
+ const workflowSkills = allSkills
171
+ .filter((s) => !s.startsWith('roles/'))
172
+ .map((skillPath) => {
173
+ const manifestEntry = Object.entries(manifest.artifacts ?? {}).find(([, v]) => v.workflowSkill === skillPath);
174
+ return {
175
+ path: skillPath,
176
+ artifactType: manifestEntry?.[0] ?? null,
177
+ toneDefault: manifestEntry?.[1]?.toneDefault ?? null,
178
+ };
179
+ });
180
+
181
+ const roleCards = validateRoleCards({ rootDir: root });
182
+ const specialistContracts = auditSpecialistContracts({ rootDir: root });
183
+ const roleOverlayAudit = validateAllRoleOverlays({ rootDir: root });
184
+ if (!roleCards.pass) {
185
+ for (const err of roleCards.errors) crossCheckIssues.push({ kind: 'role-card-missing', detail: err });
186
+ }
187
+ if (!specialistContracts.pass) {
188
+ for (const fail of specialistContracts.failures) {
189
+ crossCheckIssues.push({ kind: 'specialist-contract-fail', specialist: fail.specialistId });
190
+ }
191
+ }
192
+ if (!roleOverlayAudit.pass) {
193
+ for (const err of roleOverlayAudit.errors.slice(0, 5)) {
194
+ crossCheckIssues.push({ kind: 'role-overlay-fail', detail: err });
195
+ }
196
+ }
197
+
198
+ const result = {
199
+ version: 1,
200
+ generatedAt: new Date().toISOString(),
201
+ specialistCount: specialists.length,
202
+ roleOverlayCount: roleOverlays.length,
203
+ workflowSkillCount: workflowSkills.length,
204
+ specialists,
205
+ roleOverlays,
206
+ workflowSkills,
207
+ crossCheckIssues,
208
+ roleCards,
209
+ specialistContracts,
210
+ roleOverlayAudit,
211
+ pass: crossCheckIssues.length === 0 && skillAudit.pass,
212
+ };
213
+
214
+ if (!silent) {
215
+ const line = (msg) => process.stdout.write(`${msg}\n`);
216
+ line('Construct Specialist Audit');
217
+ line('═══════════════════════════');
218
+ line(` Specialists: ${result.specialistCount}`);
219
+ line(` Role overlays: ${result.roleOverlayCount}`);
220
+ line(` Workflow skills: ${result.workflowSkillCount}`);
221
+ line('');
222
+ if (crossCheckIssues.length === 0) {
223
+ line(' ✓ Cross-checks passed (skills, docArtifacts, manifest, contracts)');
224
+ } else {
225
+ line(` ✗ Cross-check issues (${crossCheckIssues.length}):`);
226
+ for (const issue of crossCheckIssues) line(` - ${JSON.stringify(issue)}`);
227
+ }
228
+ line('');
229
+ line(result.pass ? ' Result: PASS' : ' Result: FAIL');
230
+ }
231
+
232
+ return result;
233
+ }
234
+
235
+ export function formatAuditMarkdown(result) {
236
+ const lines = [
237
+ '# Specialist & skill audit',
238
+ '',
239
+ `Generated: ${result.generatedAt}. Re-run \`construct audit specialists --json\` for the live matrix.`,
240
+ '',
241
+ '## Specialists',
242
+ '',
243
+ '| Specialist | Human equivalent | Outputs | Skills | Role overlay | Research | Tone | Grade | Gap |',
244
+ '|---|---|---|---|---|---|---|---|---|',
245
+ ];
246
+
247
+ for (const s of result.specialists) {
248
+ lines.push(
249
+ `| cx-${s.name} | ${s.humanEquivalent} | ${s.primaryOutputs.join(', ') || '—'} | ${s.skillCount} | ${s.roleOverlay ?? '—'} | ${s.researchProfile ?? '—'} | ${s.toneProfile} | ${s.grade} | ${s.gap} |`,
250
+ );
251
+ }
252
+
253
+ lines.push('', '## Role overlays', '', '| Overlay | Grade | Gap |', '|---|---|---|');
254
+ for (const r of result.roleOverlays) {
255
+ lines.push(`| ${r.path} | ${r.grade} | ${r.gap} |`);
256
+ }
257
+
258
+ lines.push('', '## Cross-check issues', '');
259
+ if (result.crossCheckIssues.length === 0) {
260
+ lines.push('None.');
261
+ } else {
262
+ for (const issue of result.crossCheckIssues) {
263
+ lines.push(`- \`${issue.kind}\`: ${JSON.stringify(issue)}`);
264
+ }
265
+ }
266
+
267
+ return `${lines.join('\n')}\n`;
268
+ }
269
+
270
+ export async function runAuditSpecialistsCli(args = []) {
271
+ const json = args.includes('--json');
272
+ const markdown = args.includes('--markdown');
273
+ const rootDir = args.find((a) => a.startsWith('--root='))?.split('=')[1];
274
+ const result = auditSpecialists({ rootDir, silent: true });
275
+
276
+ if (markdown) {
277
+ process.stdout.write(formatAuditMarkdown(result));
278
+ } else if (json) {
279
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
280
+ } else {
281
+ auditSpecialists({ rootDir, silent: false });
282
+ }
283
+
284
+ if (!result.pass) process.exit(1);
285
+ }
@@ -16,7 +16,7 @@
16
16
  * Corrupted links cannot be re-derived, so a `chain_reset` boundary line seals
17
17
  * the damaged prefix as immutable legacy and re-bases the live chain;
18
18
  * verifyChain validates only from the last boundary forward. See
19
- * docs/concepts/observability.mdx.
19
+ * docs/concepts/observability.md.
20
20
  */
21
21
  import { readFileSync, existsSync, statSync, mkdirSync } from 'node:fs';
22
22
  import { createHash } from 'node:crypto';
package/lib/auto-docs.mjs CHANGED
@@ -90,20 +90,25 @@ function buildCoreDocsContract() {
90
90
  }
91
91
 
92
92
  const DIR_DESCRIPTIONS = {
93
- agents: 'Registry and generated platform adapter chains',
94
- apps: 'User-facing apps shipped from this repo (e.g. apps/docs/, the Next.js docs site)',
93
+ apps: 'User-facing apps shipped from this repo (chat, dashboard, docs)',
95
94
  bin: 'CLI entrypoint (`construct`)',
96
- claude: 'Claude Code integration (agents, settings template)',
97
95
  commands: 'Command prompt assets',
98
- codex: 'GitHub Copilot / Codex integration',
96
+ config: 'Repo-wide controlled vocabulary (tag-vocabulary.json)',
97
+ deploy: 'Terraform and deployment configs',
99
98
  docs: 'Architecture notes, runbooks, and documentation contract',
100
- telemetry: 'Local trace capture and optional telemetry export',
101
- lib: 'Core runtime: CLI, hooks, MCP, status, sync, workflow',
102
- opencode: 'OpenCode integration config',
99
+ examples: 'Example projects and persona fixtures',
100
+ lib: 'Core runtime: CLI, hooks, MCP, providers, oracle, sync',
101
+ packages: 'Shared workspace packages (e.g. cx-ui)',
103
102
  personas: 'Persona prompt definitions',
103
+ platforms: 'Host adapter capability configs',
104
+ profiles: 'Profile catalog',
105
+ registry: 'Product capability registry',
104
106
  rules: 'Coding and quality standards',
105
- site: 'Legacy docs-site output',
107
+ schemas: 'Registry and config JSON Schema',
108
+ scripts: 'Audit, alignment, release, and sync scripts',
106
109
  skills: 'Reusable domain knowledge files',
110
+ specialists: 'Org registry, contracts, and specialist prompts',
111
+ templates: 'Doc and workflow templates',
107
112
  tests: 'Test suite',
108
113
  };
109
114
 
@@ -164,16 +169,16 @@ function buildAgentsTable(rootDir) {
164
169
  let registry;
165
170
  try { registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')); } catch { return ''; }
166
171
 
167
- const agents = registry.agents ?? registry;
168
- if (!Array.isArray(agents)) return '';
172
+ const agents = [
173
+ ...(registry.orchestrator ? [{ ...registry.orchestrator, name: registry.orchestrator.name }] : []),
174
+ ...(registry.specialists ?? []),
175
+ ];
176
+ if (!agents.length) return '';
169
177
 
170
178
  const rows = ['| Specialist | Tier | Purpose |', '|---|---|---|'];
171
179
  for (const agent of agents.slice(0, 30)) {
172
180
  const name = agent.name ?? agent.id ?? 'n/a';
173
181
  const tier = agent.modelTier ?? agent.tier ?? agent.model_tier ?? 'n/a';
174
- // Strip em-dashes from descriptions so the prose lint stays clean across
175
- // AUTO regen. Em-dashes are allowed in the registry's source descriptions
176
- // but never in the rendered markdown.
177
182
  const purpose = (agent.description ?? agent.purpose ?? '')
178
183
  .split('\n')[0].slice(0, 80)
179
184
  .replace(/ — /g, '. ').replace(/—/g, ',');
@@ -474,13 +479,77 @@ export function buildFumadocsReference({ rootDir } = {}) {
474
479
  written.push(path.relative(rootDir, hooksPage));
475
480
  }
476
481
 
477
- const agentsContent = renderAgentsPage(rootDir);
478
- if (agentsContent) {
479
- const agentsPage = path.join(refDir, 'agents.md');
480
- if (writeIfChanged(agentsPage, agentsContent)) {
481
- written.push(path.relative(rootDir, agentsPage));
482
+ const specialistsContent = renderAgentsPage(rootDir);
483
+ if (specialistsContent) {
484
+ const specialistsPage = path.join(refDir, 'specialists.md');
485
+ if (writeIfChanged(specialistsPage, specialistsContent)) {
486
+ written.push(path.relative(rootDir, specialistsPage));
487
+ }
488
+ const legacyAgentsPage = path.join(refDir, 'agents.md');
489
+ if (fs.existsSync(legacyAgentsPage)) {
490
+ fs.unlinkSync(legacyAgentsPage);
491
+ written.push(`- ${path.relative(rootDir, legacyAgentsPage)}`);
482
492
  }
483
493
  }
484
494
 
485
495
  return { written };
486
496
  }
497
+
498
+ export function checkFumadocsReferenceDrift({ rootDir } = {}) {
499
+ rootDir = rootDir ?? process.cwd();
500
+ const refDir = path.join(rootDir, 'docs', 'reference');
501
+ const cliDir = path.join(refDir, 'cli');
502
+ const drift = [];
503
+
504
+ for (const category of REFERENCE_CATEGORIES) {
505
+ const commands = CLI_COMMANDS_BY_CATEGORY[category] ?? [];
506
+ if (!commands.length) continue;
507
+ const slug = slugify(category);
508
+ const out = path.join(cliDir, `${slug}.md`);
509
+ const content = renderCommandPage(category, commands);
510
+ if (readFile(out) !== content) drift.push(path.relative(rootDir, out));
511
+ }
512
+
513
+ const cliPages = REFERENCE_CATEGORIES
514
+ .map((cat) => ({ cat, commands: CLI_COMMANDS_BY_CATEGORY[cat] ?? [] }))
515
+ .filter(({ commands }) => commands.length)
516
+ .map(({ cat }) => slugify(cat));
517
+ const cliMetaContent = JSON.stringify({ title: 'CLI', pages: ['index', ...cliPages] }, null, 2) + '\n';
518
+ const cliMeta = path.join(cliDir, 'meta.json');
519
+ if (readFile(cliMeta) !== cliMetaContent) drift.push(path.relative(rootDir, cliMeta));
520
+
521
+ const cliIndexContent = [
522
+ '---',
523
+ 'title: CLI',
524
+ 'description: Every construct command, grouped by category. Generated from lib/cli-commands.mjs.',
525
+ '---',
526
+ '',
527
+ '> Generated from `lib/cli-commands.mjs`. Re-run `construct docs:site` to refresh.',
528
+ '',
529
+ 'Commands are grouped by what they do. Pick a category below.',
530
+ '',
531
+ ...REFERENCE_CATEGORIES.map((cat) => {
532
+ const slug = slugify(cat);
533
+ const commands = CLI_COMMANDS_BY_CATEGORY[cat] ?? [];
534
+ if (!commands.length) return null;
535
+ return `- [${cat}](/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
536
+ }).filter(Boolean),
537
+ '',
538
+ ].join('\n');
539
+ const cliIndex = path.join(cliDir, 'index.md');
540
+ if (readFile(cliIndex) !== cliIndexContent) drift.push(path.relative(rootDir, cliIndex));
541
+
542
+ const hooksPage = path.join(refDir, 'hooks.md');
543
+ const hooksContent = renderHooksPage(rootDir);
544
+ if (hooksContent && readFile(hooksPage) !== hooksContent) drift.push(path.relative(rootDir, hooksPage));
545
+
546
+ const specialistsContent = renderAgentsPage(rootDir);
547
+ const specialistsPage = path.join(refDir, 'specialists.md');
548
+ if (specialistsContent && readFile(specialistsPage) !== specialistsContent) {
549
+ drift.push(path.relative(rootDir, specialistsPage));
550
+ }
551
+ const legacyAgentsPage = path.join(refDir, 'agents.md');
552
+ if (fs.existsSync(legacyAgentsPage)) drift.push(path.relative(rootDir, legacyAgentsPage));
553
+
554
+ return { drift };
555
+ }
@@ -37,6 +37,10 @@ function daysAgo(value) {
37
37
  return (Date.now() - ts) / (24 * 60 * 60 * 1000);
38
38
  }
39
39
 
40
+ function beadUpdatedAt(bead) {
41
+ return bead?.updated || bead?.updatedAt || bead?.updated_at || bead?.created || bead?.createdAt || bead?.created_at || null;
42
+ }
43
+
40
44
  function listBeadsJson({ status, runner = spawnSync } = {}) {
41
45
  const args = ['list', '--json'];
42
46
  if (status) args.push(`--status=${status}`);
@@ -78,6 +82,20 @@ function titleAppearsInSubject(beadTitle, commitSubject) {
78
82
  return hits >= 3;
79
83
  }
80
84
 
85
+ /**
86
+ * Oracle auto-raised meta beads must not feed back into hygiene drift counts.
87
+ */
88
+ export function isOracleMetaBead(bead) {
89
+ const title = String(bead?.title ?? '');
90
+ const labels = Array.isArray(bead?.labels) ? bead.labels : [];
91
+ return title.startsWith('[oracle/') || labels.includes('oracle');
92
+ }
93
+
94
+ function filterOracleMeta(beads, excludeOracleMeta) {
95
+ if (!excludeOracleMeta) return beads;
96
+ return beads.filter((b) => !isOracleMetaBead(b));
97
+ }
98
+
81
99
  /**
82
100
  * Run the three drift checks against the live tracker + git state.
83
101
  * Pure-ish — accepts a runner injection for tests.
@@ -86,14 +104,17 @@ export function detectBeadsDrift({
86
104
  staleOpenDays = DEFAULT_STALE_OPEN_DAYS,
87
105
  stuckInProgressDays = DEFAULT_STUCK_IN_PROGRESS_DAYS,
88
106
  mergeLookback = DEFAULT_MERGE_LOOKBACK,
107
+ excludeOracleMeta = true,
89
108
  runner = spawnSync,
90
109
  } = {}) {
91
- const open = listBeadsJson({ status: 'open', runner });
92
- const inProgress = listBeadsJson({ status: 'in_progress', runner });
110
+ const openRaw = listBeadsJson({ status: 'open', runner });
111
+ const inProgressRaw = listBeadsJson({ status: 'in_progress', runner });
112
+ const open = filterOracleMeta(openRaw, excludeOracleMeta);
113
+ const inProgress = filterOracleMeta(inProgressRaw, excludeOracleMeta);
93
114
  const commits = recentCommitSubjects(mergeLookback, { runner });
94
115
 
95
- const staleOpen = open.filter((b) => daysAgo(b.updated || b.updatedAt) > staleOpenDays);
96
- const stuckInProgress = inProgress.filter((b) => daysAgo(b.updated || b.updatedAt) > stuckInProgressDays);
116
+ const staleOpen = open.filter((b) => daysAgo(beadUpdatedAt(b)) > staleOpenDays);
117
+ const stuckInProgress = inProgress.filter((b) => daysAgo(beadUpdatedAt(b)) > stuckInProgressDays);
97
118
 
98
119
  const mergeDrift = [];
99
120
  for (const bead of open) {
@@ -106,8 +127,8 @@ export function detectBeadsDrift({
106
127
  }
107
128
 
108
129
  return {
109
- staleOpen: staleOpen.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(b.updated || b.updatedAt)) })),
110
- stuckInProgress: stuckInProgress.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(b.updated || b.updatedAt)) })),
130
+ staleOpen: staleOpen.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(beadUpdatedAt(b))) })),
131
+ stuckInProgress: stuckInProgress.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(beadUpdatedAt(b))) })),
111
132
  mergeDrift,
112
133
  thresholds: { staleOpenDays, stuckInProgressDays, mergeLookback },
113
134
  counts: {