@geraldmaron/construct 1.0.0 → 1.0.2

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 +9 -6
  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 +1000 -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 +523 -36
  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
@@ -0,0 +1,168 @@
1
+ /**
2
+ * lib/server/auth.mjs — Dashboard authentication middleware.
3
+ *
4
+ * Token-based auth for the Construct dashboard. A single shared bearer token
5
+ * is stored at ~/.construct/config.env under CONSTRUCT_DASHBOARD_TOKEN. If no
6
+ * token is configured the dashboard runs open (localhost-only by design). The
7
+ * browser receives a short-lived session cookie on successful token exchange so
8
+ * subsequent requests don't need to re-send the Authorization header.
9
+ *
10
+ * API routes check: Authorization: Bearer <token> OR session cookie.
11
+ * The login page POSTs to /api/auth/login and receives a Set-Cookie response.
12
+ * GET /api/auth/status returns { configured, authenticated }.
13
+ */
14
+
15
+ import { randomBytes, timingSafeEqual } from 'crypto';
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
17
+ import { join } from 'path';
18
+ import { homedir } from 'os';
19
+
20
+ const HOME = homedir();
21
+ const CONFIG_ENV = join(HOME, '.construct', 'config.env');
22
+ const SESSION_COOKIE = 'cx_session';
23
+ const SESSION_TTL_MS = 8 * 60 * 60 * 1000; // 8 hours
24
+ const SUPPORTED_OAUTH_PROVIDERS = new Set(['github', 'google']);
25
+
26
+ // ── In-memory session store ────────────────────────────────────────────────
27
+ // Sessions are ephemeral — cleared on server restart. Acceptable for a local
28
+ // single-user dashboard; no persistence needed.
29
+ const sessions = new Map(); // token → { expiresAt }
30
+
31
+ function parseEnvFile(filePath) {
32
+ if (!existsSync(filePath)) return {};
33
+ const out = {};
34
+ for (const line of readFileSync(filePath, 'utf8').split('\n')) {
35
+ const m = line.trim().match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
36
+ if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, '');
37
+ }
38
+ return out;
39
+ }
40
+
41
+ // ── Token management ───────────────────────────────────────────────────────
42
+
43
+ export function getDashboardToken() {
44
+ const env = parseEnvFile(CONFIG_ENV);
45
+ return env.CONSTRUCT_DASHBOARD_TOKEN || null;
46
+ }
47
+
48
+ export function setDashboardToken(token) {
49
+ mkdirSync(join(HOME, '.construct'), { recursive: true });
50
+ const env = existsSync(CONFIG_ENV) ? readFileSync(CONFIG_ENV, 'utf8') : '';
51
+ const lines = env.split('\n').filter(l => !l.startsWith('CONSTRUCT_DASHBOARD_TOKEN='));
52
+ lines.push(`CONSTRUCT_DASHBOARD_TOKEN=${token}`);
53
+ writeFileSync(CONFIG_ENV, lines.filter(Boolean).join('\n') + '\n', 'utf8');
54
+ }
55
+
56
+ export function generateToken() {
57
+ return randomBytes(32).toString('hex');
58
+ }
59
+
60
+ // ── Auth state ─────────────────────────────────────────────────────────────
61
+
62
+ export function isAuthConfigured() {
63
+ return Boolean(getDashboardToken());
64
+ }
65
+
66
+ export function getAuthConfig(env = process.env) {
67
+ const rawMode = String(env.CONSTRUCT_AUTH_MODE || '').trim().toLowerCase();
68
+ const mode = rawMode === 'oauth' ? 'oauth' : 'token';
69
+ const providers = String(env.CONSTRUCT_OAUTH_PROVIDERS || '')
70
+ .split(',')
71
+ .map((value) => value.trim().toLowerCase())
72
+ .filter(Boolean)
73
+ .filter((value, index, array) => array.indexOf(value) === index)
74
+ .filter((value) => SUPPORTED_OAUTH_PROVIDERS.has(value));
75
+ const rolesEnabled = String(env.CONSTRUCT_AUTH_ROLES_ENABLED || '').trim() === '1';
76
+
77
+ return {
78
+ mode,
79
+ providers,
80
+ rolesEnabled,
81
+ sessionBackend: 'memory',
82
+ tokenConfigured: isAuthConfigured(),
83
+ oauthConfigured: providers.length > 0,
84
+ supportsTokenLogin: true,
85
+ supportsOauthLogin: providers.length > 0,
86
+ };
87
+ }
88
+
89
+ function safeCompare(a, b) {
90
+ try {
91
+ const ab = Buffer.from(a, 'utf8');
92
+ const bb = Buffer.from(b, 'utf8');
93
+ if (ab.length !== bb.length) {
94
+ // Still run the comparison to avoid timing leak
95
+ timingSafeEqual(ab, Buffer.alloc(ab.length));
96
+ return false;
97
+ }
98
+ return timingSafeEqual(ab, bb);
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ export function validateToken(candidate) {
105
+ const stored = getDashboardToken();
106
+ if (!stored) return true; // No token configured → open
107
+ if (!candidate) return false;
108
+ return safeCompare(candidate, stored);
109
+ }
110
+
111
+ // ── Session cookie helpers ─────────────────────────────────────────────────
112
+
113
+ function parseCookies(header) {
114
+ const out = {};
115
+ if (!header) return out;
116
+ for (const part of header.split(';')) {
117
+ const [k, ...rest] = part.trim().split('=');
118
+ if (k) out[k.trim()] = rest.join('=').trim();
119
+ }
120
+ return out;
121
+ }
122
+
123
+ export function createSession() {
124
+ const token = randomBytes(32).toString('hex');
125
+ sessions.set(token, { expiresAt: Date.now() + SESSION_TTL_MS });
126
+ return token;
127
+ }
128
+
129
+ export function validateSession(cookieHeader) {
130
+ const cookies = parseCookies(cookieHeader);
131
+ const token = cookies[SESSION_COOKIE];
132
+ if (!token) return false;
133
+ const session = sessions.get(token);
134
+ if (!session) return false;
135
+ if (Date.now() > session.expiresAt) { sessions.delete(token); return false; }
136
+ return true;
137
+ }
138
+
139
+ export function sessionCookieHeader(token) {
140
+ const maxAge = Math.floor(SESSION_TTL_MS / 1000);
141
+ return `${SESSION_COOKIE}=${token}; HttpOnly; SameSite=Strict; Max-Age=${maxAge}; Path=/`;
142
+ }
143
+
144
+ export function clearSessionCookieHeader() {
145
+ return `${SESSION_COOKIE}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/`;
146
+ }
147
+
148
+ // ── Middleware ─────────────────────────────────────────────────────────────
149
+
150
+ /**
151
+ * Returns true if the request is authenticated (or auth is not configured).
152
+ * Checks: session cookie first, then Authorization: Bearer header.
153
+ */
154
+ export function isAuthenticated(req) {
155
+ if (!isAuthConfigured()) return true;
156
+ if (validateSession(req.headers.cookie)) return true;
157
+ const authHeader = req.headers.authorization || '';
158
+ const bearer = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : null;
159
+ return validateToken(bearer);
160
+ }
161
+
162
+ /**
163
+ * Send a 401 JSON response.
164
+ */
165
+ export function rejectUnauthorized(res) {
166
+ res.writeHead(401, { 'Content-Type': 'application/json' });
167
+ res.end(JSON.stringify({ error: 'Unauthorized', hint: 'Set Authorization: Bearer <CONSTRUCT_DASHBOARD_TOKEN>' }));
168
+ }
@@ -0,0 +1,336 @@
1
+ /**
2
+ * lib/server/chat.mjs — Dashboard chat endpoint handler.
3
+ *
4
+ * Handles POST /api/chat (non-streaming JSON) and GET /api/chat/stream (SSE).
5
+ * Shells out to the `claude --print` CLI and streams the response back as
6
+ * Server-Sent Events. Returns a structured fallback if the CLI is unavailable.
7
+ *
8
+ * Message history is kept per conversation ID in memory (cleared on restart).
9
+ * The client sends { id?, message } — omitting id starts a new conversation.
10
+ */
11
+
12
+ import { spawn, execSync } from 'child_process';
13
+ import { randomBytes } from 'crypto';
14
+ import { existsSync, readFileSync } from 'fs';
15
+ import { join } from 'path';
16
+ import { homedir } from 'os';
17
+ import { fileURLToPath } from 'url';
18
+ import { dirname, resolve } from 'path';
19
+
20
+ const HOME = homedir();
21
+ const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
22
+
23
+ // ── Session-end optimize hook ──────────────────────────────────────────────
24
+ // After a chat session closes, check whether enough new quality scores have
25
+ // accumulated in the performance review to warrant running optimize.
26
+ // Fires asynchronously — never blocks the response.
27
+
28
+ let _sessionsSinceOptimize = 0;
29
+ const OPTIMIZE_EVERY_N_SESSIONS = parseInt(process.env.CX_OPTIMIZE_INTERVAL ?? '5', 10);
30
+
31
+ function maybeRunOptimize() {
32
+ _sessionsSinceOptimize++;
33
+ if (_sessionsSinceOptimize < OPTIMIZE_EVERY_N_SESSIONS) return;
34
+ _sessionsSinceOptimize = 0;
35
+
36
+ // Spawn optimize --list to see if any agents are below threshold
37
+ const script = join(ROOT_DIR, 'scripts', 'optimize.mjs');
38
+ if (!existsSync(script)) return;
39
+
40
+ const proc = spawn(process.execPath, [script, '--list', '--threshold=0.7'], {
41
+ cwd: ROOT_DIR,
42
+ env: { ...process.env },
43
+ stdio: ['ignore', 'pipe', 'pipe'],
44
+ });
45
+
46
+ let out = '';
47
+ proc.stdout.on('data', d => { out += d; });
48
+ proc.on('close', () => {
49
+ // Find agents flagged below threshold and run optimize on each
50
+ const flagged = out.split('\n')
51
+ .filter(l => l.includes('← below threshold'))
52
+ .map(l => l.trim().split(/\s+/)[0])
53
+ .filter(Boolean);
54
+
55
+ for (const agent of flagged) {
56
+ const opt = spawn(process.execPath, [script, agent, `--threshold=0.7`], {
57
+ cwd: ROOT_DIR,
58
+ env: { ...process.env },
59
+ stdio: 'ignore',
60
+ detached: true,
61
+ });
62
+ opt.unref();
63
+ }
64
+ });
65
+ proc.unref();
66
+ }
67
+
68
+ // ── Conversation store ─────────────────────────────────────────────────────
69
+ const conversations = new Map();
70
+ const CONV_TTL_MS = 2 * 60 * 60 * 1000;
71
+
72
+ function pruneConversations() {
73
+ const now = Date.now();
74
+ for (const [id, conv] of conversations) {
75
+ if (now - conv.lastAt > CONV_TTL_MS) conversations.delete(id);
76
+ }
77
+ }
78
+
79
+ export function getOrCreateConversation(id) {
80
+ pruneConversations();
81
+ if (id && conversations.has(id)) {
82
+ const conv = conversations.get(id);
83
+ conv.lastAt = Date.now();
84
+ return conv;
85
+ }
86
+ const newId = randomBytes(12).toString('hex');
87
+ const conv = { id: newId, messages: [], createdAt: Date.now(), lastAt: Date.now() };
88
+ conversations.set(newId, conv);
89
+ return conv;
90
+ }
91
+
92
+ // ── CLI detection ──────────────────────────────────────────────────────────
93
+
94
+ let _cliCmd = undefined; // undefined = not yet checked, false = not found, string = found
95
+
96
+ function detectCli() {
97
+ if (_cliCmd !== undefined) return _cliCmd;
98
+ for (const cmd of ['claude', 'anthropic']) {
99
+ try {
100
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
101
+ _cliCmd = cmd;
102
+ return _cliCmd;
103
+ } catch { /* not found */ }
104
+ }
105
+ _cliCmd = false;
106
+ return _cliCmd;
107
+ }
108
+
109
+ // Read-only Construct MCP tools that are pre-authorized in dashboard chat.
110
+ // Destructive tools (storage_reset, delete_ingested_artifacts, ingest_document)
111
+ // are deliberately omitted and still require explicit approval.
112
+ const ALLOWED_CONSTRUCT_TOOLS = [
113
+ 'mcp__construct-mcp__provider_fetch',
114
+ 'mcp__construct-mcp__knowledge_search',
115
+ 'mcp__construct-mcp__memory_search',
116
+ 'mcp__construct-mcp__memory_recent',
117
+ 'mcp__construct-mcp__project_context',
118
+ 'mcp__construct-mcp__session_list',
119
+ 'mcp__construct-mcp__session_load',
120
+ 'mcp__construct-mcp__session_search',
121
+ 'mcp__construct-mcp__agent_health',
122
+ 'mcp__construct-mcp__storage_status',
123
+ 'mcp__construct-mcp__list_skills',
124
+ 'mcp__construct-mcp__get_skill',
125
+ 'mcp__construct-mcp__list_templates',
126
+ 'mcp__construct-mcp__get_template',
127
+ 'mcp__construct-mcp__list_teams',
128
+ 'mcp__construct-mcp__get_team',
129
+ 'mcp__construct-mcp__agent_contract',
130
+ 'mcp__construct-mcp__orchestration_policy',
131
+ 'mcp__construct-mcp__search_skills',
132
+ 'mcp__construct-mcp__efficiency_snapshot',
133
+ 'mcp__construct-mcp__session_usage',
134
+ 'mcp__construct-mcp__list_schema_artifacts',
135
+ 'mcp__construct-mcp__extract_document_text',
136
+ 'mcp__construct-mcp__summarize_diff',
137
+ 'mcp__construct-mcp__scan_file',
138
+ 'Read', 'Glob', 'Grep',
139
+ ];
140
+
141
+ function buildCliArgs() {
142
+ return [
143
+ '--print',
144
+ '--permission-mode', 'default',
145
+ '--allowedTools', ...ALLOWED_CONSTRUCT_TOOLS,
146
+ ];
147
+ }
148
+
149
+ // ── Prompt builder ─────────────────────────────────────────────────────────
150
+
151
+ function buildPrompt(messages, newMessage, rootDir) {
152
+ const lines = [];
153
+
154
+ // Attach project context prefix on first message
155
+ if (messages.length === 0) {
156
+ lines.push(
157
+ '[System]',
158
+ 'You are Construct, the operator-facing chat for this project. The MCP tools',
159
+ '`provider_fetch`, `knowledge_search`, `memory_search`, `memory_recent`,',
160
+ '`project_context`, and `session_*` are pre-authorized read-only lookups.',
161
+ 'Call them directly without asking for approval. They route to the configured',
162
+ 'GitHub/Jira/Slack/local sources via authority-guarded providers.',
163
+ 'For "what is X" / "tell me about X" queries about a project name, repo, or',
164
+ 'concept, your first action is `provider_fetch({ query: "<user query>" })`.',
165
+ '',
166
+ );
167
+ const contextFile = join(rootDir || process.cwd(), '.cx', 'context.md');
168
+ if (existsSync(contextFile)) {
169
+ try {
170
+ const ctx = readFileSync(contextFile, 'utf8').slice(0, 1500);
171
+ lines.push(`[Project context]\n${ctx}\n`);
172
+ } catch { /* ignore */ }
173
+ }
174
+ }
175
+
176
+ // Include last 6 turns of history
177
+ for (const m of messages.slice(-6)) {
178
+ lines.push(`${m.role === 'user' ? 'Human' : 'Assistant'}: ${m.content}`);
179
+ }
180
+ lines.push(`Human: ${newMessage}`, '', 'Assistant:');
181
+ return lines.join('\n');
182
+ }
183
+
184
+ // ── SSE helpers ────────────────────────────────────────────────────────────
185
+
186
+ function sseWrite(res, type, text, id) {
187
+ res.write(`data: ${JSON.stringify({ type, text, id })}\n\n`);
188
+ }
189
+
190
+ function cliMissingResponse(res, conv) {
191
+ sseWrite(res, 'chunk',
192
+ '**Construct chat** requires the `claude` CLI.\n\n' +
193
+ 'Install: `npm install -g @anthropic-ai/claude-code`\n\n' +
194
+ 'Then authenticate once with `claude` before using the dashboard chat.',
195
+ conv.id);
196
+ sseWrite(res, 'done', '', conv.id);
197
+ conv.messages.push({ role: 'assistant', content: '[claude CLI not available]' });
198
+ res.end();
199
+ }
200
+
201
+ // ── Handlers ───────────────────────────────────────────────────────────────
202
+
203
+ /**
204
+ * GET /api/chat/stream?message=<text>[&id=<convId>]
205
+ * Streams response as SSE: data: { type, text, id }
206
+ * type = 'chunk' | 'done' | 'error'
207
+ */
208
+ export function handleChatStream(req, res, { rootDir } = {}) {
209
+ const url = new URL(req.url, 'http://localhost');
210
+ const message = url.searchParams.get('message');
211
+ const convId = url.searchParams.get('id') || undefined;
212
+
213
+ if (!message) {
214
+ res.writeHead(400, { 'Content-Type': 'application/json' });
215
+ res.end(JSON.stringify({ error: 'message query param required' }));
216
+ return;
217
+ }
218
+
219
+ res.writeHead(200, {
220
+ 'Content-Type': 'text/event-stream',
221
+ 'Cache-Control': 'no-cache',
222
+ 'Connection': 'keep-alive',
223
+ });
224
+
225
+ const conv = getOrCreateConversation(convId);
226
+ conv.messages.push({ role: 'user', content: message });
227
+
228
+ const cli = detectCli();
229
+ if (!cli) { cliMissingResponse(res, conv); return; }
230
+
231
+ const prompt = buildPrompt(conv.messages.slice(0, -1), message, rootDir);
232
+ const proc = spawn(cli, buildCliArgs(), {
233
+ cwd: rootDir || process.cwd(),
234
+ env: { ...process.env },
235
+ stdio: ['pipe', 'pipe', 'pipe'],
236
+ });
237
+
238
+ proc.stdin.write(prompt);
239
+ proc.stdin.end();
240
+
241
+ let fullResponse = '';
242
+
243
+ proc.stdout.on('data', chunk => {
244
+ const text = chunk.toString();
245
+ fullResponse += text;
246
+ sseWrite(res, 'chunk', text, conv.id);
247
+ });
248
+
249
+ proc.on('close', code => {
250
+ if (code !== 0 && !fullResponse) {
251
+ sseWrite(res, 'error', `Chat exited with code ${code}. Check that \`claude\` is authenticated.`, conv.id);
252
+ } else {
253
+ sseWrite(res, 'done', '', conv.id);
254
+ }
255
+ conv.messages.push({ role: 'assistant', content: fullResponse || '[no response]' });
256
+ res.end();
257
+ maybeRunOptimize();
258
+ });
259
+
260
+ proc.on('error', err => {
261
+ sseWrite(res, 'error', 'Failed to start chat: ' + err.message, conv.id);
262
+ res.end();
263
+ });
264
+
265
+ req.on('close', () => { try { proc.kill(); } catch { /* ignore */ } });
266
+ }
267
+
268
+ /**
269
+ * POST /api/chat — non-streaming, full response as JSON.
270
+ * Body: { id?, message } → { id, reply }
271
+ */
272
+ export function handleChat(req, res, { rootDir } = {}) {
273
+ let body = '';
274
+ req.on('data', chunk => { body += chunk; });
275
+ req.on('end', () => {
276
+ try {
277
+ const data = JSON.parse(body || '{}');
278
+ if (!data.message) throw new Error('message required');
279
+
280
+ const conv = getOrCreateConversation(data.id);
281
+ conv.messages.push({ role: 'user', content: data.message });
282
+
283
+ const cli = detectCli();
284
+ if (!cli) {
285
+ const reply = 'The `claude` CLI is not installed. Run: npm install -g @anthropic-ai/claude-code';
286
+ conv.messages.push({ role: 'assistant', content: reply });
287
+ res.writeHead(200, { 'Content-Type': 'application/json' });
288
+ res.end(JSON.stringify({ id: conv.id, reply, cliMissing: true }));
289
+ return;
290
+ }
291
+
292
+ const prompt = buildPrompt(conv.messages.slice(0, -1), data.message, rootDir);
293
+ const proc = spawn(cli, buildCliArgs(), {
294
+ cwd: rootDir || process.cwd(),
295
+ env: { ...process.env },
296
+ stdio: ['pipe', 'pipe', 'pipe'],
297
+ });
298
+
299
+ proc.stdin.write(prompt);
300
+ proc.stdin.end();
301
+
302
+ let reply = '';
303
+ proc.stdout.on('data', c => { reply += c.toString(); });
304
+ proc.on('close', code => {
305
+ const text = reply.trim() || (code !== 0 ? `[exit ${code}]` : '[empty response]');
306
+ conv.messages.push({ role: 'assistant', content: text });
307
+ res.writeHead(200, { 'Content-Type': 'application/json' });
308
+ res.end(JSON.stringify({ id: conv.id, reply: text }));
309
+ maybeRunOptimize();
310
+ });
311
+ proc.on('error', err => {
312
+ res.writeHead(500, { 'Content-Type': 'application/json' });
313
+ res.end(JSON.stringify({ error: err.message }));
314
+ });
315
+ } catch (err) {
316
+ res.writeHead(400, { 'Content-Type': 'application/json' });
317
+ res.end(JSON.stringify({ error: err.message }));
318
+ }
319
+ });
320
+ }
321
+
322
+ /**
323
+ * GET /api/chat/history?id=<convId>
324
+ */
325
+ export function handleChatHistory(req, res) {
326
+ const url = new URL(req.url, 'http://localhost');
327
+ const id = url.searchParams.get('id');
328
+ if (!id || !conversations.has(id)) {
329
+ res.writeHead(200, { 'Content-Type': 'application/json' });
330
+ res.end(JSON.stringify({ id: null, messages: [] }));
331
+ return;
332
+ }
333
+ const conv = conversations.get(id);
334
+ res.writeHead(200, { 'Content-Type': 'application/json' });
335
+ res.end(JSON.stringify({ id: conv.id, messages: conv.messages }));
336
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * lib/server/cors.mjs — origin allowlist for the dashboard server.
3
+ *
4
+ * The default is exact-match against the dashboard's own origin (single-host
5
+ * self-hosted install). Operators can pass extra origins via the env var
6
+ * `CONSTRUCT_DASHBOARD_ORIGINS` as a comma-separated list.
7
+ *
8
+ * CONSTRUCT_DASHBOARD_ORIGINS="https://my-laptop.tail-net.ts.net,https://construct.example.com"
9
+ *
10
+ * Wildcard origins are NOT supported. `Access-Control-Allow-Origin: *` is
11
+ * never set — paid deployments behind a private network or behind an ALB
12
+ * with a CloudFront distribution declare exactly which origins may issue
13
+ * requests.
14
+ *
15
+ * Exports:
16
+ * - applyCors(req, res, opts) : sets the right Access-Control-* headers
17
+ * on a request. Returns true when the
18
+ * request was a preflight (OPTIONS) and
19
+ * the caller should respond with 204.
20
+ * - originAllowed(req, opts) : true/false. Useful when middleware wants
21
+ * to enforce same-origin without setting
22
+ * any headers (e.g. on SSE streams).
23
+ */
24
+
25
+ const HEADER_NAME = 'access-control-allow-origin';
26
+
27
+ function parseOrigins(env) {
28
+ const list = (env.CONSTRUCT_DASHBOARD_ORIGINS || '')
29
+ .split(',')
30
+ .map((s) => s.trim())
31
+ .filter(Boolean);
32
+ return new Set(list);
33
+ }
34
+
35
+ function selfOrigin(req) {
36
+ const proto = req.headers['x-forwarded-proto'] || (req.socket?.encrypted ? 'https' : 'http');
37
+ const host = req.headers['x-forwarded-host'] || req.headers.host;
38
+ if (!host) return null;
39
+ return `${proto}://${host}`;
40
+ }
41
+
42
+ export function originAllowed(req, { env = process.env } = {}) {
43
+ const origin = req.headers.origin;
44
+ if (!origin) return true;
45
+ const allowed = parseOrigins(env);
46
+ allowed.add(selfOrigin(req));
47
+ return allowed.has(origin);
48
+ }
49
+
50
+ /**
51
+ * Set CORS headers on a response. Returns `true` for preflight requests
52
+ * that the caller should answer with 204 immediately.
53
+ */
54
+ export function applyCors(req, res, { env = process.env } = {}) {
55
+ const origin = req.headers.origin;
56
+ if (origin && originAllowed(req, { env })) {
57
+ res.setHeader(HEADER_NAME, origin);
58
+ res.setHeader('Vary', 'Origin');
59
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
60
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
61
+ res.setHeader(
62
+ 'Access-Control-Allow-Headers',
63
+ 'Content-Type, Authorization, X-Construct-Csrf, X-Construct-Token'
64
+ );
65
+ }
66
+ if (req.method === 'OPTIONS') {
67
+ if (origin && !originAllowed(req, { env })) {
68
+ res.statusCode = 403;
69
+ res.end('origin not allowed');
70
+ return true;
71
+ }
72
+ res.statusCode = 204;
73
+ res.end();
74
+ return true;
75
+ }
76
+ return false;
77
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * lib/server/csrf.mjs — double-submit CSRF protection for state-mutating routes.
3
+ *
4
+ * Every non-GET request must include a `X-Construct-Csrf` header whose value
5
+ * matches the `cx_csrf` cookie. The dashboard mints the cookie on first
6
+ * authenticated load (the cookie value is a 256-bit random hex string,
7
+ * SameSite=Lax, HttpOnly=false so the SPA can read it). Issuing the header
8
+ * proves the request originated from a page that received the cookie, which
9
+ * a cross-origin attacker cannot forge.
10
+ *
11
+ * Routes that legitimately accept unauthenticated requests (webhooks,
12
+ * /api/auth/login) opt out by passing `{ skipMethods: ['POST'] }` or by
13
+ * checking `csrfRequired(url)` themselves.
14
+ */
15
+
16
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
17
+
18
+ const COOKIE_NAME = 'cx_csrf';
19
+ const HEADER_NAME = 'x-construct-csrf';
20
+ const TOKEN_BYTES = 32;
21
+ const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
22
+
23
+ function parseCookies(req) {
24
+ const out = {};
25
+ const header = req.headers.cookie;
26
+ if (!header) return out;
27
+ for (const part of header.split(';')) {
28
+ const [k, ...rest] = part.split('=');
29
+ if (!k) continue;
30
+ out[k.trim()] = rest.join('=').trim();
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function bufFromHex(s) {
36
+ if (typeof s !== 'string' || s.length % 2 !== 0) return null;
37
+ try { return Buffer.from(s, 'hex'); } catch { return null; }
38
+ }
39
+
40
+ export function mintCsrfToken() {
41
+ return randomBytes(TOKEN_BYTES).toString('hex');
42
+ }
43
+
44
+ /**
45
+ * Ensure the response has the `cx_csrf` cookie. If the request already had
46
+ * one, leave it alone. Otherwise mint and set it.
47
+ *
48
+ * @returns {string} the token now in play
49
+ */
50
+ export function ensureCsrfCookie(req, res) {
51
+ const existing = parseCookies(req)[COOKIE_NAME];
52
+ if (existing) return existing;
53
+ const token = mintCsrfToken();
54
+ const sec = req.socket?.encrypted ? '; Secure' : '';
55
+ res.setHeader(
56
+ 'Set-Cookie',
57
+ `${COOKIE_NAME}=${token}; Path=/; SameSite=Lax; Max-Age=43200${sec}`
58
+ );
59
+ return token;
60
+ }
61
+
62
+ /**
63
+ * Verify the `X-Construct-Csrf` header matches the `cx_csrf` cookie. Returns
64
+ * true on safe methods (GET/HEAD/OPTIONS) without checking. Returns true on
65
+ * URLs that opt out via `skip(url)`.
66
+ */
67
+ export function verifyCsrf(req, { skip } = {}) {
68
+ if (SAFE_METHODS.has(req.method)) return true;
69
+ if (typeof skip === 'function' && skip(req.url || '')) return true;
70
+ const headerVal = req.headers[HEADER_NAME];
71
+ const cookieVal = parseCookies(req)[COOKIE_NAME];
72
+ if (!headerVal || !cookieVal) return false;
73
+ if (headerVal.length !== cookieVal.length) return false;
74
+ const a = bufFromHex(headerVal);
75
+ const b = bufFromHex(cookieVal);
76
+ if (!a || !b || a.length !== b.length) return false;
77
+ try { return timingSafeEqual(a, b); } catch { return false; }
78
+ }
79
+
80
+ /**
81
+ * Default skip predicate — webhooks and auth/login may legitimately accept
82
+ * cross-origin POSTs without the CSRF dance.
83
+ */
84
+ export function defaultSkip(url) {
85
+ if (!url) return false;
86
+ return (
87
+ url.startsWith('/api/webhooks/') ||
88
+ url === '/api/auth/login' ||
89
+ url === '/api/slack/commands'
90
+ );
91
+ }