@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,343 @@
1
+ /**
2
+ * lib/embed/inbox.mjs — filesystem inbox watcher for the embed daemon.
3
+ *
4
+ * Watches one or more local directories for new ingestable files and
5
+ * processes them into the observation store. Agnostic to content type —
6
+ * handles specs, ADRs, meeting notes, internal docs, research, PDFs,
7
+ * Office files, plain text, code, or any format supported by document-extract.
8
+ *
9
+ * Watch dirs (in priority order):
10
+ * 1. CX_INBOX_DIRS env var — colon-separated absolute paths
11
+ * 2. <rootDir>/.cx/inbox/ — per-project drop folder (always included)
12
+ * 3. <rootDir>/docs/intake/ — project intake drop zone when docs init is used
13
+ *
14
+ * State tracking:
15
+ * <rootDir>/.cx/runtime/inbox-state.json — maps filePath → processedAt
16
+ * Prevents re-processing the same file across daemon restarts.
17
+ *
18
+ * Usage:
19
+ * const watcher = new InboxWatcher({ rootDir, env });
20
+ * const result = await watcher.poll();
21
+ * // result: { processed: [...], skipped: number, errors: [...] }
22
+ */
23
+
24
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
25
+ import { join, resolve, isAbsolute } from 'node:path';
26
+ import { isExtractableDocumentPath } from '../document-extract.mjs';
27
+ import { ingestDocuments } from '../document-ingest.mjs';
28
+ import { addObservation } from '../observation-store.mjs';
29
+ import { inferKnowledgeTarget, knowledgeDirForCategory } from '../knowledge/layout.mjs';
30
+ import { docLaneDir, suggestDocsLaneForFile } from '../docs-routing.mjs';
31
+ import { shouldCreateCx } from '../project-detection.mjs';
32
+ import { prepareIntakeForIngestedFile } from '../intake/prepare.mjs';
33
+ import { loadIntakeConfig, INTAKE_DEFAULT_MAX_DEPTH } from '../intake/intake-config.mjs';
34
+
35
+ const STATE_FILE = '.cx/runtime/inbox-state.json';
36
+ const DEFAULT_INBOX_SUBDIR = '.cx/inbox';
37
+ const DOCS_INTAKE_SUBDIR = 'docs/intake';
38
+ const MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB hard cap
39
+
40
+ function nextAvailablePath(targetPath) {
41
+ if (!existsSync(targetPath)) return targetPath;
42
+ const dot = targetPath.lastIndexOf('.');
43
+ const base = dot === -1 ? targetPath : targetPath.slice(0, dot);
44
+ const ext = dot === -1 ? '' : targetPath.slice(dot);
45
+ let index = 2;
46
+ while (true) {
47
+ const candidate = `${base}-${index}${ext}`;
48
+ if (!existsSync(candidate)) return candidate;
49
+ index += 1;
50
+ }
51
+ }
52
+
53
+ // ─── State helpers ────────────────────────────────────────────────────────────
54
+
55
+ function statePath(rootDir) {
56
+ return join(rootDir, STATE_FILE);
57
+ }
58
+
59
+ function readState(rootDir) {
60
+ const p = statePath(rootDir);
61
+ if (!existsSync(p)) return {};
62
+ try { return JSON.parse(readFileSync(p, 'utf8')); } catch (err) { process.stderr.write('[inbox.mjs] readState: ' + (err?.message ?? String(err)) + '\n'); return {}; }
63
+ }
64
+
65
+ function writeState(rootDir, state) {
66
+ const p = statePath(rootDir);
67
+
68
+ // Only create .cx directories if this is an initialized project
69
+ if (shouldCreateCx(rootDir)) {
70
+ mkdirSync(join(rootDir, '.cx', 'runtime'), { recursive: true });
71
+ writeFileSync(p, JSON.stringify(state, null, 2) + '\n');
72
+ }
73
+ // If not an initialized project, don't create .cx - state will be lost
74
+ // but that's okay since we shouldn't be processing files in uninitialized dirs
75
+ }
76
+
77
+ // ─── Directory resolution ─────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Resolve inbox watch directories from the persisted intake-config.
81
+ * Always includes <rootDir>/.cx/inbox/ (created on first use) when the
82
+ * config has includeProjectInbox: true (default).
83
+ * Includes <rootDir>/docs/intake/ when it exists and includeDocsIntake is true.
84
+ * `parentDirs` from intake-config.json plus CX_INBOX_DIRS env var add extras.
85
+ */
86
+ export function resolveInboxDirs(rootDir, env = process.env) {
87
+ const config = loadIntakeConfig(rootDir, env);
88
+ const dirs = [];
89
+
90
+ if (config.includeProjectInbox) {
91
+ const projectInbox = join(rootDir, DEFAULT_INBOX_SUBDIR);
92
+ mkdirSync(projectInbox, { recursive: true });
93
+ dirs.push(projectInbox);
94
+ }
95
+
96
+ if (config.includeDocsIntake) {
97
+ const docsIntake = join(rootDir, DOCS_INTAKE_SUBDIR);
98
+ if (existsSync(docsIntake)) dirs.push(docsIntake);
99
+ }
100
+
101
+ for (const candidate of config.parentDirs) {
102
+ if (!candidate) continue;
103
+ if (!existsSync(candidate)) continue;
104
+ if (dirs.includes(candidate)) continue;
105
+ dirs.push(candidate);
106
+ }
107
+
108
+ return dirs;
109
+ }
110
+
111
+ // ─── File scanning ────────────────────────────────────────────────────────────
112
+
113
+ /**
114
+ * Walk `dir` up to `maxDepth` subdirectory levels.
115
+ * maxDepth=0 → only files in `dir` itself (no subdirs).
116
+ * Directories are tracked with their depth so the walk stops at the limit.
117
+ */
118
+ function scanDir(dir, maxDepth = INTAKE_DEFAULT_MAX_DEPTH) {
119
+ const results = [];
120
+ const stack = [{ path: dir, depth: 0 }];
121
+ try {
122
+ while (stack.length > 0) {
123
+ const { path: current, depth } = stack.pop();
124
+ const entries = readdirSync(current, { withFileTypes: true });
125
+ for (const entry of entries) {
126
+ if (entry.name.startsWith('.')) continue;
127
+ const full = join(current, entry.name);
128
+ if (entry.isDirectory()) {
129
+ if (depth < maxDepth) stack.push({ path: full, depth: depth + 1 });
130
+ continue;
131
+ }
132
+ if (!entry.isFile()) continue;
133
+ if (!isExtractableDocumentPath(full)) continue;
134
+ try {
135
+ const st = statSync(full);
136
+ if (st.size === 0 || st.size > MAX_FILE_SIZE_BYTES) continue;
137
+ results.push({ path: full, size: st.size, mtimeMs: st.mtimeMs });
138
+ } catch (err) { process.stderr.write('[inbox.mjs] scan-file-stat: ' + (err?.message ?? String(err)) + '\n'); }
139
+ }
140
+ }
141
+ } catch (err) { process.stderr.write('[inbox.mjs] scan-dir: ' + (err?.message ?? String(err)) + '\n'); }
142
+ return results;
143
+ }
144
+
145
+ // ─── Observation extraction ───────────────────────────────────────────────────
146
+
147
+ /**
148
+ * Derive observation category and knowledge subdir from a file path.
149
+ * Uses inferKnowledgeTarget for routing, then maps subdir → obs category.
150
+ */
151
+ function inferFileClassification(filePath) {
152
+ const knowledgeSubdir = inferKnowledgeTarget(filePath);
153
+ // Map subdir to observation category
154
+ let category;
155
+ switch (knowledgeSubdir) {
156
+ case 'decisions': category = 'decision'; break;
157
+ case 'how-tos': category = 'pattern'; break;
158
+ case 'reference': category = 'insight'; break;
159
+ case 'external': category = 'insight'; break;
160
+ default: category = 'insight';
161
+ }
162
+ // Postmortem / incident files deserve anti-pattern category regardless of subdir
163
+ const name = filePath.toLowerCase();
164
+ if (/\bpost.?mortem\b|\bincident\b|\brca\b/.test(name)) category = 'anti-pattern';
165
+ return { category, knowledgeSubdir };
166
+ }
167
+
168
+ /**
169
+ * Write an observation summarising the ingested document.
170
+ * Summary = first non-empty line of extracted text (≤120 chars).
171
+ * Content = first 1500 chars of extracted text for RAG searchability.
172
+ */
173
+ function recordInboxObservation(rootDir, { sourcePath, outputPath, characters, extractedText, category, knowledgeSubdir }) {
174
+ const lines = (extractedText ?? '').split('\n').map((l) => l.trim()).filter(Boolean);
175
+ const firstLine = (lines[0] ?? '').slice(0, 120);
176
+ const filename = sourcePath.split('/').pop();
177
+ const summary = firstLine
178
+ ? `[inbox] ${filename}: ${firstLine}`
179
+ : `[inbox] Ingested document: ${filename}`;
180
+ const content = (extractedText ?? '').slice(0, 1500);
181
+ const tags = ['inbox', 'ingested-doc', category, `knowledge:${knowledgeSubdir}`];
182
+
183
+ addObservation(rootDir, {
184
+ role: 'construct',
185
+ category,
186
+ summary,
187
+ content: `source: ${sourcePath}\noutput: ${outputPath}\nknowledge-subdir: ${knowledgeSubdir}\ncharacters: ${characters}\n\n${content}`,
188
+ tags,
189
+ confidence: 0.75,
190
+ source: 'inbox-watcher',
191
+ });
192
+ }
193
+
194
+ function maybePromoteToDocs(rootDir, { sourcePath, extractedText }) {
195
+ const suggestedLane = suggestDocsLaneForFile(sourcePath, extractedText);
196
+ if (!suggestedLane || suggestedLane === 'intake') return null;
197
+
198
+ const laneDir = join(rootDir, 'docs', docLaneDir(suggestedLane));
199
+ if (!existsSync(laneDir)) return null;
200
+
201
+ const sourceName = sourcePath.split('/').pop() ?? 'intake-doc';
202
+ const targetPath = nextAvailablePath(join(laneDir, `${sourceName}.md`));
203
+ const title = sourceName.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').trim() || 'Promoted intake document';
204
+ const markdown = [
205
+ '# ' + title,
206
+ '',
207
+ '> Promoted from intake for review and incorporation into this docs lane.',
208
+ '',
209
+ `- Source: \`${sourcePath}\``,
210
+ `- Suggested lane: \`${suggestedLane}\``,
211
+ '',
212
+ '## Extracted Content',
213
+ '',
214
+ extractedText || '',
215
+ '',
216
+ ].join('\n');
217
+
218
+ writeFileSync(targetPath, markdown);
219
+ return targetPath;
220
+ }
221
+
222
+ // ─── InboxWatcher class ───────────────────────────────────────────────────────
223
+
224
+ export class InboxWatcher {
225
+ #rootDir;
226
+ #env;
227
+ #cwd;
228
+
229
+ /**
230
+ * @param {object} opts
231
+ * @param {string} opts.rootDir - Root dir for observation store + state file
232
+ * @param {object} [opts.env] - Env override (default: process.env)
233
+ * @param {string} [opts.cwd] - Working dir for ingest output (default: rootDir)
234
+ */
235
+ constructor({ rootDir, env = process.env, cwd } = {}) {
236
+ this.#rootDir = rootDir;
237
+ this.#env = env;
238
+ this.#cwd = cwd ?? rootDir;
239
+ }
240
+
241
+ /**
242
+ * Scan all inbox dirs, ingest new files, record observations.
243
+ * Returns { processed: [...], skipped: number, errors: [...] }
244
+ */
245
+ async poll() {
246
+ const config = loadIntakeConfig(this.#rootDir, this.#env);
247
+ const dirs = resolveInboxDirs(this.#rootDir, this.#env);
248
+ const state = readState(this.#rootDir);
249
+ const processed = [];
250
+ const errors = [];
251
+ let skipped = 0;
252
+
253
+ for (const dir of dirs) {
254
+ const candidates = scanDir(dir, config.maxDepth);
255
+ for (const candidate of candidates) {
256
+ const key = candidate.path;
257
+
258
+ // Skip if already processed (state key = path + mtime for change detection)
259
+ const stateKey = `${key}:${candidate.mtimeMs}`;
260
+ if (state[key] && state[key].mtimeMs === candidate.mtimeMs) {
261
+ skipped += 1;
262
+ continue;
263
+ }
264
+
265
+ try {
266
+ const { category, knowledgeSubdir } = inferFileClassification(candidate.path);
267
+ const result = await ingestDocuments([candidate.path], {
268
+ cwd: this.#cwd,
269
+ target: `knowledge/${knowledgeSubdir}`,
270
+ sync: false,
271
+ env: this.#env,
272
+ });
273
+
274
+ const fileResult = result.files?.[0];
275
+ if (fileResult) {
276
+ // Best-effort: read back the extracted text from the written markdown
277
+ let extractedText = '';
278
+ try {
279
+ const md = readFileSync(fileResult.outputPath, 'utf8');
280
+ const contentIdx = md.indexOf('## Extracted Content');
281
+ if (contentIdx !== -1) extractedText = md.slice(contentIdx + 20).trim();
282
+ } catch (err) { process.stderr.write('[inbox.mjs] read-extracted-text: ' + (err?.message ?? String(err)) + '\n'); }
283
+
284
+ recordInboxObservation(this.#rootDir, {
285
+ sourcePath: candidate.path,
286
+ outputPath: fileResult.outputPath,
287
+ characters: fileResult.characters,
288
+ extractedText,
289
+ category,
290
+ knowledgeSubdir,
291
+ });
292
+
293
+ const docsPath = maybePromoteToDocs(this.#rootDir, {
294
+ sourcePath: candidate.path,
295
+ extractedText,
296
+ });
297
+
298
+ // Queue an intake packet so the agent can triage the new
299
+ // signal against the existing corpus. Failure here is
300
+ // non-fatal — ingest already succeeded; the queue is a
301
+ // best-effort handoff, not the ingestion's source of truth.
302
+ let intakeId = null;
303
+ try {
304
+ const intakeResult = await prepareIntakeForIngestedFile({
305
+ rootDir: this.#rootDir,
306
+ ingestedFile: {
307
+ sourcePath: candidate.path,
308
+ outputPath: fileResult.outputPath,
309
+ characters: fileResult.characters,
310
+ knowledgeSubdir,
311
+ },
312
+ env: this.#env,
313
+ });
314
+ intakeId = intakeResult?.id || null;
315
+ } catch (err) {
316
+ if (this.#env.CONSTRUCT_DEBUG_INTAKE === '1') {
317
+ process.stderr.write(`[inbox] intake queue write failed for ${candidate.path}: ${err.message}\n`);
318
+ }
319
+ }
320
+
321
+ state[key] = { mtimeMs: candidate.mtimeMs, processedAt: new Date().toISOString(), outputPath: fileResult.outputPath };
322
+ processed.push({ path: candidate.path, outputPath: fileResult.outputPath, docsPath, characters: fileResult.characters, knowledgeSubdir, intakeId });
323
+ }
324
+ } catch (err) {
325
+ errors.push({ path: candidate.path, error: err.message });
326
+ }
327
+ }
328
+ }
329
+
330
+ if (processed.length || errors.length) {
331
+ writeState(this.#rootDir, state);
332
+ }
333
+
334
+ return { processed, skipped, errors, dirs };
335
+ }
336
+
337
+ /**
338
+ * Return the configured inbox directories (creates project inbox if missing).
339
+ */
340
+ dirs() {
341
+ return resolveInboxDirs(this.#rootDir, this.#env);
342
+ }
343
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * lib/embed/intake-metrics.mjs — Intake pipeline observability.
3
+ *
4
+ * Aggregates volume, velocity, processing time, and acceptance rate from
5
+ * the filesystem intake queue. Designed for the dashboard /api/insights
6
+ * endpoint and the `construct intake metrics` CLI.
7
+ *
8
+ * Data sources:
9
+ * .cx/intake/pending/ — JSON files waiting for triage
10
+ * .cx/intake/processed/ — JSON files already handled
11
+ * .cx/intake/skipped/ — JSON files intentionally skipped
12
+ * ~/.cx/intake/recommendations-index.json — recommendations
13
+ *
14
+ * All read-only. Never writes to the intake queue.
15
+ */
16
+
17
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { homedir } from 'node:os';
20
+
21
+ /**
22
+ * Scan an intake directory and return file metadata.
23
+ */
24
+ function scanDir(dir) {
25
+ if (!existsSync(dir)) return [];
26
+ try {
27
+ return readdirSync(dir)
28
+ .filter(f => f.endsWith('.json'))
29
+ .map(f => {
30
+ const fp = join(dir, f);
31
+ try {
32
+ const stat = statSync(fp);
33
+ const content = readFileSync(fp, 'utf8');
34
+ const parsed = JSON.parse(content);
35
+ return {
36
+ id: f.replace('.json', ''),
37
+ createdAt: parsed.createdAt || parsed.triage?.createdAt || stat.birthtime.toISOString(),
38
+ size: stat.size,
39
+ mtimeMs: stat.mtimeMs,
40
+ triage: parsed.triage?.intakeType || 'unknown',
41
+ owner: parsed.triage?.primaryOwner || '—',
42
+ action: parsed.triage?.recommendedAction || '—',
43
+ customers: parsed.customers?.length || 0,
44
+ };
45
+ } catch {
46
+ return null;
47
+ }
48
+ })
49
+ .filter(Boolean)
50
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Compute metrics for an intake queue.
58
+ *
59
+ * @param {object} [opts]
60
+ * @param {string} [opts.rootDir] - Project root (default: cwd)
61
+ * @returns {object} Metrics summary
62
+ */
63
+ export function computeIntakeMetrics({ rootDir } = {}) {
64
+ const root = rootDir || process.cwd();
65
+ const pendingDir = join(root, '.cx', 'intake', 'pending');
66
+ const processedDir = join(root, '.cx', 'intake', 'processed');
67
+ const skippedDir = join(root, '.cx', 'intake', 'skipped');
68
+
69
+ const pending = scanDir(pendingDir);
70
+ const processed = scanDir(processedDir);
71
+ const skipped = scanDir(skippedDir);
72
+
73
+ // Volume
74
+ const volume = {
75
+ pending: pending.length,
76
+ processed: processed.length,
77
+ skipped: skipped.length,
78
+ total: pending.length + processed.length + skipped.length,
79
+ };
80
+
81
+ // Velocity — items per day over trailing 7d window
82
+ const now = Date.now();
83
+ const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
84
+ const allItems = [...pending, ...processed, ...skipped];
85
+ const recentItems = allItems.filter(i => (now - new Date(i.createdAt).getTime()) < SEVEN_DAYS);
86
+ const velocity = Number((recentItems.length / 7).toFixed(1));
87
+
88
+ // Processing time — how long between creation and processing
89
+ const processingTimes = [];
90
+ for (const item of processed) {
91
+ try {
92
+ const fp = join(processedDir, `${item.id}.json`);
93
+ const raw = JSON.parse(readFileSync(fp, 'utf8'));
94
+ if (raw.createdAt && raw.processedAt) {
95
+ const ms = new Date(raw.processedAt) - new Date(raw.createdAt);
96
+ if (ms > 0) processingTimes.push(ms);
97
+ }
98
+ } catch { /* skip */ }
99
+ }
100
+ const avgProcessingTimeMs = processingTimes.length
101
+ ? Math.round(processingTimes.reduce((s, t) => s + t, 0) / processingTimes.length)
102
+ : null;
103
+
104
+ // — By type breakdown
105
+ const byType = {};
106
+ for (const item of allItems) {
107
+ byType[item.triage] = (byType[item.triage] || 0) + 1;
108
+ }
109
+
110
+ // — By owner breakdown
111
+ const byOwner = {};
112
+ for (const item of allItems) {
113
+ byOwner[item.owner] = (byOwner[item.owner] || 0) + 1;
114
+ }
115
+
116
+ // — Acceptance rate (processed / total actionable)
117
+ const actionable = volume.processed + volume.skipped;
118
+ const acceptRate = actionable > 0
119
+ ? Number((volume.processed / actionable * 100).toFixed(1))
120
+ : null;
121
+
122
+ // — Customer-linked items
123
+ const customerLinked = allItems.filter(i => i.customers > 0).length;
124
+
125
+ // — Metrics from recommendation store
126
+ let recStats = { active: 0, dismissed: 0, total: 0 };
127
+ try {
128
+ const recIndexPath = join(homedir(), '.cx', 'intake', 'recommendations-index.json');
129
+ if (existsSync(recIndexPath)) {
130
+ const index = JSON.parse(readFileSync(recIndexPath, 'utf8'));
131
+ const entries = Object.values(index);
132
+ recStats = {
133
+ total: entries.length,
134
+ active: entries.filter(e => !e.dismissedAt && !e.supersededAt).length,
135
+ dismissed: entries.filter(e => e.dismissedAt).length,
136
+ };
137
+ }
138
+ } catch { /* skip */ }
139
+
140
+ return {
141
+ volume,
142
+ velocity,
143
+ recentItems: recentItems.length,
144
+ avgProcessingTimeMs,
145
+ avgProcessingTimeFormatted: avgProcessingTimeMs
146
+ ? formatDuration(avgProcessingTimeMs)
147
+ : null,
148
+ byType,
149
+ byOwner,
150
+ acceptRate,
151
+ customerLinked,
152
+ recommendations: recStats,
153
+ sampledAt: new Date().toISOString(),
154
+ };
155
+ }
156
+
157
+ function formatDuration(ms) {
158
+ if (ms < 1000) return `${ms}ms`;
159
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
160
+ const mins = Math.floor(ms / 60000);
161
+ const secs = Math.round((ms % 60000) / 1000);
162
+ return `${mins}m ${secs}s`;
163
+ }
164
+
165
+ /**
166
+ * Get the age of the oldest pending item.
167
+ *
168
+ * @param {object} [opts]
169
+ * @param {string} [opts.rootDir]
170
+ * @returns {{ oldest: string|null, ageHours: number|null, count: number }}
171
+ */
172
+ export function pendingAge({ rootDir } = {}) {
173
+ const root = rootDir || process.cwd();
174
+ const pendingDir = join(root, '.cx', 'intake', 'pending');
175
+ const items = scanDir(pendingDir);
176
+ if (!items.length) return { oldest: null, ageHours: null, count: 0 };
177
+
178
+ const oldest = items.reduce((a, b) =>
179
+ new Date(a.createdAt) < new Date(b.createdAt) ? a : b
180
+ );
181
+ const ageHours = (Date.now() - new Date(oldest.createdAt).getTime()) / (60 * 60 * 1000);
182
+
183
+ return {
184
+ oldest: oldest.id,
185
+ oldestCreatedAt: oldest.createdAt,
186
+ ageHours: Number(ageHours.toFixed(1)),
187
+ count: items.length,
188
+ oldItemsCount: items.filter(i => (Date.now() - new Date(i.createdAt).getTime()) > 24 * 60 * 60 * 1000).length,
189
+ };
190
+ }