@geraldmaron/construct 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +10 -7
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1116 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +525 -38
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
package/lib/validator.mjs CHANGED
@@ -1,7 +1,10 @@
1
1
  /**
2
- * lib/validator.mjs — <one-line purpose>
2
+ * lib/validator.mjs — Schema and contract validation for Construct data structures.
3
3
  *
4
- * <2–6 line summary: what it does, who calls it, key side effects.>
4
+ * Validates workflow state objects, task packets, agent registry entries, and
5
+ * MCP tool payloads against expected shapes. Throws structured errors with
6
+ * field-level detail. Backs workflow-state, cli-commands, and MCP tools
7
+ * before they mutate persistent state.
5
8
  */
6
9
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
10
  import { existsSync, readFileSync } from 'node:fs';
@@ -10,6 +13,23 @@ import { join, dirname } from 'node:path';
10
13
  const VALID_TIERS = new Set(['reasoning', 'standard', 'fast']);
11
14
  const VALID_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
12
15
 
16
+ const PROMPT_HARD_CAP_WORDS = 4000;
17
+ const DESCRIPTION_MAX_CHARS = 240;
18
+ const DISPLAY_NAME_MAX_CHARS = 60;
19
+
20
+ function wordCount(text) {
21
+ return String(text || '').trim().split(/\s+/).filter(Boolean).length;
22
+ }
23
+
24
+ function loadPromptContent(rootDir, relPath) {
25
+ if (!relPath) return null;
26
+ try {
27
+ return readFileSync(join(rootDir, relPath), 'utf8');
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
13
33
  function hasProviderModelShape(value) {
14
34
  return typeof value === 'string' && /^[^\s/]+\/\S+$/.test(value);
15
35
  }
@@ -71,6 +91,8 @@ export function validateRegistry(reg, options = {}) {
71
91
 
72
92
  if (!agent.description || typeof agent.description !== 'string') {
73
93
  errors.push(`${label}: description must be a non-empty string`);
94
+ } else if (agent.description.length > DESCRIPTION_MAX_CHARS) {
95
+ errors.push(`${label}: description exceeds ${DESCRIPTION_MAX_CHARS} chars (got ${agent.description.length})`);
74
96
  }
75
97
  if (agent.prompt !== undefined && typeof agent.prompt !== 'string') {
76
98
  errors.push(`${label}: prompt must be a string when present`);
@@ -83,6 +105,18 @@ export function validateRegistry(reg, options = {}) {
83
105
  } else if (agent.promptFile && !existsSync(join(rootDir, agent.promptFile))) {
84
106
  errors.push(`${label}: promptFile does not exist (${agent.promptFile})`);
85
107
  }
108
+ const promptText = typeof agent.prompt === 'string'
109
+ ? agent.prompt
110
+ : loadPromptContent(rootDir, agent.promptFile);
111
+ if (promptText) {
112
+ const cap = Number.isInteger(agent.wordCapOverride) && agent.wordCapOverride > 0
113
+ ? agent.wordCapOverride
114
+ : PROMPT_HARD_CAP_WORDS;
115
+ const words = wordCount(promptText);
116
+ if (words > cap) {
117
+ errors.push(`${label}: prompt is ${words} words, exceeds cap of ${cap} (set wordCapOverride if intentional)`);
118
+ }
119
+ }
86
120
  if (!agent.model && !agent.modelTier) {
87
121
  errors.push(`${label}: must define model or modelTier`);
88
122
  }
@@ -107,6 +141,11 @@ export function validateRegistry(reg, options = {}) {
107
141
 
108
142
  if (!persona.description || typeof persona.description !== 'string') {
109
143
  errors.push(`${label}: description must be a non-empty string`);
144
+ } else if (persona.description.length > DESCRIPTION_MAX_CHARS) {
145
+ errors.push(`${label}: description exceeds ${DESCRIPTION_MAX_CHARS} chars (got ${persona.description.length})`);
146
+ }
147
+ if (persona.displayName && persona.displayName.length > DISPLAY_NAME_MAX_CHARS) {
148
+ errors.push(`${label}: displayName exceeds ${DISPLAY_NAME_MAX_CHARS} chars (got ${persona.displayName.length})`);
110
149
  }
111
150
  if (!persona.role || typeof persona.role !== 'string') {
112
151
  errors.push(`${label}: role must be a non-empty string`);
@@ -116,6 +155,16 @@ export function validateRegistry(reg, options = {}) {
116
155
  } else if (!existsSync(join(rootDir, persona.promptFile))) {
117
156
  errors.push(`${label}: promptFile does not exist (${persona.promptFile})`);
118
157
  }
158
+ const personaPromptText = loadPromptContent(rootDir, persona.promptFile);
159
+ if (personaPromptText) {
160
+ const cap = Number.isInteger(persona.wordCapOverride) && persona.wordCapOverride > 0
161
+ ? persona.wordCapOverride
162
+ : PROMPT_HARD_CAP_WORDS;
163
+ const words = wordCount(personaPromptText);
164
+ if (words > cap) {
165
+ errors.push(`${label}: prompt is ${words} words, exceeds cap of ${cap} (set wordCapOverride if intentional)`);
166
+ }
167
+ }
119
168
  if (!persona.model && !persona.modelTier) {
120
169
  errors.push(`${label}: must define model or modelTier`);
121
170
  }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * lib/validators/skills.mjs — Validate the structure of skill files.
3
+ *
4
+ * Skills are markdown files under `skills/` (and any merged plugin or project
5
+ * skill paths) that follow this convention:
6
+ *
7
+ * <!--
8
+ * skills/<domain>/<name>.md — <Title> — <one-line description>
9
+ * ...
10
+ * -->
11
+ * # <Title>
12
+ *
13
+ * Use this skill when <triggering condition>.
14
+ * ...
15
+ *
16
+ * The validator splits findings into hard errors (block the build) and soft
17
+ * warnings (surfaced by `construct doctor` without failing it):
18
+ *
19
+ * Hard errors:
20
+ * - missing H1 title
21
+ * - title over 80 chars
22
+ * - duplicate relative paths across roots
23
+ * - unreadable file
24
+ *
25
+ * Soft warnings:
26
+ * - opener is missing or doesn't match a recognised "Use this skill
27
+ * when/to/for ..." form (skills with non-trigger openers still load,
28
+ * but they're harder for routing to surface)
29
+ * - opener over 240 chars
30
+ *
31
+ * Hard checks prevent broken file structure; soft checks surface drift from
32
+ * authoring conventions without blocking authors who legitimately diverge.
33
+ */
34
+
35
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
36
+ import path from 'node:path';
37
+
38
+ const DESCRIPTION_MAX_CHARS = 240;
39
+ const TITLE_MAX_CHARS = 80;
40
+
41
+ // Accept any of the conventional skill-trigger openers actually in use across
42
+ // the corpus: "Use this skill when/to/for", "Use when:", "Use this when",
43
+ // "Trigger:", or a leading "When ..." that signals a triggering condition.
44
+ const TRIGGER_PATTERN = /^(?:use\s+(?:this\s+(?:skill\s+)?)?(?:when|to|for|if)\b|when\s+to\s+use\b|trigger\s*:|use\s+when\s*:)/i;
45
+
46
+ function* walk(root) {
47
+ const entries = readdirSync(root, { withFileTypes: true });
48
+ for (const entry of entries) {
49
+ const full = path.join(root, entry.name);
50
+ if (entry.isDirectory()) {
51
+ yield* walk(full);
52
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
53
+ yield full;
54
+ }
55
+ }
56
+ }
57
+
58
+ function extractTitleAndOpener(text) {
59
+ const lines = text.split(/\r?\n/);
60
+ let title = null;
61
+ let opener = null;
62
+ let inBody = false;
63
+ for (let i = 0; i < lines.length; i++) {
64
+ const line = lines[i].trim();
65
+ if (!title) {
66
+ const m = line.match(/^#\s+(.+?)\s*$/);
67
+ if (m) {
68
+ title = m[1];
69
+ inBody = true;
70
+ continue;
71
+ }
72
+ } else if (inBody && line && !line.startsWith('#') && !line.startsWith('<!--')) {
73
+ opener = line;
74
+ break;
75
+ }
76
+ }
77
+ return { title, opener };
78
+ }
79
+
80
+ /**
81
+ * Validate every skill file under one or more directories.
82
+ *
83
+ * @param {string[]|string} roots — directories to walk recursively
84
+ * @returns {{
85
+ * valid: boolean,
86
+ * errors: string[],
87
+ * warnings: string[],
88
+ * skills: Array<{ path: string, title: string|null, opener: string|null }>,
89
+ * }}
90
+ */
91
+ export function validateSkills(roots) {
92
+ const dirs = Array.isArray(roots) ? roots : [roots];
93
+ const errors = [];
94
+ const warnings = [];
95
+ const skills = [];
96
+ const seenRelative = new Map();
97
+
98
+ for (const dir of dirs) {
99
+ let exists = false;
100
+ try { exists = statSync(dir).isDirectory(); } catch { exists = false; }
101
+ if (!exists) continue;
102
+
103
+ for (const filePath of walk(dir)) {
104
+ const rel = path.relative(dir, filePath);
105
+ if (rel === 'routing.md') continue;
106
+
107
+ const prevSource = seenRelative.get(rel);
108
+ if (prevSource) {
109
+ errors.push(`duplicate skill path '${rel}' in ${prevSource} and ${dir}`);
110
+ } else {
111
+ seenRelative.set(rel, dir);
112
+ }
113
+
114
+ let raw;
115
+ try { raw = readFileSync(filePath, 'utf8'); }
116
+ catch (err) {
117
+ errors.push(`${rel}: cannot read (${err.message})`);
118
+ continue;
119
+ }
120
+
121
+ const { title, opener } = extractTitleAndOpener(raw);
122
+
123
+ if (!title) {
124
+ errors.push(`${rel}: missing H1 title (e.g. "# Skill Name")`);
125
+ } else if (title.length > TITLE_MAX_CHARS) {
126
+ errors.push(`${rel}: title exceeds ${TITLE_MAX_CHARS} chars (got ${title.length})`);
127
+ }
128
+
129
+ if (!opener) {
130
+ warnings.push(`${rel}: no trigger opener after the H1 title — routing will have nothing to match`);
131
+ } else if (!TRIGGER_PATTERN.test(opener)) {
132
+ warnings.push(`${rel}: opener should start with "Use this skill when/to/for ..." (got "${opener.slice(0, 60)}…")`);
133
+ } else if (opener.length > DESCRIPTION_MAX_CHARS) {
134
+ warnings.push(`${rel}: opener exceeds ${DESCRIPTION_MAX_CHARS} chars (got ${opener.length})`);
135
+ }
136
+
137
+ skills.push({ path: rel, title, opener });
138
+ }
139
+ }
140
+
141
+ return { valid: errors.length === 0, errors, warnings, skills };
142
+ }
@@ -0,0 +1,422 @@
1
+ /**
2
+ * lib/wireframe.mjs — `construct wireframe` command, generates Mermaid
3
+ * diagrams and low-fidelity HTML wireframes from a natural-language prompt.
4
+ *
5
+ * Honest design: this is a scaffold generator, not a model-backed visual
6
+ * synthesis engine. It picks a template appropriate to the detected
7
+ * diagram kind, seeds it with keywords extracted from the prompt, and
8
+ * writes to `.cx/wireframes/<slug>-<ts>.<ext>`. A specialist (cx-designer)
9
+ * or the user can then refine the generated file.
10
+ *
11
+ * Modern best practices honored:
12
+ * - Low-fi sketch look for HTML wireframes (the point of a wireframe is
13
+ * to delay visual-polish commitment; sketch styles signal "not final")
14
+ * - Semantic HTML5 landmarks, accessible labels, keyboard order
15
+ * - Mermaid diagrams for flows/states/sequences/ER — widely rendered by
16
+ * OpenCode, Claude Code, GitHub, Notion, etc.
17
+ * - Zero external dependencies; bundled sketch CSS inlined
18
+ * - Output is text — review-able in PRs, diff-able, version-controllable
19
+ */
20
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
21
+ import { join, resolve } from 'node:path';
22
+
23
+ const TYPES = ['flow', 'state', 'sequence', 'er', 'layout', 'user-journey'];
24
+
25
+ function slugify(text) {
26
+ return String(text || '')
27
+ .toLowerCase()
28
+ .replace(/[^a-z0-9]+/g, '-')
29
+ .replace(/^-+|-+$/g, '')
30
+ .slice(0, 60) || 'wireframe';
31
+ }
32
+
33
+ function timestamp() {
34
+ return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
35
+ }
36
+
37
+ function inferType(description, explicit) {
38
+ if (explicit) return explicit;
39
+ const text = String(description || '').toLowerCase();
40
+ if (/\b(screen|page|layout|ui|view|dashboard|form|modal|list|detail)\b/.test(text)) return 'layout';
41
+ if (/\b(state|status|transition|machine)\b/.test(text)) return 'state';
42
+ if (/\b(sequence|handshake|request|response|api call|interaction between)\b/.test(text)) return 'sequence';
43
+ if (/\b(entity|schema|database|table|relationship|foreign key)\b/.test(text)) return 'er';
44
+ if (/\b(journey|persona|step-by-step|onboarding|funnel)\b/.test(text)) return 'user-journey';
45
+ return 'flow';
46
+ }
47
+
48
+ // Stopwords that don't make good node labels.
49
+ const STOP = new Set([
50
+ 'a','an','the','of','to','for','with','from','in','on','and','or','but',
51
+ 'is','are','was','were','be','been','being','has','have','had','do','does','did',
52
+ 'this','that','these','those','i','we','you','they','he','she','it',
53
+ 'my','our','your','their','his','her','its','me','us',
54
+ 'please','build','create','design','make','show','draw','generate','wireframe','diagram',
55
+ 'flow','layout','screen','page','view',
56
+ ]);
57
+
58
+ function extractKeywords(description, max = 8) {
59
+ const words = String(description || '')
60
+ .split(/[^a-zA-Z0-9_-]+/)
61
+ .filter(Boolean)
62
+ .filter((w) => w.length > 2 && !STOP.has(w.toLowerCase()));
63
+ const seen = new Set();
64
+ const result = [];
65
+ for (const w of words) {
66
+ const key = w.toLowerCase();
67
+ if (seen.has(key)) continue;
68
+ seen.add(key);
69
+ result.push(w);
70
+ if (result.length >= max) break;
71
+ }
72
+ return result;
73
+ }
74
+
75
+ function mermaidFlow(description, keywords) {
76
+ const nodes = keywords.length > 0 ? keywords : ['Start', 'Step', 'End'];
77
+ const lines = ['graph TD'];
78
+ const ids = nodes.map((_, i) => String.fromCharCode(65 + i));
79
+ nodes.forEach((n, i) => lines.push(` ${ids[i]}[${n}]`));
80
+ for (let i = 0; i < ids.length - 1; i += 1) {
81
+ lines.push(` ${ids[i]} --> ${ids[i + 1]}`);
82
+ }
83
+ return `%% ${description}\n%% Refine nodes, add decision diamonds (C{...}), and label edges.\n${lines.join('\n')}`;
84
+ }
85
+
86
+ function mermaidState(description, keywords) {
87
+ const states = keywords.length > 0 ? keywords.slice(0, 5) : ['Idle', 'Active', 'Done'];
88
+ const lines = ['stateDiagram-v2', ' [*] --> ' + states[0]];
89
+ for (let i = 0; i < states.length - 1; i += 1) {
90
+ lines.push(` ${states[i]} --> ${states[i + 1]}`);
91
+ }
92
+ lines.push(` ${states[states.length - 1]} --> [*]`);
93
+ return `%% ${description}\n%% Add guards, actions, and error transitions.\n${lines.join('\n')}`;
94
+ }
95
+
96
+ function mermaidSequence(description, keywords) {
97
+ const actors = keywords.length >= 2 ? keywords.slice(0, 3) : ['User', 'System', 'Service'];
98
+ const lines = ['sequenceDiagram'];
99
+ actors.forEach((a) => lines.push(` participant ${a}`));
100
+ for (let i = 0; i < actors.length - 1; i += 1) {
101
+ lines.push(` ${actors[i]}->>${actors[i + 1]}: request`);
102
+ lines.push(` ${actors[i + 1]}-->>${actors[i]}: response`);
103
+ }
104
+ return `%% ${description}\n%% Replace generic labels with the real message contracts.\n${lines.join('\n')}`;
105
+ }
106
+
107
+ function mermaidER(description, keywords) {
108
+ const entities = keywords.length >= 2 ? keywords.slice(0, 3) : ['User', 'Session', 'Document'];
109
+ const lines = ['erDiagram'];
110
+ for (let i = 0; i < entities.length - 1; i += 1) {
111
+ lines.push(` ${entities[i]} ||--o{ ${entities[i + 1]} : has`);
112
+ }
113
+ entities.forEach((e) => {
114
+ lines.push(` ${e} {`);
115
+ lines.push(` string id PK`);
116
+ lines.push(` string name`);
117
+ lines.push(` }`);
118
+ });
119
+ return `%% ${description}\n%% Replace placeholder fields with real attributes and cardinality.\n${lines.join('\n')}`;
120
+ }
121
+
122
+ function mermaidUserJourney(description, keywords) {
123
+ const steps = keywords.length >= 2 ? keywords.slice(0, 5) : ['Discover', 'Evaluate', 'Try', 'Adopt'];
124
+ const lines = ['journey', ` title ${description || 'User Journey'}`];
125
+ lines.push(' section Onboarding');
126
+ steps.forEach((s, i) => {
127
+ const score = [3, 4, 5, 4, 3][i] ?? 3;
128
+ lines.push(` ${s}: ${score}: User`);
129
+ });
130
+ return `%% Refine step scores (1–5) and split into meaningful sections.\n${lines.join('\n')}`;
131
+ }
132
+
133
+ // --- HTML layout wireframe -------------------------------------------------
134
+
135
+ const SKETCH_CSS = `
136
+ :root {
137
+ --ink: #222;
138
+ --paper: #fefdf8;
139
+ --rough: #6b6b6b;
140
+ --hint: #a9a9a9;
141
+ --accent: #2c5282;
142
+ }
143
+ * { box-sizing: border-box; }
144
+ body {
145
+ margin: 0; padding: 2rem;
146
+ font-family: "Caveat", "Bradley Hand", "Comic Sans MS", cursive;
147
+ background: var(--paper);
148
+ color: var(--ink);
149
+ line-height: 1.4;
150
+ }
151
+ .wireframe { max-width: 1200px; margin: 0 auto; }
152
+ .wireframe header, .wireframe nav, .wireframe main,
153
+ .wireframe aside, .wireframe section, .wireframe footer,
154
+ .block, .card {
155
+ border: 2px solid var(--ink);
156
+ border-radius: 6px;
157
+ padding: 1rem;
158
+ margin: 0.5rem 0;
159
+ position: relative;
160
+ background: var(--paper);
161
+ box-shadow: 3px 3px 0 var(--hint);
162
+ }
163
+ .wireframe header { background: #f3f0e7; }
164
+ .label {
165
+ position: absolute; top: -10px; left: 10px;
166
+ background: var(--paper); padding: 0 6px;
167
+ font-size: 0.85em; color: var(--rough);
168
+ font-family: ui-monospace, Menlo, monospace;
169
+ }
170
+ .row { display: grid; grid-template-columns: repeat(var(--cols, 3), 1fr); gap: 1rem; }
171
+ .row-2 { --cols: 2; }
172
+ .row-3 { --cols: 3; }
173
+ .row-4 { --cols: 4; }
174
+ .sidebar-layout { display: grid; grid-template-columns: 240px 1fr; gap: 1rem; }
175
+ .placeholder {
176
+ min-height: 60px;
177
+ background: repeating-linear-gradient(
178
+ 45deg, transparent, transparent 8px,
179
+ #eee 8px, #eee 9px
180
+ );
181
+ border: 1px dashed var(--rough);
182
+ border-radius: 4px;
183
+ display: flex; align-items: center; justify-content: center;
184
+ color: var(--rough); font-size: 0.9em;
185
+ }
186
+ button, .btn {
187
+ font-family: inherit;
188
+ border: 2px solid var(--ink);
189
+ background: var(--paper);
190
+ padding: 0.5rem 1rem;
191
+ border-radius: 4px;
192
+ box-shadow: 2px 2px 0 var(--hint);
193
+ cursor: pointer;
194
+ }
195
+ button:hover { background: #f3f0e7; }
196
+ input, textarea, select {
197
+ font-family: inherit;
198
+ border: 2px solid var(--ink);
199
+ background: var(--paper);
200
+ padding: 0.4rem;
201
+ border-radius: 4px;
202
+ width: 100%;
203
+ }
204
+ h1, h2, h3 { margin: 0.25rem 0 0.5rem; }
205
+ .note {
206
+ background: #fff9c4; border: 1px dashed #c0b03f;
207
+ padding: 0.5rem 0.75rem; border-radius: 4px;
208
+ font-size: 0.9em;
209
+ }
210
+ @media (prefers-color-scheme: dark) {
211
+ :root { --ink: #e8e8e8; --paper: #1e1e1e; --rough: #9a9a9a; --hint: #4a4a4a; }
212
+ .wireframe header { background: #2a2a2a; }
213
+ .placeholder { background: repeating-linear-gradient(45deg, transparent, transparent 8px, #333 8px, #333 9px); }
214
+ button:hover { background: #2a2a2a; }
215
+ .note { background: #3a3520; color: #f0e68c; border-color: #5a5230; }
216
+ }
217
+ `;
218
+
219
+ function htmlLayout(description, keywords) {
220
+ const title = description || 'Untitled wireframe';
221
+ const sections = keywords.length > 0 ? keywords : ['Main content', 'Related items', 'Actions'];
222
+ const cards = sections.slice(0, 4).map((s) => `
223
+ <div class="block">
224
+ <span class="label">${s}</span>
225
+ <h3>${s}</h3>
226
+ <div class="placeholder">Content placeholder</div>
227
+ </div>`).join('');
228
+
229
+ return `<!doctype html>
230
+ <html lang="en">
231
+ <head>
232
+ <meta charset="utf-8" />
233
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
234
+ <title>Wireframe: ${escapeHtml(title)}</title>
235
+ <style>${SKETCH_CSS}</style>
236
+ </head>
237
+ <body>
238
+ <div class="wireframe" role="document">
239
+ <p class="note"><strong>Wireframe (low-fi).</strong> ${escapeHtml(description)}</p>
240
+
241
+ <header role="banner">
242
+ <span class="label">header</span>
243
+ <h1>${escapeHtml(title)}</h1>
244
+ <p>Primary nav / logo / global actions go here.</p>
245
+ </header>
246
+
247
+ <nav role="navigation" aria-label="primary">
248
+ <span class="label">nav</span>
249
+ <div class="row row-4">
250
+ <a href="#">Nav 1</a>
251
+ <a href="#">Nav 2</a>
252
+ <a href="#">Nav 3</a>
253
+ <a href="#">Nav 4</a>
254
+ </div>
255
+ </nav>
256
+
257
+ <div class="sidebar-layout">
258
+ <aside role="complementary">
259
+ <span class="label">sidebar</span>
260
+ <h3>Filters</h3>
261
+ <div class="placeholder">Filter / facet list</div>
262
+ </aside>
263
+
264
+ <main role="main">
265
+ <span class="label">main</span>
266
+ <div class="row row-2">${cards}
267
+ </div>
268
+ </main>
269
+ </div>
270
+
271
+ <footer role="contentinfo">
272
+ <span class="label">footer</span>
273
+ <div class="row row-3">
274
+ <div>Links</div>
275
+ <div>Legal</div>
276
+ <div>Contact</div>
277
+ </div>
278
+ </footer>
279
+ </div>
280
+ </body>
281
+ </html>
282
+ `;
283
+ }
284
+
285
+ function escapeHtml(s) {
286
+ return String(s || '')
287
+ .replace(/&/g, '&amp;')
288
+ .replace(/</g, '&lt;')
289
+ .replace(/>/g, '&gt;')
290
+ .replace(/"/g, '&quot;')
291
+ .replace(/'/g, '&#39;');
292
+ }
293
+
294
+ function wrapMermaidInMd(description, mermaid, type) {
295
+ return `# Wireframe — ${description || type}
296
+
297
+ Type: ${type}
298
+ Generated: ${new Date().toISOString()}
299
+
300
+ \`\`\`mermaid
301
+ ${mermaid}
302
+ \`\`\`
303
+
304
+ ## Next steps
305
+
306
+ - Refine node/edge labels to match the actual domain
307
+ - Add decision branches, error states, and guard conditions
308
+ - Commit the file once it reflects the real flow; treat it as part of the
309
+ architecture record, not a throwaway sketch
310
+ `;
311
+ }
312
+
313
+ export function generateWireframe({ description = '', type = null } = {}) {
314
+ const inferred = inferType(description, type);
315
+ const keywords = extractKeywords(description);
316
+
317
+ if (inferred === 'layout') {
318
+ return {
319
+ type: 'layout',
320
+ format: 'html',
321
+ content: htmlLayout(description, keywords),
322
+ extension: 'html',
323
+ };
324
+ }
325
+
326
+ let mermaid;
327
+ switch (inferred) {
328
+ case 'state': mermaid = mermaidState(description, keywords); break;
329
+ case 'sequence': mermaid = mermaidSequence(description, keywords); break;
330
+ case 'er': mermaid = mermaidER(description, keywords); break;
331
+ case 'user-journey': mermaid = mermaidUserJourney(description, keywords); break;
332
+ case 'flow':
333
+ default: mermaid = mermaidFlow(description, keywords); break;
334
+ }
335
+ return {
336
+ type: inferred,
337
+ format: 'mermaid',
338
+ content: wrapMermaidInMd(description, mermaid, inferred),
339
+ extension: 'md',
340
+ };
341
+ }
342
+
343
+ export function printHelp() {
344
+ console.log(`Usage: construct wireframe <description> [options]
345
+
346
+ Generates a low-fidelity wireframe from a description. Auto-detects the
347
+ best format (Mermaid diagram or HTML sketch) based on the description,
348
+ or force it with --type.
349
+
350
+ Options:
351
+ --type <t> flow (default) | state | sequence | er | layout | user-journey
352
+ --out <path> Save path (default: .cx/wireframes/<slug>-<ts>.<ext>)
353
+ --stdout Print to stdout instead of writing a file
354
+ -h, --help Show this message
355
+
356
+ Formats:
357
+ Mermaid diagrams (flow/state/sequence/er/user-journey) render natively
358
+ in OpenCode, Claude Code, GitHub, Notion, and most markdown viewers.
359
+ HTML wireframes (layout) open in any browser with a low-fi sketch style
360
+ that signals "not final design."
361
+
362
+ Examples:
363
+ construct wireframe "user signup flow"
364
+ construct wireframe "database schema for project iverson" --type er
365
+ construct wireframe "dashboard with sidebar filters" --type layout
366
+ construct wireframe "oauth handshake" --type sequence --stdout
367
+ `);
368
+ }
369
+
370
+ function parseArgs(argv) {
371
+ const options = { type: null, out: null, stdout: false, description: [] };
372
+ for (let i = 0; i < argv.length; i += 1) {
373
+ const arg = argv[i];
374
+ if (arg === '--help' || arg === '-h') { options.help = true; continue; }
375
+ if (arg === '--stdout') { options.stdout = true; continue; }
376
+ if (arg === '--type') { options.type = argv[++i]; continue; }
377
+ if (arg.startsWith('--type=')) { options.type = arg.slice(7); continue; }
378
+ if (arg === '--out') { options.out = argv[++i]; continue; }
379
+ if (arg.startsWith('--out=')) { options.out = arg.slice(6); continue; }
380
+ options.description.push(arg);
381
+ }
382
+ options.description = options.description.join(' ').trim();
383
+ return options;
384
+ }
385
+
386
+ export async function runWireframeCli(argv = [], { cwd = process.cwd() } = {}) {
387
+ const options = parseArgs(argv);
388
+ if (options.help || !options.description) {
389
+ printHelp();
390
+ if (!options.help && !options.description) process.exit(1);
391
+ return;
392
+ }
393
+ if (options.type && !TYPES.includes(options.type)) {
394
+ console.error(`Unknown type: ${options.type}. Valid: ${TYPES.join(', ')}`);
395
+ process.exit(1);
396
+ }
397
+
398
+ const result = generateWireframe({ description: options.description, type: options.type });
399
+
400
+ if (options.stdout) {
401
+ process.stdout.write(result.content);
402
+ return;
403
+ }
404
+
405
+ const slug = slugify(options.description);
406
+ const fileName = `${slug}-${timestamp()}.${result.extension}`;
407
+ const outPath = options.out
408
+ ? resolve(cwd, options.out)
409
+ : join(cwd, '.cx', 'wireframes', fileName);
410
+
411
+ const dir = outPath.slice(0, outPath.lastIndexOf('/'));
412
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
413
+ writeFileSync(outPath, result.content, 'utf8');
414
+
415
+ console.log(`Wireframe (${result.type}, ${result.format}) written to:`);
416
+ console.log(` ${outPath}`);
417
+ if (result.format === 'html') {
418
+ console.log(`\nOpen it with: open "${outPath}"`);
419
+ } else {
420
+ console.log(`\nRender inline: paste into any Mermaid-aware viewer (GitHub, Notion, Obsidian, OpenCode).`);
421
+ }
422
+ }