@geraldmaron/construct 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +10 -7
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1116 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +525 -38
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
@@ -0,0 +1,192 @@
1
+ /**
2
+ * lib/embed/providers/jira.mjs — Jira (Atlassian) provider for embed mode.
3
+ *
4
+ * Reads issues from Jira Cloud using the REST API v3.
5
+ * Zero external deps — uses Node's built-in fetch.
6
+ *
7
+ * Supported refs:
8
+ * issues Issues matching a JQL query or project filter
9
+ * sprints Active sprint issues for a board
10
+ *
11
+ * Source config fields (embed.yaml):
12
+ * provider: jira
13
+ * project: PROJ # project key — drives the default JQL
14
+ * projects: # OR list of project keys
15
+ * - PROJ
16
+ * - INFRA
17
+ * jql: "project = PROJ AND sprint in openSprints()" # override JQL
18
+ * refs: [issues]
19
+ * limit: 50 # max issues (default: 50)
20
+ * fields: [summary,status,assignee,priority] # optional field subset
21
+ */
22
+
23
+ export class JiraProvider {
24
+ #baseUrl;
25
+ #auth;
26
+ #fetchFn;
27
+
28
+ constructor({ baseUrl, email, token, fetchFn = globalThis.fetch } = {}) {
29
+ if (!baseUrl || !email || !token) {
30
+ throw new Error('JiraProvider requires baseUrl, email, and token');
31
+ }
32
+ this.#baseUrl = baseUrl.replace(/\/$/, '');
33
+ this.#auth = `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
34
+ this.#fetchFn = fetchFn;
35
+ }
36
+
37
+ /**
38
+ * Default sources used when no embed.yaml is present.
39
+ * Queries all non-done issues assigned to the current user.
40
+ */
41
+ defaultSources() {
42
+ return [
43
+ {
44
+ provider: 'jira',
45
+ refs: ['issues'],
46
+ jql: 'assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC',
47
+ limit: 50,
48
+ },
49
+ ];
50
+ }
51
+
52
+ /**
53
+ * @param {string} ref - 'issues' | 'sprints'
54
+ * @param {object} opts - source config (project, projects, jql, limit, fields)
55
+ * @returns {Promise<Item[]>}
56
+ */
57
+ async read(ref, opts = {}) {
58
+ switch (ref) {
59
+ case 'issues': return this.#listIssues(opts);
60
+ case 'sprints': return this.#listSprintIssues(opts);
61
+ default: throw new Error(`Jira provider: unknown ref "${ref}"`);
62
+ }
63
+ }
64
+
65
+ async #listIssues(opts) {
66
+ const limit = Number(opts.limit ?? 50);
67
+ const jql = opts.jql ?? buildDefaultJql(opts);
68
+ const fields = opts.fields ?? ['summary', 'status', 'assignee', 'priority', 'issuetype', 'labels', 'updated', 'created'];
69
+
70
+ const data = await this.#post('/rest/api/3/search/jql', {
71
+ jql,
72
+ maxResults: limit,
73
+ fields,
74
+ });
75
+
76
+ return (data.issues ?? []).map((issue) => ({
77
+ type: 'issue',
78
+ source: 'jira',
79
+ key: issue.key,
80
+ id: issue.id,
81
+ title: issue.fields?.summary ?? '',
82
+ status: issue.fields?.status?.name ?? null,
83
+ statusCategory: issue.fields?.status?.statusCategory?.name ?? null,
84
+ assignee: issue.fields?.assignee?.displayName ?? null,
85
+ priority: issue.fields?.priority?.name ?? null,
86
+ issueType: issue.fields?.issuetype?.name ?? null,
87
+ labels: issue.fields?.labels ?? [],
88
+ project: issue.key.split('-')[0],
89
+ url: `${this.#baseUrl}/browse/${issue.key}`,
90
+ createdAt: issue.fields?.created,
91
+ updatedAt: issue.fields?.updated,
92
+ summary: `[${issue.key}] ${issue.fields?.summary ?? ''}`,
93
+ }));
94
+ }
95
+
96
+ async #listSprintIssues(opts) {
97
+ // Fetch all boards, find ones matching the project, get active sprint
98
+ const projects = resolveProjects(opts);
99
+ if (!projects.length) throw new Error('Jira sprints source requires "project" or "projects"');
100
+
101
+ const results = [];
102
+ for (const projectKey of projects) {
103
+ try {
104
+ const boards = await this.#get(`/rest/agile/1.0/board?projectKeyOrId=${projectKey}&type=scrum`);
105
+ const board = boards.values?.[0];
106
+ if (!board) continue;
107
+
108
+ const sprints = await this.#get(`/rest/agile/1.0/board/${board.id}/sprint?state=active`);
109
+ const sprint = sprints.values?.[0];
110
+ if (!sprint) continue;
111
+
112
+ const sprintIssues = await this.#get(
113
+ `/rest/agile/1.0/sprint/${sprint.id}/issue?maxResults=${opts.limit ?? 50}`,
114
+ );
115
+ const items = (sprintIssues.issues ?? []).map((issue) => ({
116
+ type: 'issue',
117
+ source: 'jira',
118
+ sprint: sprint.name,
119
+ key: issue.key,
120
+ title: issue.fields?.summary ?? '',
121
+ status: issue.fields?.status?.name ?? null,
122
+ assignee: issue.fields?.assignee?.displayName ?? null,
123
+ priority: issue.fields?.priority?.name ?? null,
124
+ project: projectKey,
125
+ url: `${this.#baseUrl}/browse/${issue.key}`,
126
+ updatedAt: issue.fields?.updated,
127
+ summary: `[${issue.key}] ${issue.fields?.summary ?? ''}`,
128
+ }));
129
+ results.push(...items);
130
+ } catch (err) {
131
+ results.push({ type: 'error', source: 'jira', project: projectKey, message: err.message });
132
+ }
133
+ }
134
+ return results;
135
+ }
136
+
137
+ async #get(path) {
138
+ const controller = new AbortController();
139
+ const timer = setTimeout(() => controller.abort(), 10_000);
140
+ try {
141
+ const res = await this.#fetchFn(`${this.#baseUrl}${path}`, {
142
+ headers: { Authorization: this.#auth, Accept: 'application/json' },
143
+ signal: controller.signal,
144
+ });
145
+ if (!res.ok) {
146
+ const body = await res.text().catch(() => '');
147
+ throw new Error(`Jira API ${res.status}: ${body.slice(0, 200)}`);
148
+ }
149
+ return res.json();
150
+ } finally {
151
+ clearTimeout(timer);
152
+ }
153
+ }
154
+
155
+ async #post(path, body) {
156
+ const controller = new AbortController();
157
+ const timer = setTimeout(() => controller.abort(), 10_000);
158
+ try {
159
+ const res = await this.#fetchFn(`${this.#baseUrl}${path}`, {
160
+ method: 'POST',
161
+ headers: {
162
+ Authorization: this.#auth,
163
+ Accept: 'application/json',
164
+ 'Content-Type': 'application/json',
165
+ },
166
+ body: JSON.stringify(body),
167
+ signal: controller.signal,
168
+ });
169
+ if (!res.ok) {
170
+ const text = await res.text().catch(() => '');
171
+ throw new Error(`Jira API ${res.status}: ${text.slice(0, 200)}`);
172
+ }
173
+ return res.json();
174
+ } finally {
175
+ clearTimeout(timer);
176
+ }
177
+ }
178
+ }
179
+
180
+ function resolveProjects(opts) {
181
+ if (Array.isArray(opts.projects)) return opts.projects.filter(Boolean);
182
+ if (opts.project) return [opts.project];
183
+ return [];
184
+ }
185
+
186
+ function buildDefaultJql(opts) {
187
+ const projects = resolveProjects(opts);
188
+ const projectClause = projects.length
189
+ ? `project in (${projects.map((p) => `"${p}"`).join(', ')}) AND `
190
+ : '';
191
+ return `${projectClause}statusCategory != Done ORDER BY updated DESC`;
192
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * lib/embed/providers/linear.mjs — Linear provider for embed mode.
3
+ *
4
+ * Reads issues from Linear using the GraphQL API.
5
+ * Zero external deps — uses Node's built-in fetch.
6
+ *
7
+ * Supported refs:
8
+ * issues Open/in-progress issues
9
+ * cycles Active cycle issues
10
+ *
11
+ * Source config fields (embed.yaml):
12
+ * provider: linear
13
+ * team: ENG # team key or ID (optional — all teams if omitted)
14
+ * project: My Project # project name filter (optional)
15
+ * refs: [issues]
16
+ * states: [Todo, In Progress] # filter by state names (default: excludes Done/Cancelled)
17
+ * limit: 50 # max issues (default: 50)
18
+ */
19
+
20
+ const LINEAR_API = 'https://api.linear.app/graphql';
21
+
22
+ export class LinearProvider {
23
+ #apiKey;
24
+ #fetchFn;
25
+
26
+ constructor({ apiKey, fetchFn = globalThis.fetch } = {}) {
27
+ if (!apiKey) throw new Error('LinearProvider requires an apiKey');
28
+ this.#apiKey = apiKey;
29
+ this.#fetchFn = fetchFn;
30
+ }
31
+
32
+ /**
33
+ * Auto-discovery: return assigned non-completed issues when LINEAR_API_KEY is present.
34
+ */
35
+ defaultSources() {
36
+ return [{ provider: 'linear', refs: ['issues'], limit: 50 }];
37
+ }
38
+
39
+ /**
40
+ * @param {string} ref - 'issues' | 'cycles'
41
+ * @param {object} opts - source config (team, project, states, limit)
42
+ * @returns {Promise<Item[]>}
43
+ */
44
+ async read(ref, opts = {}) {
45
+ switch (ref) {
46
+ case 'issues': return this.#listIssues(opts);
47
+ case 'cycles': return this.#listCycleIssues(opts);
48
+ default: throw new Error(`Linear provider: unknown ref "${ref}"`);
49
+ }
50
+ }
51
+
52
+ async #listIssues(opts) {
53
+ const limit = Number(opts.limit ?? 50);
54
+ const stateFilter = Array.isArray(opts.states) ? opts.states : null;
55
+
56
+ // Build filter clauses
57
+ const filters = [];
58
+ if (opts.team) filters.push(`team: { key: { eq: "${opts.team}" } }`);
59
+ if (stateFilter) {
60
+ const stateIn = stateFilter.map((s) => `"${s}"`).join(', ');
61
+ filters.push(`state: { name: { in: [${stateIn}] } }`);
62
+ } else {
63
+ // Default: exclude terminal states
64
+ filters.push(`state: { type: { nin: ["completed", "cancelled"] } }`);
65
+ }
66
+ if (opts.project) filters.push(`project: { name: { eq: "${opts.project}" } }`);
67
+
68
+ const filterClause = filters.length ? `filter: { ${filters.join(', ')} }` : '';
69
+
70
+ const query = `{
71
+ issues(first: ${limit} ${filterClause} orderBy: updatedAt) {
72
+ nodes {
73
+ id
74
+ identifier
75
+ title
76
+ description
77
+ priority
78
+ priorityLabel
79
+ state { name type }
80
+ assignee { name }
81
+ team { name key }
82
+ project { name }
83
+ url
84
+ createdAt
85
+ updatedAt
86
+ labels { nodes { name } }
87
+ }
88
+ }
89
+ }`;
90
+
91
+ const data = await this.#graphql(query);
92
+ return (data?.issues?.nodes ?? []).map((issue) => ({
93
+ type: 'issue',
94
+ source: 'linear',
95
+ id: issue.identifier,
96
+ title: issue.title,
97
+ description: issue.description?.slice(0, 200) ?? null,
98
+ state: issue.state?.name,
99
+ stateType: issue.state?.type,
100
+ priority: issue.priorityLabel,
101
+ assignee: issue.assignee?.name ?? null,
102
+ team: issue.team?.key,
103
+ teamName: issue.team?.name,
104
+ project: issue.project?.name ?? null,
105
+ labels: (issue.labels?.nodes ?? []).map((l) => l.name),
106
+ url: issue.url,
107
+ createdAt: issue.createdAt,
108
+ updatedAt: issue.updatedAt,
109
+ summary: `[${issue.identifier}] ${issue.title}`,
110
+ }));
111
+ }
112
+
113
+ async #listCycleIssues(opts) {
114
+ const limit = Number(opts.limit ?? 50);
115
+ const teamFilter = opts.team ? `filter: { team: { key: { eq: "${opts.team}" } } }` : '';
116
+
117
+ const query = `{
118
+ cycles(first: 1 ${teamFilter} filter: { isActive: { eq: true } }) {
119
+ nodes {
120
+ id
121
+ name
122
+ number
123
+ startsAt
124
+ endsAt
125
+ team { name key }
126
+ issues(first: ${limit}) {
127
+ nodes {
128
+ id
129
+ identifier
130
+ title
131
+ state { name type }
132
+ assignee { name }
133
+ priority
134
+ priorityLabel
135
+ url
136
+ updatedAt
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }`;
142
+
143
+ const data = await this.#graphql(query);
144
+ const cycle = data?.cycles?.nodes?.[0];
145
+ if (!cycle) return [];
146
+
147
+ return (cycle.issues?.nodes ?? []).map((issue) => ({
148
+ type: 'issue',
149
+ source: 'linear',
150
+ cycle: `${cycle.team?.key ?? ''} Cycle ${cycle.number}`,
151
+ id: issue.identifier,
152
+ title: issue.title,
153
+ state: issue.state?.name,
154
+ stateType: issue.state?.type,
155
+ priority: issue.priorityLabel,
156
+ assignee: issue.assignee?.name ?? null,
157
+ url: issue.url,
158
+ updatedAt: issue.updatedAt,
159
+ summary: `[${issue.identifier}] ${issue.title}`,
160
+ }));
161
+ }
162
+
163
+ async #graphql(query) {
164
+ const controller = new AbortController();
165
+ const timer = setTimeout(() => controller.abort(), 15_000);
166
+ try {
167
+ const res = await this.#fetchFn(LINEAR_API, {
168
+ method: 'POST',
169
+ headers: {
170
+ Authorization: this.#apiKey,
171
+ 'Content-Type': 'application/json',
172
+ },
173
+ body: JSON.stringify({ query }),
174
+ signal: controller.signal,
175
+ });
176
+ if (!res.ok) throw new Error(`Linear API HTTP ${res.status}`);
177
+ const json = await res.json();
178
+ if (json.errors?.length) {
179
+ throw new Error(`Linear GraphQL: ${json.errors[0].message}`);
180
+ }
181
+ return json.data;
182
+ } finally {
183
+ clearTimeout(timer);
184
+ }
185
+ }
186
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * lib/embed/providers/registry.mjs — ProviderRegistry.
3
+ *
4
+ * Holds named provider instances and exposes an async factory that builds
5
+ * the standard set from environment variables. Each provider implements:
6
+ *
7
+ * read(ref, opts) → Promise<Item[]>
8
+ * ref: source-type string e.g. 'prs', 'issues', 'commits', 'messages'
9
+ * opts: source config fields (repo, channel, project, …)
10
+ *
11
+ * write(action) → Promise<void> (optional — only mutation providers)
12
+ * action: { type, channel?, text?, … }
13
+ *
14
+ * Providers are registered by name and can be swapped for test doubles.
15
+ */
16
+
17
+ export class ProviderRegistry {
18
+ #providers = new Map();
19
+
20
+ /**
21
+ * Register a provider under one or more names.
22
+ * @param {string|string[]} names
23
+ * @param {object} provider
24
+ */
25
+ register(names, provider) {
26
+ for (const name of [].concat(names)) {
27
+ this.#providers.set(name, provider);
28
+ }
29
+ return this;
30
+ }
31
+
32
+ /**
33
+ * Retrieve a provider by name. Returns null if not registered.
34
+ * @param {string} name
35
+ */
36
+ get(name) {
37
+ return this.#providers.get(name) ?? null;
38
+ }
39
+
40
+ /**
41
+ * Build the union of all registered providers' default sources.
42
+ * Called when no embed.yaml is present.
43
+ * @param {object} env - Merged environment (for providers that need it, e.g. GitHub repos)
44
+ * @returns {object[]} - Array of source config objects ready for SnapshotEngine
45
+ */
46
+ autoSources(env = {}) {
47
+ const sources = [];
48
+ for (const provider of this.#providers.values()) {
49
+ if (typeof provider.defaultSources === 'function') {
50
+ sources.push(...provider.defaultSources(env));
51
+ }
52
+ }
53
+ // Deduplicate by provider+refs signature — multiple registry aliases can point to same instance
54
+ const seen = new Set();
55
+ return sources.filter((s) => {
56
+ const key = `${s.provider}:${(s.refs ?? []).join(',')}`;
57
+ if (seen.has(key)) return false;
58
+ seen.add(key);
59
+ return true;
60
+ });
61
+ }
62
+
63
+ /**
64
+ * List all registered provider names.
65
+ */
66
+ names() {
67
+ return [...this.#providers.keys()];
68
+ }
69
+
70
+ /**
71
+ * Build a registry pre-populated with all concrete providers derived
72
+ * from the supplied environment. Providers whose required credentials
73
+ * are absent are silently omitted — callers get a warning at runtime
74
+ * when a source references a missing provider.
75
+ *
76
+ * @param {object} env - Merged environment (loadConstructEnv output)
77
+ * @returns {Promise<ProviderRegistry>}
78
+ */
79
+ static async fromEnv(env = process.env) {
80
+ const registry = new ProviderRegistry();
81
+
82
+ if (env.GITHUB_TOKEN || env.GITHUB_PERSONAL_ACCESS_TOKEN) {
83
+ const { GitHubProvider } = await import('./github.mjs');
84
+ registry.register(['github', 'gh'], new GitHubProvider({
85
+ token: env.GITHUB_TOKEN || env.GITHUB_PERSONAL_ACCESS_TOKEN,
86
+ }));
87
+ }
88
+
89
+ if (env.SLACK_BOT_TOKEN) {
90
+ const { SlackProvider } = await import('./slack.mjs');
91
+ registry.register(['slack'], new SlackProvider({
92
+ token: env.SLACK_BOT_TOKEN,
93
+ teamId: env.SLACK_TEAM_ID,
94
+ }));
95
+ }
96
+
97
+ if (env.LINEAR_API_KEY) {
98
+ const { LinearProvider } = await import('./linear.mjs');
99
+ registry.register(['linear'], new LinearProvider({
100
+ apiKey: env.LINEAR_API_KEY,
101
+ }));
102
+ }
103
+
104
+ if (env.JIRA_API_TOKEN && env.JIRA_USER_EMAIL && env.JIRA_BASE_URL) {
105
+ const { JiraProvider } = await import('./jira.mjs');
106
+ registry.register(['jira', 'atlassian'], new JiraProvider({
107
+ baseUrl: env.JIRA_BASE_URL,
108
+ email: env.JIRA_USER_EMAIL,
109
+ token: env.JIRA_API_TOKEN,
110
+ }));
111
+ }
112
+
113
+ return registry;
114
+ }
115
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * lib/embed/providers/slack.mjs — Slack provider for embed mode.
3
+ *
4
+ * Reads recent messages from one or more channels using the Slack Web API.
5
+ * Zero external deps — uses Node's built-in fetch.
6
+ *
7
+ * Supported refs:
8
+ * messages Recent messages in a channel
9
+ *
10
+ * Source config fields (embed.yaml):
11
+ * provider: slack
12
+ * channel: C12345678 # single channel ID or name
13
+ * channels: # OR list, optionally with intent tags
14
+ * - general # no intent — defaults to 'internal'
15
+ * - incidents:risk # channel:intent — drives observation category
16
+ * - customer-feedback:external
17
+ * refs: [messages]
18
+ * limit: 20 # max messages per channel (default: 20)
19
+ * oldest: 86400 # seconds of history to fetch (default: 86400 = 24h)
20
+ *
21
+ * SLACK_CHANNELS env format (config.env):
22
+ * SLACK_CHANNELS=#eng-general,#incidents:risk,#decisions:decision,#customer:external
23
+ *
24
+ * Intent values and their observation category mapping:
25
+ * internal → insight (default — team chat, general discussion)
26
+ * risk → anti-pattern (incidents, alerts, on-call, escalations)
27
+ * decision → decision (decision-making channels, RFC discussions)
28
+ * external → insight + tag:external (customer feedback, support, sales)
29
+ * how-to → pattern (tips, best practices, knowledge sharing)
30
+ * (none) → insight (fallback)
31
+ */
32
+
33
+ const SLACK_API = 'https://slack.com/api';
34
+
35
+ export class SlackProvider {
36
+ #token;
37
+ #teamId;
38
+ #fetchFn;
39
+
40
+ constructor({ token, teamId, fetchFn = globalThis.fetch } = {}) {
41
+ if (!token) throw new Error('SlackProvider requires a token');
42
+ this.#token = token;
43
+ this.#teamId = teamId;
44
+ this.#fetchFn = fetchFn;
45
+ }
46
+
47
+ /**
48
+ * Auto-discovery: return one source per intent group from SLACK_CHANNELS.
49
+ * Format: SLACK_CHANNELS=#eng-general,#incidents:risk,#decisions:decision
50
+ * Channels without an intent tag default to 'internal'.
51
+ * Each unique intent becomes its own source so snapshot sections are typed.
52
+ */
53
+ defaultSources(env = {}) {
54
+ const raw = env.SLACK_CHANNELS ?? env.SLACK_CHANNEL ?? '';
55
+ const entries = raw.split(',').map((c) => c.trim()).filter(Boolean).map(parseChannelEntry);
56
+ if (!entries.length) return [];
57
+
58
+ // Group channels by intent so each source section carries a consistent intent
59
+ const byIntent = new Map();
60
+ for (const { channel, intent } of entries) {
61
+ if (!byIntent.has(intent)) byIntent.set(intent, []);
62
+ byIntent.get(intent).push(channel);
63
+ }
64
+
65
+ return [...byIntent.entries()].map(([intent, channels]) => ({
66
+ provider: 'slack',
67
+ refs: ['messages'],
68
+ channels,
69
+ intent,
70
+ oldest: 86400,
71
+ limit: 50,
72
+ }));
73
+ }
74
+
75
+ /**
76
+ * @param {string} ref - 'messages'
77
+ * @param {object} opts - source config (channel, channels, limit, oldest)
78
+ * @returns {Promise<Item[]>}
79
+ */
80
+ async read(ref, opts = {}) {
81
+ if (ref !== 'messages') throw new Error(`Slack provider: unknown ref "${ref}"`);
82
+
83
+ const channelEntries = resolveChannels(opts);
84
+ if (!channelEntries.length) {
85
+ throw new Error(`Slack source requires "channel" or "channels" field`);
86
+ }
87
+
88
+ const intent = opts.intent ?? 'internal';
89
+ const limit = Number(opts.limit ?? 20);
90
+ const oldestSec = Number(opts.oldest ?? 86400);
91
+ const oldest = String(Math.floor(Date.now() / 1000) - oldestSec);
92
+ const results = [];
93
+
94
+ for (const { channel, intent: entryIntent } of channelEntries) {
95
+ const effectiveIntent = entryIntent ?? intent;
96
+ try {
97
+ const channelId = await this.#resolveChannelId(channel);
98
+ const items = await this.#listMessages(channelId, { limit, oldest });
99
+ results.push(...items.map((i) => ({ ...i, channelName: channel, intent: effectiveIntent })));
100
+ } catch (err) {
101
+ results.push({ type: 'error', source: 'slack', channel, message: err.message });
102
+ }
103
+ }
104
+
105
+ return results;
106
+ }
107
+
108
+ async #resolveChannelId(channelOrId) {
109
+ // Already looks like an ID (C... or D... or G...)
110
+ if (/^[A-Z][A-Z0-9]{8,}$/.test(channelOrId)) return channelOrId;
111
+
112
+ // Resolve by name
113
+ const name = channelOrId.replace(/^#/, '');
114
+ const data = await this.#apiGet('conversations.list', { limit: 200, exclude_archived: true });
115
+ const match = (data.channels ?? []).find(
116
+ (c) => c.name === name || c.name_normalized === name,
117
+ );
118
+ if (!match) throw new Error(`Slack channel not found: ${channelOrId}`);
119
+ return match.id;
120
+ }
121
+
122
+ async #listMessages(channelId, { limit, oldest }) {
123
+ const data = await this.#apiGet('conversations.history', {
124
+ channel: channelId,
125
+ limit,
126
+ oldest,
127
+ });
128
+
129
+ return (data.messages ?? [])
130
+ .filter((m) => m.type === 'message' && !m.subtype)
131
+ .map((m) => ({
132
+ type: 'message',
133
+ source: 'slack',
134
+ channel: channelId,
135
+ user: m.user ?? m.username ?? 'unknown',
136
+ text: m.text ?? '',
137
+ ts: m.ts,
138
+ threadTs: m.thread_ts ?? null,
139
+ replyCount: m.reply_count ?? 0,
140
+ reactions: (m.reactions ?? []).map((r) => `${r.name}:${r.count}`),
141
+ url: this.#teamId
142
+ ? `https://app.slack.com/client/${this.#teamId}/${channelId}/p${m.ts.replace('.', '')}`
143
+ : null,
144
+ summary: `<${m.user ?? 'unknown'}> ${(m.text ?? '').slice(0, 80)}`,
145
+ }));
146
+ }
147
+
148
+ async #apiGet(method, params = {}) {
149
+ const qs = new URLSearchParams(
150
+ Object.entries(params).map(([k, v]) => [k, String(v)]),
151
+ ).toString();
152
+ const controller = new AbortController();
153
+ const timer = setTimeout(() => controller.abort(), 10_000);
154
+ try {
155
+ const res = await this.#fetchFn(`${SLACK_API}/${method}?${qs}`, {
156
+ headers: {
157
+ Authorization: `Bearer ${this.#token}`,
158
+ 'Content-Type': 'application/json',
159
+ },
160
+ signal: controller.signal,
161
+ });
162
+ if (!res.ok) throw new Error(`Slack HTTP ${res.status}`);
163
+ const json = await res.json();
164
+ if (!json.ok) throw new Error(`Slack API error: ${json.error ?? 'unknown'}`);
165
+ return json;
166
+ } finally {
167
+ clearTimeout(timer);
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Parse a channel entry that may carry an intent tag: "#name:intent" or "name".
174
+ * Returns { channel: string, intent: string }.
175
+ */
176
+ function parseChannelEntry(raw) {
177
+ const s = String(raw).trim().replace(/^#/, '');
178
+ const colonIdx = s.lastIndexOf(':');
179
+ // Avoid splitting channel IDs like C12345678 — intents are alpha-only words
180
+ if (colonIdx > 0 && /^[a-z-]+$/.test(s.slice(colonIdx + 1))) {
181
+ return { channel: s.slice(0, colonIdx), intent: s.slice(colonIdx + 1) };
182
+ }
183
+ return { channel: s, intent: 'internal' };
184
+ }
185
+
186
+ /**
187
+ * Map an intent label to an observation category.
188
+ */
189
+ export function intentToCategory(intent) {
190
+ switch ((intent ?? '').toLowerCase()) {
191
+ case 'risk': return 'anti-pattern';
192
+ case 'decision': return 'decision';
193
+ case 'how-to':
194
+ case 'howto': return 'pattern';
195
+ default: return 'insight';
196
+ }
197
+ }
198
+
199
+ function resolveChannels(opts) {
200
+ if (Array.isArray(opts.channels)) return opts.channels.filter(Boolean).map(parseChannelEntry);
201
+ if (opts.channel) return [parseChannelEntry(opts.channel)];
202
+ return [];
203
+ }