@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,142 @@
1
+ /**
2
+ * lib/provider-capabilities.js — Model-agnostic provider capability interface.
3
+ *
4
+ * Follows the embeddings-engine.js pattern:
5
+ * - This file is the model-agnostic router.
6
+ * - Provider-specific logic lives in provider-capabilities-*.js adapters.
7
+ * - All adapters return the same capability shape.
8
+ *
9
+ * Standard capability interface (all adapters implement this shape):
10
+ * - cacheControl: boolean
11
+ * - cacheMechanism: 'annotation' | 'automatic' | 'resource' | 'none'
12
+ * - cacheTTL: { '5m': number, '1h': number } | null (tokens, not ms)
13
+ * - structuredOutput: boolean
14
+ * - maxContextWindow: number (tokens)
15
+ * - tokenRatio: number (chars per token for this provider)
16
+ * - annotationFormat: 'anthropic' | 'google' | 'openai' | 'none'
17
+ * - annotationHeaders: object | null
18
+ */
19
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+ import { homedir } from 'node:os';
22
+
23
+ const CAPABILITY_CACHE_PATH = join(homedir(), '.cx', 'provider-capabilities.json');
24
+ const CAPABILITY_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
25
+
26
+ const ADAPTERS = {
27
+ anthropic: () => import('./provider-capabilities-anthropic.js'),
28
+ 'anthropic-direct': () => import('./provider-capabilities-anthropic.js'),
29
+ google: () => import('./provider-capabilities-google.js'),
30
+ openai: () => import('./provider-capabilities-openai.js'),
31
+ deepseek: () => import('./provider-capabilities-deepseek.js'),
32
+ generic: () => import('./provider-capabilities-generic.js'),
33
+ };
34
+
35
+ function resolveAdapterKey(modelId) {
36
+ const id = String(modelId || '').toLowerCase();
37
+ if (/^anthropic\//.test(id) || /^openrouter\/anthropic\//.test(id)) return 'anthropic';
38
+ if (/^google\//.test(id) || /^openrouter\/google\//.test(id)) return 'google';
39
+ if (/^openai\//.test(id) || /^openrouter\/openai\//.test(id) || /^github-copilot\//.test(id)) return 'openai';
40
+ if (/^deepseek\//.test(id) || /^openrouter\/deepseek\//.test(id)) return 'deepseek';
41
+ return 'generic';
42
+ }
43
+
44
+ function readCapabilityCache() {
45
+ try {
46
+ if (!existsSync(CAPABILITY_CACHE_PATH)) return {};
47
+ const cached = JSON.parse(readFileSync(CAPABILITY_CACHE_PATH, 'utf8'));
48
+ if (cached?.fetchedAt && Date.now() - cached.fetchedAt < CAPABILITY_CACHE_TTL_MS) {
49
+ return cached.capabilities || {};
50
+ }
51
+ } catch { /* stale or corrupt */ }
52
+ return {};
53
+ }
54
+
55
+ function writeCapabilityCache(capabilities) {
56
+ try {
57
+ mkdirSync(join(homedir(), '.cx'), { recursive: true });
58
+ writeFileSync(CAPABILITY_CACHE_PATH, JSON.stringify({
59
+ fetchedAt: Date.now(),
60
+ capabilities,
61
+ }, null, 2));
62
+ } catch { /* best effort */ }
63
+ }
64
+
65
+ let _cache = null;
66
+ function getCache() {
67
+ if (_cache === null) _cache = readCapabilityCache();
68
+ return _cache;
69
+ }
70
+
71
+ /**
72
+ * Resolve provider capabilities for a given model ID.
73
+ * Returns the standard capability interface.
74
+ *
75
+ * @param {string} modelId - The model identifier (e.g., "anthropic/claude-opus-4-6")
76
+ * @returns {object} Capability object with the standard shape
77
+ */
78
+ export async function resolveProviderCapabilities(modelId) {
79
+ const adapterKey = resolveAdapterKey(modelId);
80
+ const loader = ADAPTERS[adapterKey] || ADAPTERS.generic;
81
+
82
+ try {
83
+ const { capabilities } = await loader();
84
+ return capabilities(modelId);
85
+ } catch {
86
+ // Fallback to generic
87
+ const { capabilities } = await ADAPTERS.generic();
88
+ return capabilities(modelId);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Synchronous version — uses cache if available, otherwise returns generic.
94
+ * Use this in hot paths where async is not possible.
95
+ *
96
+ * @param {string} modelId
97
+ * @returns {object} Capability object
98
+ */
99
+ export function resolveProviderCapabilitiesSync(modelId) {
100
+ const adapterKey = resolveAdapterKey(modelId);
101
+ const cache = getCache();
102
+
103
+ // Check cache first (keyed by adapter type, not full model ID)
104
+ if (cache[adapterKey]) return cache[adapterKey];
105
+
106
+ // Return generic synchronously (can't load adapter in sync context)
107
+ return {
108
+ cacheControl: false,
109
+ cacheMechanism: 'none',
110
+ cacheTTL: null,
111
+ structuredOutput: false,
112
+ maxContextWindow: 200000,
113
+ tokenRatio: 4,
114
+ annotationFormat: 'none',
115
+ annotationHeaders: null,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Probe provider for live capabilities (async).
121
+ * Results are cached for 24h.
122
+ *
123
+ * @param {string} modelId
124
+ * @param {object} opts - { probe: boolean }
125
+ * @returns {Promise<object>}
126
+ */
127
+ export async function probeProviderCapabilities(modelId, { probe = false } = {}) {
128
+ const caps = await resolveProviderCapabilities(modelId);
129
+
130
+ if (probe) {
131
+ // TODO: implement live probing (API call) in Phase A follow-up
132
+ // For now, just return the static capabilities
133
+ }
134
+
135
+ // Cache by adapter key
136
+ const adapterKey = resolveAdapterKey(modelId);
137
+ const cache = getCache();
138
+ cache[adapterKey] = caps;
139
+ writeCapabilityCache(cache);
140
+
141
+ return caps;
142
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * lib/providers/atlassian-confluence/index.mjs — Confluence data-source provider.
3
+ *
4
+ * Capabilities: read, search.
5
+ *
6
+ * Auth: shares JIRA_* env vars with the Jira provider (Atlassian Cloud uses
7
+ * the same email + API token across products) plus optional
8
+ * `CONFLUENCE_BASE_URL` if it differs from `JIRA_BASE_URL`.
9
+ *
10
+ * Config (per call):
11
+ * - pageId: numeric id of the page to read
12
+ * - cql: CQL query string (e.g. 'space = ENG AND text ~ "auth"')
13
+ * - limit: integer (default 25)
14
+ */
15
+
16
+ const DEFAULT_LIMIT = 25;
17
+ const HARD_LIMIT = 100;
18
+
19
+ function authConfig(env) {
20
+ const baseUrl = (env.CONFLUENCE_BASE_URL || env.JIRA_BASE_URL || '').replace(/\/$/, '');
21
+ const email = env.CONFLUENCE_EMAIL || env.JIRA_EMAIL || '';
22
+ const token = env.CONFLUENCE_API_TOKEN || env.JIRA_API_TOKEN || '';
23
+ return { baseUrl, email, token, ok: Boolean(baseUrl && email && token) };
24
+ }
25
+
26
+ function authHeader({ email, token }) {
27
+ if (!email || !token) return {};
28
+ const basic = Buffer.from(`${email}:${token}`).toString('base64');
29
+ return { Authorization: `Basic ${basic}` };
30
+ }
31
+
32
+ async function confluenceFetch(path, env, init = {}) {
33
+ const auth = authConfig(env);
34
+ if (!auth.ok) throw new Error('confluence: base URL + email + token required');
35
+ const res = await fetch(`${auth.baseUrl}${path}`, {
36
+ ...init,
37
+ headers: {
38
+ 'Accept': 'application/json',
39
+ ...authHeader(auth),
40
+ ...(init.headers || {}),
41
+ },
42
+ });
43
+ if (!res.ok) {
44
+ const body = await res.text().catch(() => '');
45
+ throw new Error(`Confluence ${res.status} ${res.statusText}: ${body.slice(0, 200)}`);
46
+ }
47
+ return res.json();
48
+ }
49
+
50
+ export function create({ env = process.env } = {}) {
51
+ return {
52
+ meta: {
53
+ id: 'atlassian-confluence',
54
+ displayName: 'Atlassian Confluence',
55
+ capabilities: ['read', 'search'],
56
+ description: 'Pages, spaces, CQL search.',
57
+ },
58
+
59
+ configSchema: {
60
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
61
+ type: 'object',
62
+ properties: {
63
+ pageId: { type: 'string', pattern: '^[0-9]+$' },
64
+ cql: { type: 'string' },
65
+ limit: { type: 'integer', minimum: 1, maximum: HARD_LIMIT, default: DEFAULT_LIMIT },
66
+ },
67
+ },
68
+
69
+ async health() {
70
+ const auth = authConfig(env);
71
+ if (!auth.ok) {
72
+ return { ok: false, detail: 'CONFLUENCE_BASE_URL/JIRA_BASE_URL + email + token not set' };
73
+ }
74
+ try {
75
+ await confluenceFetch('/wiki/rest/api/space?limit=1', env);
76
+ return { ok: true, detail: `connected to ${auth.baseUrl}` };
77
+ } catch (err) {
78
+ return { ok: false, detail: err.message };
79
+ }
80
+ },
81
+
82
+ async read(config) {
83
+ if (!config?.pageId) throw new Error('confluence.read: config.pageId required');
84
+ return confluenceFetch(
85
+ `/wiki/rest/api/content/${encodeURIComponent(config.pageId)}?expand=body.storage,version`,
86
+ env
87
+ );
88
+ },
89
+
90
+ async search(config) {
91
+ const cql = config?.cql;
92
+ if (!cql) throw new Error('confluence.search: config.cql required');
93
+ const limit = Math.min(config?.limit || DEFAULT_LIMIT, HARD_LIMIT);
94
+ const data = await confluenceFetch(
95
+ `/wiki/rest/api/content/search?cql=${encodeURIComponent(cql)}&limit=${limit}`,
96
+ env
97
+ );
98
+ return Array.isArray(data?.results) ? data.results : [];
99
+ },
100
+ };
101
+ }
102
+
103
+ export default create;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * lib/providers/atlassian-jira/index.mjs — Jira data-source provider.
3
+ *
4
+ * Capabilities: read, search.
5
+ *
6
+ * Auth: `JIRA_BASE_URL`, `JIRA_EMAIL`, `JIRA_API_TOKEN` from env.
7
+ *
8
+ * Config (per call):
9
+ * - issueKey: 'PROJ-123' for read
10
+ * - jql: JQL string for search (e.g. 'project = RLBLT AND assignee = currentUser()')
11
+ * - maxResults: integer (default 50, max 100)
12
+ */
13
+
14
+ const DEFAULT_MAX = 50;
15
+ const HARD_MAX = 100;
16
+
17
+ function authConfig(env) {
18
+ const baseUrl = env.JIRA_BASE_URL?.replace(/\/$/, '') || '';
19
+ const email = env.JIRA_EMAIL || '';
20
+ const token = env.JIRA_API_TOKEN || '';
21
+ return { baseUrl, email, token, ok: Boolean(baseUrl && email && token) };
22
+ }
23
+
24
+ function authHeader({ email, token }) {
25
+ if (!email || !token) return {};
26
+ const basic = Buffer.from(`${email}:${token}`).toString('base64');
27
+ return { Authorization: `Basic ${basic}` };
28
+ }
29
+
30
+ async function jiraFetch(path, env, init = {}) {
31
+ const auth = authConfig(env);
32
+ if (!auth.ok) throw new Error('jira: JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN must be set');
33
+ const url = `${auth.baseUrl}${path}`;
34
+ const res = await fetch(url, {
35
+ ...init,
36
+ headers: {
37
+ 'Accept': 'application/json',
38
+ 'Content-Type': 'application/json',
39
+ ...authHeader(auth),
40
+ ...(init.headers || {}),
41
+ },
42
+ });
43
+ if (!res.ok) {
44
+ const body = await res.text().catch(() => '');
45
+ throw new Error(`Jira ${res.status} ${res.statusText}: ${body.slice(0, 200)}`);
46
+ }
47
+ return res.json();
48
+ }
49
+
50
+ export function create({ env = process.env } = {}) {
51
+ return {
52
+ meta: {
53
+ id: 'atlassian-jira',
54
+ displayName: 'Atlassian Jira',
55
+ capabilities: ['read', 'search'],
56
+ description: 'Issues, sprints, JQL search.',
57
+ },
58
+
59
+ configSchema: {
60
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
61
+ type: 'object',
62
+ properties: {
63
+ issueKey: { type: 'string', pattern: '^[A-Z][A-Z0-9_]+-[0-9]+$' },
64
+ jql: { type: 'string' },
65
+ maxResults: { type: 'integer', minimum: 1, maximum: HARD_MAX, default: DEFAULT_MAX },
66
+ },
67
+ },
68
+
69
+ async health() {
70
+ const auth = authConfig(env);
71
+ if (!auth.ok) {
72
+ return { ok: false, detail: 'JIRA_BASE_URL / JIRA_EMAIL / JIRA_API_TOKEN not set' };
73
+ }
74
+ try {
75
+ await jiraFetch('/rest/api/3/myself', env);
76
+ return { ok: true, detail: `connected to ${auth.baseUrl}` };
77
+ } catch (err) {
78
+ return { ok: false, detail: err.message };
79
+ }
80
+ },
81
+
82
+ async read(config) {
83
+ if (!config?.issueKey) throw new Error('jira.read: config.issueKey required');
84
+ return jiraFetch(`/rest/api/3/issue/${encodeURIComponent(config.issueKey)}`, env);
85
+ },
86
+
87
+ async search(config) {
88
+ const jql = config?.jql;
89
+ if (!jql) throw new Error('jira.search: config.jql required');
90
+ const max = Math.min(config?.maxResults || DEFAULT_MAX, HARD_MAX);
91
+ const data = await jiraFetch('/rest/api/3/search', env, {
92
+ method: 'POST',
93
+ body: JSON.stringify({ jql, maxResults: max, fields: ['summary', 'status', 'assignee', 'priority', 'updated'] }),
94
+ });
95
+ return Array.isArray(data?.issues) ? data.issues : [];
96
+ },
97
+ };
98
+ }
99
+
100
+ export default create;
@@ -0,0 +1,126 @@
1
+ // lib/providers/auth-manager.mjs
2
+ // OAuth refresh and token lifecycle management
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { homedir } from 'node:os';
7
+
8
+ const AUTH_DIR = path.join(homedir(), '.construct', 'auth');
9
+
10
+ try {
11
+ fs.mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
12
+ } catch { /* ignore */ }
13
+
14
+ const PROVIDER_CONFIGS = {
15
+ github: {
16
+ name: 'GitHub',
17
+ tokenExpiry: null,
18
+ refreshable: false,
19
+ envVar: 'GITHUB_TOKEN',
20
+ },
21
+ salesforce: {
22
+ name: 'Salesforce',
23
+ tokenExpiry: 7200,
24
+ refreshable: true,
25
+ refreshBuffer: 300,
26
+ envVar: 'SALESFORCE_ACCESS_TOKEN',
27
+ refreshEnvVar: 'SALESFORCE_REFRESH_TOKEN',
28
+ },
29
+ };
30
+
31
+ export function loadAuthState(provider) {
32
+ try {
33
+ const file = path.join(AUTH_DIR, `${provider}.json`);
34
+ if (fs.existsSync(file)) {
35
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
36
+ }
37
+ } catch { /* ignore */ }
38
+ return null;
39
+ }
40
+
41
+ export function saveAuthState(provider, state) {
42
+ try {
43
+ const file = path.join(AUTH_DIR, `${provider}.json`);
44
+ fs.writeFileSync(file, JSON.stringify(state, null, 2), { mode: 0o600 });
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ export function getTokenStatus(provider) {
52
+ const config = PROVIDER_CONFIGS[provider];
53
+ if (!config) return { valid: false, error: 'Unknown provider' };
54
+
55
+ const state = loadAuthState(provider);
56
+ if (!state) {
57
+ // Fall back to env var
58
+ const envToken = process.env[config.envVar];
59
+ if (!envToken) {
60
+ return { valid: false, error: 'No token configured' };
61
+ }
62
+ return { valid: true, source: 'env', expiresAt: null };
63
+ }
64
+
65
+ if (!state.expiresAt) {
66
+ return { valid: true, source: 'auth-store' };
67
+ }
68
+
69
+ const expiresAt = new Date(state.expiresAt);
70
+ const now = new Date();
71
+ const bufferMs = (config.refreshBuffer || 0) * 1000;
72
+
73
+ if (expiresAt - now < bufferMs) {
74
+ return {
75
+ valid: false,
76
+ error: 'Token expired or expiring soon',
77
+ needsRefresh: config.refreshable,
78
+ };
79
+ }
80
+
81
+ return {
82
+ valid: true,
83
+ source: 'auth-store',
84
+ expiresAt: state.expiresAt,
85
+ expiresIn: Math.floor((expiresAt - now) / 1000),
86
+ };
87
+ }
88
+
89
+ export async function withValidToken(provider, fn) {
90
+ const status = getTokenStatus(provider);
91
+
92
+ if (!status.valid) {
93
+ if (status.needsRefresh) {
94
+ const refreshed = await refreshToken(provider);
95
+ if (!refreshed.success) {
96
+ throw new Error(`Token refresh failed: ${refreshed.error}`);
97
+ }
98
+ } else {
99
+ throw new Error(`Invalid token: ${status.error}`);
100
+ }
101
+ }
102
+
103
+ const state = loadAuthState(provider);
104
+ const token = state?.token || process.env[PROVIDER_CONFIGS[provider].envVar];
105
+
106
+ return await fn(token);
107
+ }
108
+
109
+ async function refreshToken(provider) {
110
+ const config = PROVIDER_CONFIGS[provider];
111
+ if (!config?.refreshable) {
112
+ return { success: false, error: 'Provider does not support refresh' };
113
+ }
114
+
115
+ const state = loadAuthState(provider);
116
+ if (!state?.refreshToken) {
117
+ return { success: false, error: 'No refresh token available' };
118
+ }
119
+
120
+ // Provider-specific refresh logic would go here
121
+ // For now, just indicate refresh is needed
122
+ return {
123
+ success: false,
124
+ error: 'Token refresh required - reauthenticate manually',
125
+ };
126
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * lib/providers/circuit-breaker.mjs — per-provider circuit breaker.
3
+ *
4
+ * Wraps any async function with three-state breaker semantics:
5
+ *
6
+ * CLOSED — calls run normally. Consecutive failures are counted.
7
+ * OPEN — calls fail-fast with a `CircuitOpenError`. After
8
+ * `cooldownMs`, the breaker transitions to HALF_OPEN.
9
+ * HALF_OPEN — the next call is a probe. Success → CLOSED, failure → OPEN.
10
+ *
11
+ * Wraps provider HTTP calls so when a remote system goes down, Construct
12
+ * stops drilling it (and stops blocking user-facing operations on the failure
13
+ * mode) until the cooldown elapses. State is per-key, so one provider going
14
+ * down doesn't trip another's breaker.
15
+ *
16
+ * The breaker classifies errors via `isFailure(err)`. By default any
17
+ * non-null error counts as a failure. Callers can pass a predicate to
18
+ * exclude expected errors (e.g. 4xx auth errors that don't indicate the
19
+ * remote system is unhealthy).
20
+ *
21
+ * Inspection: `getBreaker(key).state`, `.openedAt`, `.failureCount` are
22
+ * read-only views, useful for `construct doctor` to surface "GitHub
23
+ * provider circuit OPEN since 12:34:56".
24
+ */
25
+
26
+ const STATES = Object.freeze({ CLOSED: 'closed', OPEN: 'open', HALF_OPEN: 'half_open' });
27
+
28
+ const REGISTRY = new Map();
29
+
30
+ class CircuitOpenError extends Error {
31
+ constructor(key, openedAt) {
32
+ super(`circuit breaker '${key}' is open (since ${new Date(openedAt).toISOString()})`);
33
+ this.name = 'CircuitOpenError';
34
+ this.key = key;
35
+ this.openedAt = openedAt;
36
+ }
37
+ }
38
+
39
+ class Breaker {
40
+ constructor(key, options = {}) {
41
+ this.key = key;
42
+ this.failureThreshold = options.failureThreshold ?? 5;
43
+ this.cooldownMs = options.cooldownMs ?? 30_000;
44
+ this.isFailure = options.isFailure || ((err) => err != null);
45
+ this.state = STATES.CLOSED;
46
+ this.failureCount = 0;
47
+ this.openedAt = null;
48
+ }
49
+
50
+ reset() {
51
+ this.state = STATES.CLOSED;
52
+ this.failureCount = 0;
53
+ this.openedAt = null;
54
+ }
55
+
56
+ _maybeHalfOpen() {
57
+ if (this.state !== STATES.OPEN) return;
58
+ if (Date.now() - this.openedAt >= this.cooldownMs) {
59
+ this.state = STATES.HALF_OPEN;
60
+ }
61
+ }
62
+
63
+ _recordSuccess() {
64
+ if (this.state === STATES.HALF_OPEN) {
65
+ this.reset();
66
+ return;
67
+ }
68
+ if (this.state === STATES.CLOSED) {
69
+ this.failureCount = 0;
70
+ }
71
+ }
72
+
73
+ _recordFailure() {
74
+ if (this.state === STATES.HALF_OPEN) {
75
+ this.state = STATES.OPEN;
76
+ this.openedAt = Date.now();
77
+ return;
78
+ }
79
+ this.failureCount += 1;
80
+ if (this.failureCount >= this.failureThreshold) {
81
+ this.state = STATES.OPEN;
82
+ this.openedAt = Date.now();
83
+ }
84
+ }
85
+
86
+ async run(fn, ...args) {
87
+ this._maybeHalfOpen();
88
+ if (this.state === STATES.OPEN) {
89
+ throw new CircuitOpenError(this.key, this.openedAt);
90
+ }
91
+ try {
92
+ const result = await fn(...args);
93
+ this._recordSuccess();
94
+ return result;
95
+ } catch (err) {
96
+ if (this.isFailure(err)) this._recordFailure();
97
+ throw err;
98
+ }
99
+ }
100
+ }
101
+
102
+ export function getBreaker(key, options) {
103
+ if (!REGISTRY.has(key)) REGISTRY.set(key, new Breaker(key, options));
104
+ return REGISTRY.get(key);
105
+ }
106
+
107
+ export function describeBreakers() {
108
+ return [...REGISTRY.values()].map((b) => ({
109
+ key: b.key,
110
+ state: b.state,
111
+ failureCount: b.failureCount,
112
+ openedAt: b.openedAt,
113
+ }));
114
+ }
115
+
116
+ export function resetBreaker(key) {
117
+ REGISTRY.get(key)?.reset();
118
+ }
119
+
120
+ export function clearBreakerRegistry() {
121
+ REGISTRY.clear();
122
+ }
123
+
124
+ export { STATES, CircuitOpenError };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * lib/providers/contract.mjs — data-source provider contract.
3
+ *
4
+ * A provider integrates Construct with an external system that holds work
5
+ * artefacts: GitHub repos, Jira projects, Confluence spaces, Slack channels,
6
+ * Salesforce orgs, etc. Providers are stateless adapters — durable state
7
+ * (cached search results, last-seen markers, observations distilled from
8
+ * fetched items) lives in core stores, not in the provider.
9
+ *
10
+ * A provider module exports a factory:
11
+ *
12
+ * export function create(options) {
13
+ * return {
14
+ * meta: { id, displayName, capabilities: [...], description },
15
+ * configSchema: { ... } // JSON-Schema draft 2020-12
16
+ * health: async (config) => ({ ok, detail }),
17
+ * read?: async (config, query) => items[],
18
+ * search?: async (config, query) => items[],
19
+ * watch?: async (config, callback) => unsubscribe,
20
+ * write?: async (config, payload) => result,
21
+ * webhook?: async (config, request) => ack,
22
+ * };
23
+ * }
24
+ *
25
+ * Capabilities are an unordered set drawn from the canonical list in
26
+ * `CAPABILITIES`. A provider declares only what it implements; callers
27
+ * consult `meta.capabilities` and skip optional methods that aren't
28
+ * declared.
29
+ *
30
+ * `assertProviderContract(provider)` runs at registry load time and refuses
31
+ * to accept providers that miss required fields or declare capabilities
32
+ * they don't implement. The check is deliberately loose — providers can
33
+ * extend the shape with extra methods — but it gates the load-bearing
34
+ * fields so a typo in a plugin can't take down the registry.
35
+ */
36
+
37
+ export const CAPABILITIES = Object.freeze([
38
+ 'read',
39
+ 'search',
40
+ 'watch',
41
+ 'write',
42
+ 'webhook',
43
+ ]);
44
+
45
+ const REQUIRED_META = ['id', 'displayName', 'capabilities'];
46
+ const REQUIRED_METHODS = ['health'];
47
+ const CAPABILITY_TO_METHOD = {
48
+ read: 'read',
49
+ search: 'search',
50
+ watch: 'watch',
51
+ write: 'write',
52
+ webhook: 'webhook',
53
+ };
54
+
55
+ export function assertProviderContract(provider) {
56
+ if (!provider || typeof provider !== 'object') {
57
+ throw new Error('provider: must be a non-null object returned from create()');
58
+ }
59
+ if (!provider.meta || typeof provider.meta !== 'object') {
60
+ throw new Error('provider: missing required `meta` object');
61
+ }
62
+ for (const field of REQUIRED_META) {
63
+ if (provider.meta[field] === undefined || provider.meta[field] === null || provider.meta[field] === '') {
64
+ throw new Error(`provider: meta.${field} is required`);
65
+ }
66
+ }
67
+ if (!Array.isArray(provider.meta.capabilities) || provider.meta.capabilities.length === 0) {
68
+ throw new Error(`provider (${provider.meta.id}): meta.capabilities must be a non-empty array`);
69
+ }
70
+ for (const cap of provider.meta.capabilities) {
71
+ if (!CAPABILITIES.includes(cap)) {
72
+ throw new Error(`provider (${provider.meta.id}): unknown capability '${cap}'. Allowed: ${CAPABILITIES.join(', ')}`);
73
+ }
74
+ const method = CAPABILITY_TO_METHOD[cap];
75
+ if (typeof provider[method] !== 'function') {
76
+ throw new Error(`provider (${provider.meta.id}): capability '${cap}' declared but method '${method}' is missing`);
77
+ }
78
+ }
79
+ for (const fn of REQUIRED_METHODS) {
80
+ if (typeof provider[fn] !== 'function') {
81
+ throw new Error(`provider (${provider.meta.id}): missing required method '${fn}'`);
82
+ }
83
+ }
84
+ }
85
+
86
+ export function checkProviderContract(provider) {
87
+ try {
88
+ assertProviderContract(provider);
89
+ return { ok: true, errors: [] };
90
+ } catch (err) {
91
+ return { ok: false, errors: [err.message] };
92
+ }
93
+ }