@geraldmaron/construct 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +9 -6
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1000 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +523 -36
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
@@ -0,0 +1,374 @@
1
+ /**
2
+ * lib/embed/roadmap.mjs — living roadmap document for embed mode.
3
+ *
4
+ * The roadmap is a **running structured document** for a team or project:
5
+ * - Organized by year → quarter → theme/initiative
6
+ * - Items tied to work-tracking refs (Jira issues, Linear cycles, GitHub milestones)
7
+ * - Cross-linked to related docs (ADRs, PRDs, memos)
8
+ * - Updated incrementally as work progresses (not regenerated from scratch)
9
+ *
10
+ * Construct keeps it current by:
11
+ * - Reconciling snapshot items against existing roadmap entries
12
+ * - Advancing status when linked issues close or transition
13
+ * - Adding new items that appear in tracked sources
14
+ * - Archiving completed quarters
15
+ *
16
+ * Storage:
17
+ * docs/roadmap.md in the target docs lane (not hardcoded to .cx/)
18
+ *
19
+ * Format:
20
+ * # Roadmap
21
+ * > Last updated: <ISO timestamp>
22
+ * > Sources: github, jira, linear
23
+ *
24
+ * ## 2026
25
+ * ### Q2 (Apr–Jun)
26
+ * #### Theme: Embed Mode Redesign
27
+ * - [x] Target resolver — PROJ-123 · ADR-005
28
+ * - [ ] Docs lifecycle — PROJ-124
29
+ * - [ ] Dashboard notifications — PROJ-125 · PRD-012
30
+ *
31
+ * ### Q1 (Jan–Mar) ✓
32
+ * #### Theme: Core Platform
33
+ * - [x] MCP server — PROJ-100
34
+ * - [x] Provider registry — PROJ-101
35
+ */
36
+
37
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
38
+ import { join } from 'node:path';
39
+ import { searchObservations } from '../observation-store.mjs';
40
+ import { renderRoleLensSection } from './role-framing.mjs';
41
+
42
+ // ─── Quarter/Year utilities ──────────────────────────────────────────────────
43
+
44
+ function currentQuarter() {
45
+ const now = new Date();
46
+ const q = Math.ceil((now.getMonth() + 1) / 3);
47
+ return { year: now.getFullYear(), quarter: q };
48
+ }
49
+
50
+ function quarterLabel(q) {
51
+ const months = ['Jan–Mar', 'Apr–Jun', 'Jul–Sep', 'Oct–Dec'];
52
+ return `Q${q.quarter} (${months[q.quarter - 1]})`;
53
+ }
54
+
55
+ function quarterKey(q) {
56
+ return `${q.year}-Q${q.quarter}`;
57
+ }
58
+
59
+ // ─── Roadmap data model ──────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * A roadmap entry represents one trackable item.
63
+ * @typedef {object} RoadmapEntry
64
+ * @property {string} title
65
+ * @property {string} status - 'planned' | 'in-progress' | 'done' | 'cancelled'
66
+ * @property {string[]} refs - work-tracking refs (e.g. 'PROJ-123', '#45', 'ENG-100')
67
+ * @property {string[]} docs - linked docs (e.g. 'ADR-005', 'PRD-012')
68
+ * @property {string|null} theme - grouping theme/initiative
69
+ * @property {string} quarter - e.g. '2026-Q2'
70
+ */
71
+
72
+ /**
73
+ * Parse an existing roadmap.md into a structured model.
74
+ * Returns { meta, entries[] } or null if file doesn't exist.
75
+ */
76
+ export function parseRoadmap(filePath) {
77
+ if (!existsSync(filePath)) return null;
78
+
79
+ const content = readFileSync(filePath, 'utf8');
80
+ const lines = content.split('\n');
81
+ const entries = [];
82
+ let currentYear = null;
83
+ let currentQtr = null;
84
+ let currentTheme = null;
85
+
86
+ for (const line of lines) {
87
+ // Year header: ## 2026
88
+ const yearMatch = line.match(/^## (\d{4})/);
89
+ if (yearMatch) { currentYear = parseInt(yearMatch[1]); continue; }
90
+
91
+ // Quarter header: ### Q2 (Apr–Jun)
92
+ const qtrMatch = line.match(/^### Q(\d)/);
93
+ if (qtrMatch && currentYear) {
94
+ currentQtr = `${currentYear}-Q${qtrMatch[1]}`;
95
+ currentTheme = null;
96
+ continue;
97
+ }
98
+
99
+ // Theme header: #### Theme: ...
100
+ const themeMatch = line.match(/^#### (?:Theme: )?(.+)/);
101
+ if (themeMatch) { currentTheme = themeMatch[1].trim(); continue; }
102
+
103
+ // Entry: - [x] or - [ ] title — refs · docs
104
+ const entryMatch = line.match(/^- \[([ x])\] (.+)/);
105
+ if (entryMatch && currentQtr) {
106
+ const done = entryMatch[1] === 'x';
107
+ const rest = entryMatch[2];
108
+
109
+ // Extract refs (PROJ-123, #45, ENG-100 patterns) and docs (ADR-005, PRD-012)
110
+ const refs = [];
111
+ const docs = [];
112
+ const refPattern = /([A-Z]+-\d+|#\d+)/g;
113
+ const docPattern = /(ADR-\d+|PRD-\d+|RFC-\d+|MEMO-\d+)/g;
114
+ let m;
115
+ while ((m = refPattern.exec(rest)) !== null) {
116
+ if (docPattern.test(m[1])) docs.push(m[1]);
117
+ else refs.push(m[1]);
118
+ }
119
+ // Re-run doc pattern on full string
120
+ const docMatches = rest.match(/(ADR-\d+|PRD-\d+|RFC-\d+|MEMO-\d+)/g) ?? [];
121
+ docs.push(...docMatches.filter((d) => !docs.includes(d)));
122
+
123
+ // Title is everything before the first ref separator
124
+ const title = rest.split(/\s+[—·]\s+/)[0].trim();
125
+
126
+ entries.push({
127
+ title,
128
+ status: done ? 'done' : 'planned',
129
+ refs,
130
+ docs,
131
+ theme: currentTheme,
132
+ quarter: currentQtr,
133
+ });
134
+ }
135
+ }
136
+
137
+ return { entries };
138
+ }
139
+
140
+ /**
141
+ * Reconcile snapshot items against existing roadmap entries.
142
+ * - Items in snapshot that match a roadmap ref: update status
143
+ * - Items in snapshot with no roadmap entry: add to current quarter
144
+ * - Roadmap entries whose refs are now closed: mark done
145
+ *
146
+ * @param {RoadmapEntry[]} existing - Parsed roadmap entries
147
+ * @param {object[]} snapshotItems - Open items from snapshot sections
148
+ * @param {object} [opts]
149
+ * @returns {RoadmapEntry[]} Updated entry list
150
+ */
151
+ export function reconcileEntries(existing, snapshotItems, opts = {}) {
152
+ const entries = [...existing];
153
+ const { year, quarter } = currentQuarter();
154
+ const currentQtrKey = quarterKey({ year, quarter });
155
+
156
+ // Index existing entries by ref for fast lookup
157
+ const byRef = new Map();
158
+ for (const entry of entries) {
159
+ for (const ref of entry.refs) byRef.set(ref, entry);
160
+ }
161
+
162
+ // Track which snapshot items matched
163
+ const matched = new Set();
164
+
165
+ for (const item of snapshotItems) {
166
+ const key = item.key ?? (item.number ? `#${item.number}` : null);
167
+ if (!key) continue;
168
+
169
+ if (byRef.has(key)) {
170
+ // Update status
171
+ const entry = byRef.get(key);
172
+ const itemStatus = normalizeStatus(item.status);
173
+ if (itemStatus === 'done' && entry.status !== 'done') {
174
+ entry.status = 'done';
175
+ } else if (itemStatus === 'in-progress' && entry.status === 'planned') {
176
+ entry.status = 'in-progress';
177
+ }
178
+ matched.add(key);
179
+ } else {
180
+ // New item — add to current quarter
181
+ const title = item.summary ?? item.title ?? item.subject ?? '(untitled)';
182
+ entries.push({
183
+ title,
184
+ status: normalizeStatus(item.status),
185
+ refs: key ? [key] : [],
186
+ docs: [],
187
+ theme: null,
188
+ quarter: currentQtrKey,
189
+ });
190
+ matched.add(key);
191
+ }
192
+ }
193
+
194
+ return entries;
195
+ }
196
+
197
+ function normalizeStatus(raw) {
198
+ const s = String(raw ?? '').toLowerCase();
199
+ if (/done|closed|merged|resolved|completed/.test(s)) return 'done';
200
+ if (/in.?progress|active|doing|started/.test(s)) return 'in-progress';
201
+ if (/cancelled|won.?t|wont/.test(s)) return 'cancelled';
202
+ return 'planned';
203
+ }
204
+
205
+ /**
206
+ * Render entries back to roadmap markdown.
207
+ */
208
+ export function renderRoadmap(entries, opts = {}) {
209
+ const { roles, sources } = opts;
210
+ const updatedAt = new Date().toISOString();
211
+
212
+ const lines = [
213
+ '# Roadmap',
214
+ `> Last updated: ${updatedAt}`,
215
+ ];
216
+ if (sources?.length) {
217
+ lines.push(`> Sources: ${sources.join(', ')}`);
218
+ }
219
+ lines.push('');
220
+
221
+ // Role lens
222
+ if (roles) {
223
+ const roleLens = renderRoleLensSection(roles);
224
+ if (roleLens) lines.push(roleLens);
225
+ }
226
+
227
+ // Group by year → quarter → theme
228
+ const byYear = new Map();
229
+ for (const entry of entries) {
230
+ const [yearStr, qtr] = (entry.quarter ?? '').split('-');
231
+ const year = parseInt(yearStr) || currentQuarter().year;
232
+ if (!byYear.has(year)) byYear.set(year, new Map());
233
+ const quarters = byYear.get(year);
234
+ if (!quarters.has(entry.quarter)) quarters.set(entry.quarter, []);
235
+ quarters.get(entry.quarter).push(entry);
236
+ }
237
+
238
+ // Sort years descending (current year first)
239
+ const sortedYears = [...byYear.keys()].sort((a, b) => b - a);
240
+
241
+ for (const year of sortedYears) {
242
+ lines.push(`## ${year}`, '');
243
+ const quarters = byYear.get(year);
244
+ // Sort quarters descending within year
245
+ const sortedQtrs = [...quarters.keys()].sort().reverse();
246
+
247
+ for (const qtrKey of sortedQtrs) {
248
+ const qtrEntries = quarters.get(qtrKey);
249
+ const allDone = qtrEntries.every((e) => e.status === 'done' || e.status === 'cancelled');
250
+ const qNum = parseInt(qtrKey.split('Q')[1]) || 1;
251
+ const label = quarterLabel({ year, quarter: qNum });
252
+ lines.push(`### ${label}${allDone ? ' ✓' : ''}`, '');
253
+
254
+ // Group by theme within quarter
255
+ const byTheme = new Map();
256
+ for (const entry of qtrEntries) {
257
+ const theme = entry.theme ?? '_ungrouped';
258
+ if (!byTheme.has(theme)) byTheme.set(theme, []);
259
+ byTheme.get(theme).push(entry);
260
+ }
261
+
262
+ for (const [theme, items] of byTheme) {
263
+ if (theme !== '_ungrouped') {
264
+ lines.push(`#### ${theme}`, '');
265
+ }
266
+ for (const item of items) {
267
+ const check = item.status === 'done' ? 'x' : ' ';
268
+ const refStr = item.refs.length ? ` — ${item.refs.join(', ')}` : '';
269
+ const docStr = item.docs.length ? ` · ${item.docs.join(', ')}` : '';
270
+ const statusTag = item.status === 'in-progress' ? ' 🔄' : item.status === 'cancelled' ? ' ~~cancelled~~' : '';
271
+ lines.push(`- [${check}] ${item.title}${statusTag}${refStr}${docStr}`);
272
+ }
273
+ lines.push('');
274
+ }
275
+ }
276
+ }
277
+
278
+ if (!entries.length) {
279
+ lines.push('_No roadmap items tracked yet._', '');
280
+ }
281
+
282
+ return lines.join('\n');
283
+ }
284
+
285
+ /**
286
+ * Generate or update the roadmap for a target.
287
+ *
288
+ * If a roadmap already exists, parses it and reconciles with snapshot data.
289
+ * If not, creates a new one from snapshot items.
290
+ *
291
+ * @param {object} opts
292
+ * @param {string} opts.targetPath - Target root path (docs/roadmap.md will be created here)
293
+ * @param {object} opts.snapshot - Last snapshot from embed
294
+ * @param {object} [opts.roles] - Configured roles for role lens
295
+ * @returns {{ path: string, updatedAt: string, itemCount: number, isNew: boolean }}
296
+ */
297
+ export async function generateRoadmap({ targetPath, snapshot, roles }) {
298
+ const updatedAt = new Date().toISOString();
299
+
300
+ if (!snapshot) {
301
+ return { path: null, updatedAt, itemCount: 0, skipped: true, reason: 'no snapshot available' };
302
+ }
303
+
304
+ const roadmapPath = join(targetPath, 'docs', 'roadmap.md');
305
+
306
+ // Parse existing or start fresh
307
+ const existing = parseRoadmap(roadmapPath);
308
+ const isNew = !existing;
309
+
310
+ // Extract open items from snapshot
311
+ const snapshotItems = [];
312
+ for (const section of snapshot.sections ?? []) {
313
+ for (const item of section.items ?? []) {
314
+ snapshotItems.push({ ...item, _provider: section.provider });
315
+ }
316
+ }
317
+
318
+ // Reconcile
319
+ const entries = reconcileEntries(existing?.entries ?? [], snapshotItems);
320
+
321
+ // Determine sources
322
+ const sources = [...new Set((snapshot.sections ?? []).map((s) => s.provider))];
323
+
324
+ // Render
325
+ const markdown = renderRoadmap(entries, { roles, sources });
326
+
327
+ // Write
328
+ mkdirSync(join(targetPath, 'docs'), { recursive: true });
329
+ writeFileSync(roadmapPath, markdown, 'utf8');
330
+
331
+ return { path: roadmapPath, updatedAt, itemCount: entries.length, isNew };
332
+ }
333
+
334
+ /**
335
+ * Produce a short Slack-friendly summary of the roadmap (≤3000 chars).
336
+ */
337
+ export async function roadmapSlackSummary({ targetPath, snapshot, roles }) {
338
+ const result = await generateRoadmap({ targetPath, snapshot, roles });
339
+ if (result.skipped) return null;
340
+
341
+ const { year, quarter } = currentQuarter();
342
+ const qtrKey = quarterKey({ year, quarter });
343
+
344
+ const roadmapPath = join(targetPath, 'docs', 'roadmap.md');
345
+ const parsed = parseRoadmap(roadmapPath);
346
+ const currentItems = (parsed?.entries ?? []).filter((e) => e.quarter === qtrKey);
347
+
348
+ const done = currentItems.filter((e) => e.status === 'done').length;
349
+ const total = currentItems.length;
350
+
351
+ const lines = [
352
+ `*Roadmap* — ${quarterLabel({ year, quarter })} ${year} — ${done}/${total} done`,
353
+ ];
354
+
355
+ const inProgress = currentItems.filter((e) => e.status === 'in-progress').slice(0, 5);
356
+ if (inProgress.length) {
357
+ lines.push('\n*In progress:*');
358
+ inProgress.forEach((item, i) => {
359
+ const refs = item.refs.length ? ` (${item.refs.join(', ')})` : '';
360
+ lines.push(`${i + 1}. ${item.title}${refs}`);
361
+ });
362
+ }
363
+
364
+ const planned = currentItems.filter((e) => e.status === 'planned').slice(0, 5);
365
+ if (planned.length) {
366
+ lines.push('\n*Up next:*');
367
+ planned.forEach((item, i) => {
368
+ lines.push(`${i + 1}. ${item.title}`);
369
+ });
370
+ }
371
+
372
+ lines.push(`\nFull roadmap: \`docs/roadmap.md\``);
373
+ return lines.join('\n').slice(0, 3000);
374
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * lib/embed/role-framing.mjs — load and apply role orientations for embed mode.
3
+ *
4
+ * When embed has configured roles (primary/secondary), this module:
5
+ * 1. Loads the embedOrientation from the agent registry for each role
6
+ * 2. Provides a combined lens (focusAreas, riskSignals, artifactBias)
7
+ * 3. Renders a "Role Lens" section for inclusion in snapshots/roadmaps
8
+ */
9
+
10
+ import { readFileSync } from 'node:fs';
11
+ import { join, dirname } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const REGISTRY_PATH = join(__dirname, '..', '..', 'agents', 'registry.json');
16
+
17
+ let _registryCache = null;
18
+
19
+ function loadRegistry() {
20
+ if (_registryCache) return _registryCache;
21
+ try {
22
+ _registryCache = JSON.parse(readFileSync(REGISTRY_PATH, 'utf8'));
23
+ } catch (err) {
24
+ process.stderr.write('[role-framing.mjs] loadRegistry: ' + (err?.message ?? String(err)) + '\n');
25
+ _registryCache = { agents: [] };
26
+ }
27
+ return _registryCache;
28
+ }
29
+
30
+ /**
31
+ * Get the embedOrientation for a named agent role.
32
+ * @param {string} roleName - e.g. 'product-manager', 'architect'
33
+ * @returns {object|null} The embedOrientation object or null
34
+ */
35
+ export function getOrientation(roleName) {
36
+ if (!roleName) return null;
37
+ const registry = loadRegistry();
38
+ const agent = (registry.agents ?? []).find((a) => a.name === roleName);
39
+ return agent?.embedOrientation ?? null;
40
+ }
41
+
42
+ /**
43
+ * Build a combined role lens from primary and secondary roles.
44
+ * Primary focusAreas/riskSignals come first; secondary ones are appended (deduplicated).
45
+ *
46
+ * @param {object} roles - { primary: string|null, secondary: string|null }
47
+ * @returns {object} { focusAreas: string[], riskSignals: string[], artifactBias: string[], roles: { primary, secondary } }
48
+ */
49
+ export function buildRoleLens(roles) {
50
+ const primary = getOrientation(roles?.primary);
51
+ const secondary = getOrientation(roles?.secondary);
52
+
53
+ if (!primary && !secondary) return null;
54
+
55
+ const focusAreas = [...(primary?.focusAreas ?? [])];
56
+ const riskSignals = [...(primary?.riskSignals ?? [])];
57
+ const artifactBias = [...(primary?.artifactBias ?? [])];
58
+
59
+ // Append secondary, deduplicated
60
+ for (const area of secondary?.focusAreas ?? []) {
61
+ if (!focusAreas.includes(area)) focusAreas.push(area);
62
+ }
63
+ for (const sig of secondary?.riskSignals ?? []) {
64
+ if (!riskSignals.includes(sig)) riskSignals.push(sig);
65
+ }
66
+ for (const bias of secondary?.artifactBias ?? []) {
67
+ if (!artifactBias.includes(bias)) artifactBias.push(bias);
68
+ }
69
+
70
+ return {
71
+ focusAreas,
72
+ riskSignals,
73
+ artifactBias,
74
+ roles: { primary: roles.primary, secondary: roles.secondary },
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Render a Role Lens markdown section for inclusion in snapshot/roadmap output.
80
+ * Returns empty string if no roles configured.
81
+ *
82
+ * @param {object} roles - { primary, secondary } from embed config
83
+ * @returns {string}
84
+ */
85
+ export function renderRoleLensSection(roles) {
86
+ const lens = buildRoleLens(roles);
87
+ if (!lens) return '';
88
+
89
+ const lines = ['## Role Lens', ''];
90
+
91
+ if (lens.roles.primary) lines.push(`- **Primary**: ${lens.roles.primary}`);
92
+ if (lens.roles.secondary) lines.push(`- **Secondary**: ${lens.roles.secondary}`);
93
+ lines.push('');
94
+
95
+ if (lens.focusAreas.length) {
96
+ lines.push(`**Focus**: ${lens.focusAreas.join(', ')}`);
97
+ }
98
+ if (lens.riskSignals.length) {
99
+ lines.push(`**Risk signals**: ${lens.riskSignals.join(', ')}`);
100
+ }
101
+ if (lens.artifactBias.length) {
102
+ lines.push(`**Artifact bias**: ${lens.artifactBias.join(', ')}`);
103
+ }
104
+ lines.push('');
105
+
106
+ return lines.join('\n');
107
+ }
108
+
109
+ /** Reset internal cache (for testing). */
110
+ export function _resetCache() { _registryCache = null; }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * lib/embed/scheduler.mjs — interval-based task scheduler for embed mode.
3
+ *
4
+ * Lightweight alternative to a full cron library — runs tasks at fixed
5
+ * intervals, tracks last-run times, and supports graceful shutdown.
6
+ * Zero external deps.
7
+ */
8
+
9
+ export class Scheduler {
10
+ #tasks = new Map(); // id → { fn, intervalMs, lastRun, timer, label }
11
+ #running = false;
12
+
13
+ /**
14
+ * Register a task. Returns the task id.
15
+ * @param {string} label - Human-readable name for logging
16
+ * @param {number} intervalMs - How often to run (ms)
17
+ * @param {Function} fn - Async function to call; receives { label, lastRun }
18
+ * @param {object} [opts]
19
+ * @param {boolean} [opts.runImmediately=false] - Run once before first interval fires
20
+ * @param {boolean} [opts.unref=false] - Call timer.unref() so this task does not keep the event loop alive (use in tests)
21
+ */
22
+ register(label, intervalMs, fn, { runImmediately = false, unref = false } = {}) {
23
+ const id = `${label}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
24
+ const task = { label, intervalMs, fn, lastRun: null, timer: null, runImmediately, unref };
25
+ this.#tasks.set(id, task);
26
+
27
+ if (this.#running) {
28
+ this.#startTask(id, task, runImmediately);
29
+ }
30
+
31
+ return id;
32
+ }
33
+
34
+ /**
35
+ * Start all registered tasks.
36
+ */
37
+ start() {
38
+ if (this.#running) return;
39
+ this.#running = true;
40
+ for (const [id, task] of this.#tasks) {
41
+ this.#startTask(id, task, task.runImmediately);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Stop all tasks gracefully.
47
+ */
48
+ stop() {
49
+ this.#running = false;
50
+ for (const task of this.#tasks.values()) {
51
+ if (task.timer) {
52
+ clearInterval(task.timer);
53
+ task.timer = null;
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Remove a task by id.
60
+ */
61
+ unregister(id) {
62
+ const task = this.#tasks.get(id);
63
+ if (task?.timer) clearInterval(task.timer);
64
+ this.#tasks.delete(id);
65
+ }
66
+
67
+ /**
68
+ * List registered tasks with their status.
69
+ */
70
+ status() {
71
+ return [...this.#tasks.entries()].map(([id, t]) => ({
72
+ id,
73
+ label: t.label,
74
+ intervalMs: t.intervalMs,
75
+ lastRun: t.lastRun,
76
+ active: Boolean(t.timer),
77
+ }));
78
+ }
79
+
80
+ #startTask(id, task, runImmediately) {
81
+ if (runImmediately) {
82
+ this.#runTask(task).catch((err) => {
83
+ process.stderr.write(`[scheduler] ${task.label} error: ${err.message}\n`);
84
+ });
85
+ }
86
+ task.timer = setInterval(() => {
87
+ this.#runTask(task).catch((err) => {
88
+ process.stderr.write(`[scheduler] ${task.label} error: ${err.message}\n`);
89
+ });
90
+ }, task.intervalMs);
91
+ // Tasks keep the event loop alive by default. Tests opt in to unref().
92
+ if (task.unref && task.timer.unref) task.timer.unref();
93
+ }
94
+
95
+ async #runTask(task) {
96
+ task.lastRun = new Date();
97
+ await task.fn({ label: task.label, lastRun: task.lastRun });
98
+ }
99
+ }