@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
@@ -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
+ }
@@ -1,22 +1,68 @@
1
1
  /**
2
- * lib/audit-trail.mjs — reads the append-only audit log produced by
3
- * lib/hooks/audit-trail.mjs and renders it for `construct audit trail`.
2
+ * lib/audit-trail.mjs — owns the append-only audit log written by
3
+ * lib/hooks/audit-trail.mjs and rendered for `construct audit trail`.
4
4
  *
5
5
  * The log is JSONL at ~/.cx/audit-trail.jsonl. Each line carries a
6
6
  * prev_line_hash pointing at the SHA-256 of the previous line — tampering
7
7
  * (reorder, delete, edit) breaks the chain and is surfaced by --verify.
8
+ *
9
+ * A serial hash chain cannot survive concurrent appends: a read-prev-hash +
10
+ * append done outside a critical section lets two processes (hook, daemon,
11
+ * parallel tool calls in one session) both chain off the same predecessor, so
12
+ * the second link points at a stale tail. appendAuditRecord
13
+ * closes that window by computing prev_line_hash and appending inside a single
14
+ * cross-process file lock, making every writer effectively serialized.
15
+ *
16
+ * Corrupted links cannot be re-derived, so a `chain_reset` boundary line seals
17
+ * the damaged prefix as immutable legacy and re-bases the live chain;
18
+ * verifyChain validates only from the last boundary forward. See
19
+ * docs/concepts/observability.md.
8
20
  */
9
- import { readFileSync, existsSync, statSync } from 'node:fs';
21
+ import { readFileSync, existsSync, statSync, mkdirSync } from 'node:fs';
10
22
  import { createHash } from 'node:crypto';
11
- import { join } from 'node:path';
23
+ import { join, dirname } from 'node:path';
12
24
  import { homedir } from 'node:os';
13
25
 
26
+ import { appendBounded, readLastLineAcrossSegments } from './logging/rotate.mjs';
27
+ import { withFileLockSync } from './storage/file-lock.mjs';
28
+
14
29
  const AUDIT_FILE = join(homedir(), '.cx', 'audit-trail.jsonl');
15
30
 
16
31
  function sha256(input) {
17
32
  return createHash('sha256').update(input).digest('hex');
18
33
  }
19
34
 
35
+ // prev_line_hash and the append must be one atomic step or concurrent writers
36
+ // race; the file lock serializes them across processes.
37
+
38
+ export function appendAuditRecord(partial, { file = AUDIT_FILE } = {}) {
39
+ return withFileLockSync(file, () => {
40
+ let prev = null;
41
+ try {
42
+ const last = readLastLineAcrossSegments(file);
43
+ if (last !== null) prev = sha256(last);
44
+ } catch { /* first record, or unreadable tail */ }
45
+ const record = { ...partial, prev_line_hash: prev };
46
+ mkdirSync(dirname(file), { recursive: true });
47
+ appendBounded('audit-trail', file, JSON.stringify(record) + '\n');
48
+ return record;
49
+ });
50
+ }
51
+
52
+ // A reset boundary seals the records it follows as legacy excluded from
53
+ // verification and re-bases the live chain from this line.
54
+
55
+ export function writeChainReset({ file = AUDIT_FILE, reason = 'manual reset' } = {}) {
56
+ return appendAuditRecord({
57
+ ts: new Date().toISOString(),
58
+ chain_reset: true,
59
+ reason,
60
+ agent: 'construct',
61
+ tool: null,
62
+ target: null,
63
+ }, { file });
64
+ }
65
+
20
66
  export function readAuditTrail({ limit = 50, since = null, agent = null, tool = null } = {}) {
21
67
  if (!existsSync(AUDIT_FILE)) return [];
22
68
  const lines = readFileSync(AUDIT_FILE, 'utf8').split('\n').filter(Boolean);
@@ -34,19 +80,29 @@ export function readAuditTrail({ limit = 50, since = null, agent = null, tool =
34
80
  return filtered.slice(-limit);
35
81
  }
36
82
 
37
- export function verifyChain() {
38
- if (!existsSync(AUDIT_FILE)) return { ok: true, verified: 0, broken: [] };
39
- const lines = readFileSync(AUDIT_FILE, 'utf8').split('\n').filter(Boolean);
83
+ export function verifyChain(file = AUDIT_FILE) {
84
+ if (!existsSync(file)) return { ok: true, verified: 0, broken: [], legacy: 0 };
85
+ const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
86
+
87
+ // Validation starts at the last reset boundary; the prefix it seals is legacy
88
+ // and exempt, so concurrency damage predating the migration can't fail the chain.
89
+
90
+ let start = 0;
91
+ for (let i = 0; i < lines.length; i += 1) {
92
+ try { if (JSON.parse(lines[i]).chain_reset) start = i; } catch { /* legacy malformed */ }
93
+ }
94
+
40
95
  const broken = [];
41
96
  let prevHash = null;
42
- for (let i = 0; i < lines.length; i += 1) {
97
+ for (let i = start; i < lines.length; i += 1) {
43
98
  const line = lines[i];
44
99
  let row;
45
100
  try { row = JSON.parse(line); } catch {
46
101
  broken.push({ line: i + 1, reason: 'malformed JSON' });
102
+ prevHash = sha256(line);
47
103
  continue;
48
104
  }
49
- if (i > 0 && row.prev_line_hash !== prevHash) {
105
+ if (i > start && !row.chain_reset && row.prev_line_hash !== prevHash) {
50
106
  broken.push({
51
107
  line: i + 1,
52
108
  expected: prevHash,
@@ -56,7 +112,7 @@ export function verifyChain() {
56
112
  }
57
113
  prevHash = sha256(line);
58
114
  }
59
- return { ok: broken.length === 0, verified: lines.length, broken };
115
+ return { ok: broken.length === 0, verified: lines.length - start, broken, legacy: start };
60
116
  }
61
117
 
62
118
  function formatRow(row) {
@@ -74,6 +130,9 @@ export async function runAuditTrailCli(args = []) {
74
130
  for (let i = 0; i < args.length; i += 1) {
75
131
  const arg = args[i];
76
132
  if (arg === '--verify') flags.add('verify');
133
+ else if (arg === '--reset') flags.add('reset');
134
+ else if (arg.startsWith('--reason=')) options.reason = arg.slice(9);
135
+ else if (arg === '--reason') options.reason = args[++i];
77
136
  else if (arg === '--json') flags.add('json');
78
137
  else if (arg === '--all') flags.add('all');
79
138
  else if (arg.startsWith('--limit=')) options.limit = Number(arg.slice(8));
@@ -97,6 +156,8 @@ Options:
97
156
  --agent <name> Filter to a specific agent (e.g. cx-engineer)
98
157
  --tool <name> Filter to a specific tool (Edit, Write, MultiEdit, NotebookEdit, Bash)
99
158
  --verify Verify the tamper-evidence chain and exit
159
+ --reset Seal the current chain as legacy and re-base a fresh chain
160
+ --reason <text> Reason recorded on the --reset boundary
100
161
  --json Emit raw JSONL lines instead of formatted output
101
162
  -h, --help Show this message
102
163
 
@@ -105,6 +166,12 @@ Log location: ${AUDIT_FILE}
105
166
  return;
106
167
  }
107
168
 
169
+ if (flags.has('reset')) {
170
+ writeChainReset({ reason: options.reason || 'manual reset' });
171
+ console.log(`Audit chain re-based at ${AUDIT_FILE}. Prior records sealed as legacy; verification resumes from the new boundary.`);
172
+ return;
173
+ }
174
+
108
175
  if (flags.has('verify')) {
109
176
  const result = verifyChain();
110
177
  if (result.ok) {
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
+ }