@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,158 @@
1
+ /**
2
+ * lib/intake/filesystem-queue.mjs — filesystem adapter for the IntakeQueue interface.
3
+ *
4
+ * Backs the `.cx/intake/{pending,processed,skipped}/` layout used in solo
5
+ * mode. Implements the contract defined in lib/intake/queue.mjs: enqueue,
6
+ * listPending, read, markProcessed, markSkipped, reopen, count. The
7
+ * Postgres adapter (for team and enterprise modes) lives alongside and
8
+ * implements the same shape so callers do not branch on backend.
9
+ */
10
+
11
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
+ import path from 'node:path';
13
+
14
+ const QUEUE_SUBDIR = '.cx/intake';
15
+
16
+ export function queueRoot(rootDir) {
17
+ return path.join(rootDir, QUEUE_SUBDIR);
18
+ }
19
+
20
+ export function pendingDir(rootDir) {
21
+ return path.join(queueRoot(rootDir), 'pending');
22
+ }
23
+
24
+ export function processedDir(rootDir) {
25
+ return path.join(queueRoot(rootDir), 'processed');
26
+ }
27
+
28
+ export function skippedDir(rootDir) {
29
+ return path.join(queueRoot(rootDir), 'skipped');
30
+ }
31
+
32
+ function slugify(value) {
33
+ return String(value || 'untitled')
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9]+/g, '-')
36
+ .replace(/^-+|-+$/g, '')
37
+ .slice(0, 60) || 'untitled';
38
+ }
39
+
40
+ let counter = 0;
41
+ function timestamp() {
42
+ // Milliseconds + a per-process counter to guarantee uniqueness even when
43
+ // two enqueues fire in the same millisecond.
44
+ counter = (counter + 1) % 1000;
45
+ const c = String(counter).padStart(3, '0');
46
+ return `${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 23)}-${c}`;
47
+ }
48
+
49
+ export class FilesystemIntakeQueue {
50
+ constructor(rootDir) {
51
+ if (!rootDir) throw new Error('FilesystemIntakeQueue: rootDir is required');
52
+ this.rootDir = rootDir;
53
+ }
54
+
55
+ enqueue(entry) {
56
+ if (!entry?.intake?.sourcePath) throw new Error('enqueue: entry.intake.sourcePath is required');
57
+ const dir = pendingDir(this.rootDir);
58
+ mkdirSync(dir, { recursive: true });
59
+
60
+ const ts = timestamp();
61
+ const slug = slugify(path.basename(entry.intake.sourcePath, path.extname(entry.intake.sourcePath)));
62
+ const id = `${ts}-${slug}`;
63
+ const filePath = path.join(dir, `${id}.json`);
64
+
65
+ const payload = { id, createdAt: new Date().toISOString(), status: 'pending', ...entry };
66
+ writeFileSync(filePath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
67
+ return { id, filePath };
68
+ }
69
+
70
+ listPending() {
71
+ const dir = pendingDir(this.rootDir);
72
+ if (!existsSync(dir)) return [];
73
+ return readdirSync(dir)
74
+ .filter((name) => name.endsWith('.json'))
75
+ .map((name) => {
76
+ const filePath = path.join(dir, name);
77
+ try {
78
+ const data = JSON.parse(readFileSync(filePath, 'utf8'));
79
+ return { ...data, filePath };
80
+ } catch {
81
+ return null;
82
+ }
83
+ })
84
+ .filter(Boolean)
85
+ .sort((a, b) => (a.createdAt || '').localeCompare(b.createdAt || ''));
86
+ }
87
+
88
+ count() {
89
+ const dir = pendingDir(this.rootDir);
90
+ if (!existsSync(dir)) return 0;
91
+ return readdirSync(dir).filter((name) => name.endsWith('.json')).length;
92
+ }
93
+
94
+ read(id) {
95
+ for (const dir of [pendingDir(this.rootDir), processedDir(this.rootDir), skippedDir(this.rootDir)]) {
96
+ const filePath = path.join(dir, `${id}.json`);
97
+ if (existsSync(filePath)) {
98
+ const data = JSON.parse(readFileSync(filePath, 'utf8'));
99
+ return { ...data, filePath };
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ markProcessed(id, { processedBy = 'unknown', notes = '' } = {}) {
106
+ const src = path.join(pendingDir(this.rootDir), `${id}.json`);
107
+ if (!existsSync(src)) throw new Error(`markProcessed: no pending entry ${id}`);
108
+ const data = JSON.parse(readFileSync(src, 'utf8'));
109
+ data.status = 'processed';
110
+ data.processedAt = new Date().toISOString();
111
+ data.processedBy = processedBy;
112
+ if (notes) data.notes = notes;
113
+
114
+ const dst = path.join(processedDir(this.rootDir), `${id}.json`);
115
+ mkdirSync(path.dirname(dst), { recursive: true });
116
+ writeFileSync(dst, JSON.stringify(data, null, 2) + '\n', 'utf8');
117
+ rmSync(src);
118
+ return { id, filePath: dst };
119
+ }
120
+
121
+ markSkipped(id, { skippedBy = 'unknown', reason = '' } = {}) {
122
+ const src = path.join(pendingDir(this.rootDir), `${id}.json`);
123
+ if (!existsSync(src)) throw new Error(`markSkipped: no pending entry ${id}`);
124
+ const data = JSON.parse(readFileSync(src, 'utf8'));
125
+ data.status = 'skipped';
126
+ data.skippedAt = new Date().toISOString();
127
+ data.skippedBy = skippedBy;
128
+ if (reason) data.reason = reason;
129
+
130
+ const dst = path.join(skippedDir(this.rootDir), `${id}.json`);
131
+ mkdirSync(path.dirname(dst), { recursive: true });
132
+ writeFileSync(dst, JSON.stringify(data, null, 2) + '\n', 'utf8');
133
+ rmSync(src);
134
+ return { id, filePath: dst };
135
+ }
136
+
137
+ reopen(id) {
138
+ for (const dir of [processedDir(this.rootDir), skippedDir(this.rootDir)]) {
139
+ const src = path.join(dir, `${id}.json`);
140
+ if (!existsSync(src)) continue;
141
+ const data = JSON.parse(readFileSync(src, 'utf8'));
142
+ data.status = 'pending';
143
+ delete data.processedAt;
144
+ delete data.processedBy;
145
+ delete data.notes;
146
+ delete data.skippedAt;
147
+ delete data.skippedBy;
148
+ delete data.reason;
149
+
150
+ const dst = path.join(pendingDir(this.rootDir), `${id}.json`);
151
+ mkdirSync(path.dirname(dst), { recursive: true });
152
+ writeFileSync(dst, JSON.stringify(data, null, 2) + '\n', 'utf8');
153
+ rmSync(src);
154
+ return { id, filePath: dst, from: path.basename(dir) };
155
+ }
156
+ throw new Error(`reopen: no processed or skipped entry ${id}`);
157
+ }
158
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * lib/intake/intake-config.mjs — user-facing intake watcher config.
3
+ *
4
+ * Lets the user define which directories the inbox watcher scans and how
5
+ * deep it descends through subdirectories. Persists to
6
+ * `<rootDir>/.cx/intake-config.json` and merges with env-driven defaults so
7
+ * CLI, dashboard, and process env all converge on the same answer.
8
+ *
9
+ * Schema:
10
+ * {
11
+ * parentDirs: string[] // absolute or rootDir-relative
12
+ * maxDepth: number // 0 = only the parent dir, no subdirs
13
+ * includeProjectInbox: boolean // always include <rootDir>/.cx/inbox
14
+ * includeDocsIntake: boolean // include <rootDir>/docs/intake when present
15
+ * }
16
+ *
17
+ * maxDepth guidance (surfaced verbatim in the dashboard):
18
+ * 0 — only files directly inside the parent dir
19
+ * 1 — parent dir + its immediate subdirs
20
+ * 2 — two levels of subdirs (a common default for project intake roots)
21
+ * 3+ — deep scans; use with care on large trees
22
+ */
23
+
24
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
25
+ import { isAbsolute, join, resolve } from 'node:path';
26
+ import { shouldCreateCx } from '../project-detection.mjs';
27
+
28
+ export const INTAKE_CONFIG_FILE = '.cx/intake-config.json';
29
+ export const INTAKE_DEFAULT_MAX_DEPTH = 4;
30
+ export const INTAKE_HARD_MAX_DEPTH = 16;
31
+
32
+ export const INTAKE_DEPTH_GUIDANCE = [
33
+ { value: 0, label: 'Only this directory', detail: 'Scans files directly in the parent dir, ignores all subdirs.' },
34
+ { value: 1, label: 'One level deep', detail: 'Parent dir plus its immediate subdirs (e.g. parent/intake/file.md).' },
35
+ { value: 2, label: 'Two levels deep', detail: 'Parent and two subdirs. A reasonable default for organized intake roots.' },
36
+ { value: 4, label: 'Four levels (default)', detail: 'Catches most nested layouts without scanning huge trees.' },
37
+ { value: 8, label: 'Deep scan', detail: 'Useful for archives. Slower; skip if the parent contains build output.' },
38
+ { value: INTAKE_HARD_MAX_DEPTH, label: 'Unlimited (capped)', detail: `Walks up to ${INTAKE_HARD_MAX_DEPTH} levels — effectively unlimited. May be slow.` },
39
+ ];
40
+
41
+ export const DEFAULT_INTAKE_CONFIG = Object.freeze({
42
+ parentDirs: [],
43
+ maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
44
+ includeProjectInbox: true,
45
+ includeDocsIntake: true,
46
+ });
47
+
48
+ export function intakeConfigPath(rootDir) {
49
+ return join(rootDir, INTAKE_CONFIG_FILE);
50
+ }
51
+
52
+ function clampDepth(value) {
53
+ const n = Number(value);
54
+ if (!Number.isFinite(n) || n < 0) return INTAKE_DEFAULT_MAX_DEPTH;
55
+ return Math.min(Math.floor(n), INTAKE_HARD_MAX_DEPTH);
56
+ }
57
+
58
+ function normalizeDir(dir, rootDir) {
59
+ if (typeof dir !== 'string') return null;
60
+ const trimmed = dir.trim();
61
+ if (!trimmed) return null;
62
+ return isAbsolute(trimmed) ? trimmed : resolve(rootDir, trimmed);
63
+ }
64
+
65
+ function parseEnvDirs(env) {
66
+ const raw = String(env?.CX_INBOX_DIRS ?? '').trim();
67
+ if (!raw) return [];
68
+ return raw.split(':').map((p) => p.trim()).filter(Boolean);
69
+ }
70
+
71
+ export function loadIntakeConfig(rootDir, env = process.env) {
72
+ const file = intakeConfigPath(rootDir);
73
+ let stored = {};
74
+ if (existsSync(file)) {
75
+ try { stored = JSON.parse(readFileSync(file, 'utf8')); } catch { stored = {}; }
76
+ }
77
+
78
+ const parentDirs = []
79
+ .concat(Array.isArray(stored.parentDirs) ? stored.parentDirs : [])
80
+ .concat(parseEnvDirs(env))
81
+ .map((dir) => normalizeDir(dir, rootDir))
82
+ .filter(Boolean);
83
+
84
+ const seen = new Set();
85
+ const uniqueDirs = [];
86
+ for (const dir of parentDirs) {
87
+ if (seen.has(dir)) continue;
88
+ seen.add(dir);
89
+ uniqueDirs.push(dir);
90
+ }
91
+
92
+ const envDepth = env?.CX_INTAKE_MAX_DEPTH;
93
+ const maxDepth = clampDepth(envDepth ?? stored.maxDepth ?? DEFAULT_INTAKE_CONFIG.maxDepth);
94
+
95
+ return {
96
+ parentDirs: uniqueDirs,
97
+ maxDepth,
98
+ includeProjectInbox: stored.includeProjectInbox !== false,
99
+ includeDocsIntake: stored.includeDocsIntake !== false,
100
+ };
101
+ }
102
+
103
+ export function saveIntakeConfig(rootDir, patch = {}) {
104
+ if (!shouldCreateCx(rootDir)) {
105
+ throw new Error('Refusing to write intake config: directory is not an initialized construct project. Run `construct init` first.');
106
+ }
107
+ const current = loadIntakeConfig(rootDir, {});
108
+ const next = {
109
+ parentDirs: Array.isArray(patch.parentDirs)
110
+ ? patch.parentDirs.map((dir) => normalizeDir(dir, rootDir)).filter(Boolean)
111
+ : current.parentDirs,
112
+ maxDepth: clampDepth(patch.maxDepth ?? current.maxDepth),
113
+ includeProjectInbox: patch.includeProjectInbox !== undefined ? Boolean(patch.includeProjectInbox) : current.includeProjectInbox,
114
+ includeDocsIntake: patch.includeDocsIntake !== undefined ? Boolean(patch.includeDocsIntake) : current.includeDocsIntake,
115
+ };
116
+
117
+ const file = intakeConfigPath(rootDir);
118
+ mkdirSync(join(rootDir, '.cx'), { recursive: true });
119
+ writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
120
+ return next;
121
+ }
122
+
123
+ export function describeIntakeDepth(depth = INTAKE_DEFAULT_MAX_DEPTH) {
124
+ const value = clampDepth(depth);
125
+ const exact = INTAKE_DEPTH_GUIDANCE.find((g) => g.value === value);
126
+ if (exact) return exact;
127
+ return {
128
+ value,
129
+ label: `Custom depth (${value})`,
130
+ detail: `Walks up to ${value} levels of subdirectories below each parent.`,
131
+ };
132
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * lib/intake/postgres-queue.mjs — Postgres adapter for the IntakeQueue interface.
3
+ *
4
+ * Backs team and enterprise deployment modes. Implements the same six-method
5
+ * contract as FilesystemIntakeQueue (enqueue, listPending, count, read,
6
+ * markProcessed, markSkipped, reopen) plus a claim() method that uses
7
+ * SELECT ... FOR UPDATE SKIP LOCKED so concurrent workers cannot grab the
8
+ * same pending item.
9
+ *
10
+ * Triage fields (intake_type, rd_stage, primary_owner, recommended_action,
11
+ * risk, requires_approval, confidence) are flattened into typed columns for
12
+ * efficient filtering; the full packet — including suggestion / related /
13
+ * excerpt / query — lives in payload jsonb so the on-disk and in-table
14
+ * shapes round-trip without loss.
15
+ */
16
+
17
+ import path from 'node:path';
18
+
19
+ function slugify(value) {
20
+ return String(value || 'untitled')
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]+/g, '-')
23
+ .replace(/^-+|-+$/g, '')
24
+ .slice(0, 60) || 'untitled';
25
+ }
26
+
27
+ let counter = 0;
28
+ function timestamp() {
29
+ // Milliseconds + a per-process counter to guarantee uniqueness even when
30
+ // two enqueues fire in the same millisecond (test loops, batched ingests).
31
+ counter = (counter + 1) % 1000;
32
+ const c = String(counter).padStart(3, '0');
33
+ return `${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 23)}-${c}`;
34
+ }
35
+
36
+ function rowToEntry(row) {
37
+ if (!row) return null;
38
+ const payload = row.payload || {};
39
+ return {
40
+ id: row.id,
41
+ createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
42
+ status: row.status,
43
+ ...payload,
44
+ processedAt: row.processed_at instanceof Date ? row.processed_at.toISOString() : row.processed_at || undefined,
45
+ processedBy: row.processed_by || undefined,
46
+ notes: row.notes || undefined,
47
+ skippedAt: row.skipped_at instanceof Date ? row.skipped_at.toISOString() : row.skipped_at || undefined,
48
+ skippedBy: row.skipped_by || undefined,
49
+ reason: row.skip_reason || undefined,
50
+ claimedAt: row.claimed_at instanceof Date ? row.claimed_at.toISOString() : row.claimed_at || undefined,
51
+ claimedBy: row.claimed_by || undefined,
52
+ };
53
+ }
54
+
55
+ export class PostgresIntakeQueue {
56
+ constructor({ sql, project, tenantId = null } = {}) {
57
+ if (!sql) throw new Error('PostgresIntakeQueue: sql client is required');
58
+ if (!project) throw new Error('PostgresIntakeQueue: project is required');
59
+ this.sql = sql;
60
+ this.project = project;
61
+ this.tenantId = tenantId;
62
+ }
63
+
64
+ async enqueue(entry) {
65
+ if (!entry?.intake?.sourcePath) throw new Error('enqueue: entry.intake.sourcePath is required');
66
+
67
+ const ts = timestamp();
68
+ const slug = slugify(path.basename(entry.intake.sourcePath, path.extname(entry.intake.sourcePath)));
69
+ const id = `${ts}-${slug}`;
70
+ const triage = entry.triage || {};
71
+
72
+ await this.sql`
73
+ INSERT INTO construct_intake_items (
74
+ id, project, tenant_id, status,
75
+ intake_type, rd_stage, primary_owner, recommended_action,
76
+ risk, requires_approval, confidence, payload
77
+ ) VALUES (
78
+ ${id}, ${this.project}, ${this.tenantId}, 'pending',
79
+ ${triage.intakeType || null}, ${triage.rdStage || null},
80
+ ${triage.primaryOwner || null}, ${triage.recommendedAction || null},
81
+ ${triage.risk || null}, ${triage.requiresApproval || false},
82
+ ${typeof triage.confidence === 'number' ? triage.confidence : null},
83
+ ${this.sql.json(entry)}
84
+ )
85
+ `;
86
+ return { id };
87
+ }
88
+
89
+ async listPending({ limit = 100 } = {}) {
90
+ const rows = await this.sql`
91
+ SELECT * FROM construct_intake_items
92
+ WHERE project = ${this.project}
93
+ AND (${this.tenantId}::text IS NULL OR tenant_id IS NOT DISTINCT FROM ${this.tenantId})
94
+ AND status = 'pending'
95
+ ORDER BY created_at ASC
96
+ LIMIT ${limit}
97
+ `;
98
+ return rows.map(rowToEntry);
99
+ }
100
+
101
+ async count() {
102
+ const rows = await this.sql`
103
+ SELECT COUNT(*)::int AS n FROM construct_intake_items
104
+ WHERE project = ${this.project}
105
+ AND (${this.tenantId}::text IS NULL OR tenant_id IS NOT DISTINCT FROM ${this.tenantId})
106
+ AND status = 'pending'
107
+ `;
108
+ return rows[0]?.n ?? 0;
109
+ }
110
+
111
+ async read(id) {
112
+ const rows = await this.sql`
113
+ SELECT * FROM construct_intake_items
114
+ WHERE id = ${id}
115
+ AND project = ${this.project}
116
+ AND (${this.tenantId}::text IS NULL OR tenant_id IS NOT DISTINCT FROM ${this.tenantId})
117
+ LIMIT 1
118
+ `;
119
+ return rows[0] ? rowToEntry(rows[0]) : null;
120
+ }
121
+
122
+ async markProcessed(id, { processedBy = 'unknown', notes = '' } = {}) {
123
+ const result = await this.sql`
124
+ UPDATE construct_intake_items
125
+ SET status = 'processed', processed_at = now(), processed_by = ${processedBy},
126
+ notes = ${notes || null}, updated_at = now()
127
+ WHERE id = ${id}
128
+ AND project = ${this.project}
129
+ AND status IN ('pending', 'claimed')
130
+ RETURNING id
131
+ `;
132
+ if (result.count === 0) throw new Error(`markProcessed: no pending entry ${id}`);
133
+ return { id };
134
+ }
135
+
136
+ async markSkipped(id, { skippedBy = 'unknown', reason = '' } = {}) {
137
+ const result = await this.sql`
138
+ UPDATE construct_intake_items
139
+ SET status = 'skipped', skipped_at = now(), skipped_by = ${skippedBy},
140
+ skip_reason = ${reason || null}, updated_at = now()
141
+ WHERE id = ${id}
142
+ AND project = ${this.project}
143
+ AND status IN ('pending', 'claimed')
144
+ RETURNING id
145
+ `;
146
+ if (result.count === 0) throw new Error(`markSkipped: no pending entry ${id}`);
147
+ return { id };
148
+ }
149
+
150
+ async reopen(id) {
151
+ const result = await this.sql`
152
+ UPDATE construct_intake_items
153
+ SET status = 'pending',
154
+ processed_at = NULL, processed_by = NULL, notes = NULL,
155
+ skipped_at = NULL, skipped_by = NULL, skip_reason = NULL,
156
+ claimed_at = NULL, claimed_by = NULL,
157
+ updated_at = now()
158
+ WHERE id = ${id}
159
+ AND project = ${this.project}
160
+ AND status IN ('processed', 'skipped')
161
+ RETURNING id, (CASE WHEN processed_at IS NOT NULL THEN 'processed' ELSE 'skipped' END) AS from_status
162
+ `;
163
+ if (result.count === 0) throw new Error(`reopen: no processed or skipped entry ${id}`);
164
+ return { id, from: result[0]?.from_status || 'unknown' };
165
+ }
166
+
167
+ /**
168
+ * Atomically claim the next pending intake item for a worker.
169
+ * Uses SELECT ... FOR UPDATE SKIP LOCKED so concurrent workers across
170
+ * processes / containers cannot grab the same item. Returns the claimed
171
+ * entry (with status='claimed'), or null when the queue is empty.
172
+ */
173
+ async claim({ claimedBy }) {
174
+ if (!claimedBy) throw new Error('claim: claimedBy is required');
175
+ return await this.sql.begin(async (tx) => {
176
+ const rows = await tx`
177
+ SELECT * FROM construct_intake_items
178
+ WHERE project = ${this.project}
179
+ AND (${this.tenantId}::text IS NULL OR tenant_id IS NOT DISTINCT FROM ${this.tenantId})
180
+ AND status = 'pending'
181
+ ORDER BY created_at ASC
182
+ FOR UPDATE SKIP LOCKED
183
+ LIMIT 1
184
+ `;
185
+ const row = rows[0];
186
+ if (!row) return null;
187
+ await tx`
188
+ UPDATE construct_intake_items
189
+ SET status = 'claimed', claimed_at = now(), claimed_by = ${claimedBy}, updated_at = now()
190
+ WHERE id = ${row.id}
191
+ `;
192
+ row.status = 'claimed';
193
+ row.claimed_by = claimedBy;
194
+ row.claimed_at = new Date();
195
+ return rowToEntry(row);
196
+ });
197
+ }
198
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * lib/intake/prepare.mjs — turn a fresh inbox ingestion into an R&D intake packet.
3
+ *
4
+ * Called by `InboxWatcher.poll()` after each successful file ingestion.
5
+ * Four deterministic preparation steps, then write the result to the
6
+ * intake queue:
7
+ *
8
+ * 1. Suggest a docs lane via lib/docs-routing.mjs (postmortem? PRD?).
9
+ * 2. Query the corpus via lib/storage/hybrid-query.mjs for top-K existing
10
+ * docs related to the new content. Uses the file's filename + first
11
+ * paragraph as the query (no LLM call needed for retrieval).
12
+ * 3. Extract a short excerpt of the new content for the agent to see
13
+ * without reopening the file.
14
+ * 4. Run classifyRdIntake to produce the R&D triage object (intake type,
15
+ * owner persona, recommended chain, action, risk).
16
+ *
17
+ * The agent — invoked manually or via session-start hook nudge — reads
18
+ * the resulting `.cx/intake/pending/<id>.json` and does the real
19
+ * comparison work (overlap with existing PRD? contradicts ADR? new RFC
20
+ * candidate?). The daemon never calls an LLM. This separation keeps the
21
+ * daemon cheap and predictable; the model spend stays with the agent.
22
+ */
23
+
24
+ import { existsSync, readFileSync } from 'node:fs';
25
+ import path from 'node:path';
26
+
27
+ import { buildHybridSearchResultsAsync } from '../storage/hybrid-query.mjs';
28
+ import { suggestDocsLaneForFile } from '../docs-routing.mjs';
29
+ import { createIntakeQueue } from './queue.mjs';
30
+ import { classifyRdIntake } from './classify.mjs';
31
+ import { detectCustomerMentions, linkSignalToCustomer, updateCustomerProfile } from '../embed/customer-profiles.mjs';
32
+
33
+ const DEFAULT_RELATED_LIMIT = 5;
34
+ const EXCERPT_CHARS = 800;
35
+ const QUERY_CHARS = 500;
36
+
37
+ export async function prepareIntakeForIngestedFile({
38
+ rootDir,
39
+ ingestedFile,
40
+ env = process.env,
41
+ relatedLimit = DEFAULT_RELATED_LIMIT,
42
+ hybridSearchFn = buildHybridSearchResultsAsync,
43
+ queue,
44
+ readFileFn = readContent,
45
+ classifyFn = classifyRdIntake,
46
+ } = {}) {
47
+ if (!rootDir) throw new Error('prepareIntakeForIngestedFile: rootDir is required');
48
+ if (!ingestedFile?.sourcePath) throw new Error('prepareIntakeForIngestedFile: ingestedFile.sourcePath is required');
49
+
50
+ const extracted = readFileFn(ingestedFile.outputPath) || '';
51
+ const query = buildQuery(ingestedFile.sourcePath, extracted);
52
+
53
+ let related = [];
54
+ try {
55
+ const search = await hybridSearchFn(rootDir, query, { limit: relatedLimit, env });
56
+ related = (search?.results || []).map((hit) => ({
57
+ path: hit.source_path || hit.id,
58
+ title: hit.title || hit.id,
59
+ score: hit.score,
60
+ summary: hit.summary || '',
61
+ }));
62
+ } catch (err) {
63
+ related = [];
64
+ if (process.env.CONSTRUCT_DEBUG_INTAKE === '1') {
65
+ process.stderr.write(`[intake:prepare] hybrid search failed: ${err.message}\n`);
66
+ }
67
+ }
68
+
69
+ const lane = suggestDocsLaneForFile(ingestedFile.sourcePath, extracted) || null;
70
+ const triage = classifyFn({
71
+ sourcePath: ingestedFile.sourcePath,
72
+ extractedText: extracted,
73
+ related,
74
+ });
75
+
76
+ // Detect customer mentions and update profiles
77
+ const customerMentions = detectCustomerMentions(extracted.slice(0, 2000));
78
+ const customers = customerMentions.map((c) => c.customerId);
79
+
80
+ const entry = {
81
+ intake: {
82
+ sourcePath: ingestedFile.sourcePath,
83
+ outputPath: ingestedFile.outputPath,
84
+ characters: ingestedFile.characters,
85
+ knowledgeSubdir: ingestedFile.knowledgeSubdir,
86
+ },
87
+ triage,
88
+ suggestion: lane ? { lane, source: 'docs-routing.suggestDocsLaneForFile' } : null,
89
+ related,
90
+ excerpt: extracted.slice(0, EXCERPT_CHARS),
91
+ query,
92
+ customers: customers.length ? customers : undefined,
93
+ };
94
+
95
+ const intakeQueue = queue || createIntakeQueue(rootDir, env);
96
+ const result = intakeQueue.enqueue(entry);
97
+ const intakeId = result?.id || '';
98
+
99
+ // Update customer profiles with new evidence
100
+ for (const mention of customerMentions) {
101
+ try {
102
+ const intakeType = triage?.intakeType || 'insight';
103
+ const excerptPreview = (extracted || '').slice(0, 200).replace(/\n/g, ' ').trim();
104
+ linkSignalToCustomer(mention.customerId, {
105
+ id: intakeId,
106
+ type: 'intake',
107
+ sourcePath: ingestedFile.sourcePath,
108
+ summary: `[${intakeType}] ${ingestedFile.sourcePath.split('/').pop()}${excerptPreview ? ` — ${excerptPreview.slice(0, 100)}` : ''}`,
109
+ });
110
+ } catch (err) {
111
+ if (process.env.CONSTRUCT_DEBUG_INTAKE === '1') {
112
+ process.stderr.write(`[intake:prepare] customer profile update failed for ${mention.customerId}: ${err.message}\n`);
113
+ }
114
+ }
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ function buildQuery(sourcePath, extracted) {
121
+ const filename = path.basename(sourcePath, path.extname(sourcePath))
122
+ .replace(/[-_]+/g, ' ')
123
+ .trim();
124
+ const firstParagraph = (extracted || '').split(/\n\s*\n/).find((p) => p.trim().length > 20) || '';
125
+ const combined = `${filename}\n\n${firstParagraph}`.slice(0, QUERY_CHARS).trim();
126
+ return combined || filename || 'untitled intake';
127
+ }
128
+
129
+ function readContent(outputPath) {
130
+ if (!outputPath || !existsSync(outputPath)) return '';
131
+ try {
132
+ const text = readFileSync(outputPath, 'utf8');
133
+ const marker = text.indexOf('## Extracted Content');
134
+ if (marker !== -1) return text.slice(marker + '## Extracted Content'.length).trim();
135
+ return text;
136
+ } catch {
137
+ return '';
138
+ }
139
+ }