@geraldmaron/construct 1.4.2 → 1.5.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 (519) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1027 -64
  4. package/examples/distribution/sources/deck-one-pager.md +1 -1
  5. package/lib/acp/server.mjs +21 -7
  6. package/lib/adapters-sync.mjs +2 -1
  7. package/lib/audit-trail.mjs +185 -2
  8. package/lib/beads/auto-close.mjs +2 -1
  9. package/lib/beads/drift.mjs +2 -1
  10. package/lib/bridges/copilot-proxy.mjs +20 -5
  11. package/lib/certification/skill-inventory.mjs +10 -1
  12. package/lib/cli/approvals.mjs +147 -0
  13. package/lib/cli-commands.mjs +105 -14
  14. package/lib/comment-lint.mjs +127 -8
  15. package/lib/config/schema.mjs +6 -29
  16. package/lib/config/source-target-registry.mjs +70 -0
  17. package/lib/config/source-targets.mjs +179 -205
  18. package/lib/contracts/coverage.mjs +77 -0
  19. package/lib/contracts/validate.mjs +102 -13
  20. package/lib/contracts/violation-log.mjs +50 -4
  21. package/lib/db/migrate.mjs +69 -0
  22. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  23. package/lib/db/migrations/002_queue_provider.sql +50 -0
  24. package/lib/db/migrations/003_worker_registry.sql +17 -0
  25. package/lib/db/migrations/004_trace_events.sql +16 -0
  26. package/lib/db/migrations/005_shared_memory.sql +15 -0
  27. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  28. package/lib/decisions/enforced-baseline.json +0 -2
  29. package/lib/decisions/registry.mjs +3 -2
  30. package/lib/deployment/parity-contract.mjs +1 -1
  31. package/lib/deployment-mode.mjs +78 -2
  32. package/lib/diagram-export.mjs +10 -1
  33. package/lib/distill.mjs +5 -2
  34. package/lib/docs-verify.mjs +2 -1
  35. package/lib/doctor/cli.mjs +1 -0
  36. package/lib/doctor/command-on-path.mjs +24 -0
  37. package/lib/doctor/diagnosis.mjs +89 -0
  38. package/lib/doctor/embedding-health.mjs +50 -0
  39. package/lib/doctor/engine-health.mjs +93 -0
  40. package/lib/doctor/graph-validate.mjs +47 -0
  41. package/lib/doctor/index.mjs +18 -4
  42. package/lib/doctor/sidecar-providers.mjs +56 -0
  43. package/lib/doctor/watchers/consistency.mjs +93 -1
  44. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  45. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  46. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  47. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  48. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  49. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  50. package/lib/document-export.mjs +52 -27
  51. package/lib/document-extract/docling-client.mjs +9 -5
  52. package/lib/document-extract/docling-sidecar.py +30 -0
  53. package/lib/document-extract.mjs +1 -1
  54. package/lib/document-ingest.mjs +9 -0
  55. package/lib/embed/approval-queue.mjs +184 -88
  56. package/lib/embed/authority-guard.mjs +65 -2
  57. package/lib/embed/capability-jobs.mjs +365 -0
  58. package/lib/embed/capability-lifecycle.mjs +219 -0
  59. package/lib/embed/capability-loader.mjs +303 -0
  60. package/lib/embed/capability-runtime.mjs +58 -0
  61. package/lib/embed/cli.mjs +185 -8
  62. package/lib/embed/config.mjs +4 -0
  63. package/lib/embed/daemon.mjs +125 -6
  64. package/lib/embed/demand-fetch.mjs +190 -54
  65. package/lib/embed/inbox.mjs +12 -10
  66. package/lib/embed/presets/ops-triage.mjs +236 -0
  67. package/lib/embed/presets/pm-feedback.mjs +341 -0
  68. package/lib/embed/presets/tpm.mjs +401 -0
  69. package/lib/embed/providers/jira.mjs +72 -1
  70. package/lib/embed/providers/registry.mjs +140 -33
  71. package/lib/embedded-contract/capability.mjs +4 -0
  72. package/lib/embedded-contract/execution.mjs +1 -1
  73. package/lib/embedded-contract/model-resolve.mjs +53 -3
  74. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  75. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  76. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  77. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  78. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  79. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  80. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  81. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  82. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  83. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  84. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  85. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  86. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  88. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  90. package/lib/env-config.mjs +50 -9
  91. package/lib/extensions/index.mjs +27 -0
  92. package/lib/extensions/loader.mjs +120 -0
  93. package/lib/extensions/manifest-schema.mjs +141 -0
  94. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  95. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  96. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  97. package/lib/extensions/manifests/directory.manifest.json +12 -0
  98. package/lib/extensions/manifests/docling.manifest.json +37 -0
  99. package/lib/extensions/manifests/echo.manifest.json +8 -0
  100. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  101. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  102. package/lib/extensions/manifests/github.manifest.json +52 -0
  103. package/lib/extensions/manifests/linear.manifest.json +50 -0
  104. package/lib/extensions/manifests/local.manifest.json +12 -0
  105. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  106. package/lib/extensions/manifests/openai.manifest.json +12 -0
  107. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  108. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  109. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  110. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  113. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  114. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  115. package/lib/extensions/manifests/slack.manifest.json +58 -0
  116. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  117. package/lib/extensions/validate.mjs +175 -0
  118. package/lib/features.mjs +13 -9
  119. package/lib/flows/checkpoint.mjs +184 -0
  120. package/lib/flows/constants.mjs +27 -0
  121. package/lib/flows/define.mjs +146 -0
  122. package/lib/flows/engine.mjs +249 -0
  123. package/lib/flows/errors.mjs +19 -0
  124. package/lib/flows/index.mjs +27 -0
  125. package/lib/flows/joins.mjs +18 -0
  126. package/lib/flows/schema.mjs +90 -0
  127. package/lib/flows/state.mjs +31 -0
  128. package/lib/frameworks/loader.mjs +99 -0
  129. package/lib/frameworks/schema.mjs +157 -0
  130. package/lib/graph/build-from-corpus.mjs +67 -0
  131. package/lib/graph/build-from-embed.mjs +82 -0
  132. package/lib/graph/build-from-registry.mjs +164 -3
  133. package/lib/graph/cli.mjs +456 -12
  134. package/lib/graph/gap-queries.mjs +156 -0
  135. package/lib/graph/gaps.mjs +41 -0
  136. package/lib/graph/impacted.mjs +129 -0
  137. package/lib/graph/runtime-evidence.mjs +177 -0
  138. package/lib/graph/security-coverage.mjs +113 -0
  139. package/lib/graph/staleness.mjs +241 -10
  140. package/lib/graph/store.mjs +24 -7
  141. package/lib/graph/validate.mjs +161 -0
  142. package/lib/headhunt.mjs +9 -8
  143. package/lib/health-check.mjs +15 -7
  144. package/lib/hook-health.mjs +1 -1
  145. package/lib/hooks/agent-tracker.mjs +10 -4
  146. package/lib/hooks/edit-guard.mjs +3 -3
  147. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  148. package/lib/hooks/guard-bash.mjs +41 -15
  149. package/lib/hooks/mcp-health-check.mjs +11 -4
  150. package/lib/hooks/model-fallback.mjs +50 -6
  151. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  152. package/lib/hooks/pre-compact.mjs +181 -173
  153. package/lib/hooks/rule-verifier.mjs +2 -1
  154. package/lib/hooks/session-reflect.mjs +2 -1
  155. package/lib/hooks/session-start.mjs +3 -3
  156. package/lib/host-capabilities.mjs +13 -2
  157. package/lib/host-disposition.mjs +19 -7
  158. package/lib/identity.mjs +92 -0
  159. package/lib/ingest/degraded-extract.mjs +96 -0
  160. package/lib/ingest/docling-remote.mjs +2 -1
  161. package/lib/ingest/provider-extract.mjs +7 -19
  162. package/lib/ingest/sidecar-providers.mjs +196 -0
  163. package/lib/ingest-tooling.mjs +11 -5
  164. package/lib/init-unified.mjs +57 -45
  165. package/lib/init-update.mjs +2 -1
  166. package/lib/install/legacy-global-cleanup.mjs +25 -0
  167. package/lib/intake/daemon.mjs +99 -8
  168. package/lib/intake/git-queue.mjs +110 -38
  169. package/lib/intake/queue-registry.mjs +50 -0
  170. package/lib/intake/queue.mjs +133 -17
  171. package/lib/intake/session-prelude.mjs +57 -8
  172. package/lib/integrations/intake-integrations.mjs +61 -111
  173. package/lib/intent-classifier.mjs +43 -5
  174. package/lib/libreoffice-export.mjs +8 -8
  175. package/lib/logging/rotate.mjs +5 -4
  176. package/lib/mcp/broker.mjs +332 -17
  177. package/lib/mcp/denied-store.mjs +95 -0
  178. package/lib/mcp/destructive-gate.mjs +30 -0
  179. package/lib/mcp/dispatch-envelope.mjs +199 -0
  180. package/lib/mcp/memory-bridge.mjs +2 -1
  181. package/lib/mcp/server.mjs +154 -1411
  182. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  183. package/lib/mcp/tool-definitions-project.mjs +271 -0
  184. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  185. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  186. package/lib/mcp/tool-definitions.mjs +25 -0
  187. package/lib/mcp/tool-registry.mjs +107 -0
  188. package/lib/mcp/tool-safety.mjs +1 -0
  189. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  190. package/lib/mcp/tools/orchestration-run.mjs +165 -25
  191. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  192. package/lib/mcp/tools/provider-write.mjs +187 -0
  193. package/lib/mcp/tools/scope.mjs +0 -3
  194. package/lib/mcp/tools/skills.mjs +1 -1
  195. package/lib/mcp/tools/storage.mjs +0 -8
  196. package/lib/mcp/transport/auth.mjs +238 -0
  197. package/lib/mcp/transport/http.mjs +136 -0
  198. package/lib/mcp/transport/mode.mjs +31 -0
  199. package/lib/mcp/transport/stdio.mjs +22 -0
  200. package/lib/mcp-catalog.json +4 -4
  201. package/lib/mcp-manager.mjs +34 -23
  202. package/lib/mcp-platform-config.mjs +31 -7
  203. package/lib/mode-capabilities.mjs +109 -0
  204. package/lib/model-router.mjs +42 -9
  205. package/lib/models/catalog.mjs +40 -1
  206. package/lib/net-guard.mjs +247 -0
  207. package/lib/observation-store.mjs +6 -2
  208. package/lib/ollama/provision-context.mjs +2 -1
  209. package/lib/opencode-config.mjs +22 -3
  210. package/lib/opencode-runtime-plugin.mjs +120 -2
  211. package/lib/oracle/actions.mjs +204 -10
  212. package/lib/oracle/cli.mjs +24 -3
  213. package/lib/oracle/dispatch.mjs +2 -1
  214. package/lib/oracle/execute.mjs +2 -2
  215. package/lib/oracle/gaps.mjs +3 -3
  216. package/lib/oracle/heartbeat.mjs +53 -0
  217. package/lib/oracle/read-model.mjs +69 -13
  218. package/lib/oracle/reconcile.mjs +3 -46
  219. package/lib/oracle/routing.mjs +37 -31
  220. package/lib/oracle/synthesize.mjs +1 -1
  221. package/lib/orchestration/classification.mjs +434 -0
  222. package/lib/orchestration/delegation-flow.mjs +132 -0
  223. package/lib/orchestration/flow-selection.mjs +418 -0
  224. package/lib/orchestration/gates.mjs +244 -0
  225. package/lib/orchestration/host-sampling.mjs +121 -0
  226. package/lib/orchestration/policy-constants.mjs +48 -0
  227. package/lib/orchestration/provider-outcome.mjs +197 -0
  228. package/lib/orchestration/readiness.mjs +114 -13
  229. package/lib/orchestration/run-store-postgres.mjs +32 -26
  230. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  231. package/lib/orchestration/run-store.mjs +25 -12
  232. package/lib/orchestration/runtime.mjs +446 -39
  233. package/lib/orchestration/store.mjs +87 -12
  234. package/lib/orchestration/trace-store.mjs +125 -0
  235. package/lib/orchestration/web-capability.mjs +3 -2
  236. package/lib/orchestration/worker-runtime.mjs +162 -0
  237. package/lib/orchestration/worker.mjs +528 -89
  238. package/lib/orchestration-policy.mjs +67 -1111
  239. package/lib/packs/cli.mjs +144 -0
  240. package/lib/packs/core-pack.mjs +112 -0
  241. package/lib/packs/enablement.mjs +187 -0
  242. package/lib/packs/index.mjs +23 -0
  243. package/lib/packs/loader.mjs +176 -0
  244. package/lib/packs/manifest-schema.mjs +58 -0
  245. package/lib/packs/prompts.mjs +120 -0
  246. package/lib/packs/validate.mjs +208 -0
  247. package/lib/plugin-registry.mjs +4 -5
  248. package/lib/policy/audit-gate.mjs +42 -0
  249. package/lib/policy/consumption-budget.mjs +149 -0
  250. package/lib/policy/engine.mjs +81 -6
  251. package/lib/policy/role-authority.mjs +89 -0
  252. package/lib/prompt-composer.js +19 -3
  253. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  254. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  255. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  256. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  257. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  258. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  259. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  260. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  261. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  262. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  263. package/lib/providers/contract.mjs +207 -0
  264. package/lib/providers/credential-bootstrap.mjs +7 -4
  265. package/lib/providers/credential-sources.mjs +14 -2
  266. package/lib/providers/directory/index.mjs +273 -0
  267. package/lib/providers/feedback/index.mjs +392 -0
  268. package/lib/providers/filter-audit.mjs +68 -0
  269. package/lib/providers/filter-schema.mjs +54 -0
  270. package/lib/providers/github/index.mjs +31 -0
  271. package/lib/providers/instance-config.mjs +215 -0
  272. package/lib/providers/op-locate.mjs +96 -0
  273. package/lib/providers/op-run.mjs +64 -9
  274. package/lib/providers/registry.mjs +26 -3
  275. package/lib/providers/secret-audit-wiring.mjs +6 -0
  276. package/lib/providers/secret-resolver.mjs +240 -23
  277. package/lib/publish-template.mjs +6 -3
  278. package/lib/publish-tooling.mjs +4 -0
  279. package/lib/publish.mjs +32 -4
  280. package/lib/queue/pg-queue.mjs +395 -0
  281. package/lib/reflect.mjs +2 -1
  282. package/lib/registry/assemble.mjs +28 -2
  283. package/lib/registry/consolidation.mjs +8 -1
  284. package/lib/registry/custom-scaffold.mjs +200 -0
  285. package/lib/registry/custom-schema.mjs +137 -0
  286. package/lib/registry/loader.mjs +8 -3
  287. package/lib/registry/manifests/format-engines.default.json +15 -0
  288. package/lib/registry/manifests/surface-map.default.json +46 -0
  289. package/lib/registry/retired-paths.mjs +1 -0
  290. package/lib/registry/surface-map.mjs +100 -50
  291. package/lib/render-visual-check.mjs +240 -0
  292. package/lib/resources/budget.mjs +40 -17
  293. package/lib/roles/flavor-bindings.mjs +36 -14
  294. package/lib/roles/gateway.mjs +15 -8
  295. package/lib/roots.mjs +107 -0
  296. package/lib/runtime/uv-bootstrap.mjs +32 -6
  297. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  298. package/lib/runtime-env.mjs +5 -2
  299. package/lib/scheduler/index.mjs +8 -20
  300. package/lib/schema-infer.mjs +31 -2
  301. package/lib/scopes/lifecycle.mjs +29 -25
  302. package/lib/scopes/loader.mjs +49 -12
  303. package/lib/scopes/teams.mjs +1 -1
  304. package/lib/security/ingest-boundary.mjs +80 -0
  305. package/lib/security/recall-wrapper.mjs +99 -0
  306. package/lib/security/trust.mjs +132 -0
  307. package/lib/service-manager.mjs +38 -19
  308. package/lib/setup.mjs +41 -23
  309. package/lib/skills-apply.mjs +4 -2
  310. package/lib/specialists/postconditions.mjs +2 -2
  311. package/lib/state-root.mjs +147 -0
  312. package/lib/status.mjs +597 -6
  313. package/lib/storage/admin.mjs +77 -5
  314. package/lib/storage/backend-registry.mjs +73 -0
  315. package/lib/storage/backend.mjs +63 -9
  316. package/lib/storage/embeddings-openai.mjs +12 -2
  317. package/lib/storage/hybrid-query.mjs +11 -3
  318. package/lib/storage/retrieval-hardening.mjs +384 -0
  319. package/lib/storage/shared-memory.mjs +158 -0
  320. package/lib/storage/vector-client.mjs +69 -2
  321. package/lib/team/health.mjs +72 -0
  322. package/lib/telemetry/backends/local.mjs +9 -3
  323. package/lib/telemetry/client.mjs +3 -7
  324. package/lib/template-registry.mjs +10 -12
  325. package/lib/tenant/context.mjs +82 -0
  326. package/lib/tenant/isolation.mjs +129 -0
  327. package/lib/test-corpus-inventory.mjs +104 -2
  328. package/lib/uninstall/uninstall.mjs +78 -12
  329. package/lib/validator.mjs +4 -6
  330. package/lib/worker/entrypoint.mjs +2 -1
  331. package/lib/worker/run.mjs +2 -2
  332. package/lib/worker/trace.mjs +5 -11
  333. package/lib/workflow-state.mjs +52 -8
  334. package/lib/workflows/liveness.mjs +203 -0
  335. package/lib/workflows/loader.mjs +177 -0
  336. package/lib/workflows/manifest-schema.mjs +71 -0
  337. package/lib/workflows/surface-parity.mjs +118 -0
  338. package/lib/workflows/validate.mjs +127 -0
  339. package/lib/writes/control-plane.mjs +140 -0
  340. package/lib/writes/envelope.mjs +206 -0
  341. package/lib/writes/sent-log.mjs +86 -0
  342. package/lib/writes/write-intent.mjs +109 -0
  343. package/package.json +16 -8
  344. package/personas/construct.md +12 -3
  345. package/registry/capabilities.json +143 -36
  346. package/rules/common/comments.md +1 -0
  347. package/schemas/project-config.schema.json +0 -12
  348. package/schemas/unified-registry.schema.json +2 -4
  349. package/scripts/sync-specialists.mjs +284 -81
  350. package/skills/docs/adr-workflow.md +1 -1
  351. package/skills/docs/codebase-research-workflow.md +3 -3
  352. package/skills/docs/prd-workflow.md +5 -6
  353. package/skills/docs/runbook-workflow.md +2 -2
  354. package/skills/docs/user-research-workflow.md +3 -3
  355. package/skills/operating/fleet-health-routing.md +77 -0
  356. package/skills/roles/ai-engineer.md +1 -1
  357. package/skills/roles/architect.md +0 -1
  358. package/skills/roles/business-strategist.md +1 -1
  359. package/skills/roles/data-analyst.telemetry.md +1 -1
  360. package/skills/roles/data-engineer.md +1 -1
  361. package/skills/roles/data-engineer.pipeline.md +1 -1
  362. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  363. package/skills/roles/data-engineer.warehouse.md +1 -1
  364. package/skills/roles/designer.accessibility.md +1 -1
  365. package/skills/roles/designer.md +0 -1
  366. package/skills/roles/devil-advocate.md +1 -1
  367. package/skills/roles/docs-keeper.md +1 -1
  368. package/skills/roles/evaluator.md +1 -1
  369. package/skills/roles/explorer.md +1 -1
  370. package/skills/roles/platform-engineer.md +1 -1
  371. package/skills/roles/qa.ai-eval.md +1 -2
  372. package/skills/roles/qa.api-contract.md +0 -1
  373. package/skills/roles/qa.data-pipeline.md +0 -1
  374. package/skills/roles/qa.md +0 -1
  375. package/skills/roles/qa.web-ui.md +0 -1
  376. package/skills/roles/release-manager.md +1 -1
  377. package/skills/roles/researcher.md +0 -2
  378. package/skills/roles/reviewer.md +0 -3
  379. package/skills/roles/security.ai.md +1 -1
  380. package/skills/roles/security.legal-compliance.md +1 -1
  381. package/skills/roles/security.md +0 -1
  382. package/skills/roles/security.privacy.md +0 -1
  383. package/skills/roles/security.supply-chain.md +1 -1
  384. package/skills/roles/sre.md +1 -1
  385. package/skills/roles/test-automation.md +1 -1
  386. package/skills/roles/trace-reviewer.md +1 -1
  387. package/skills/roles/ux-researcher.md +1 -1
  388. package/specialists/artifact-manifest.json +36 -36
  389. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  390. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  391. package/specialists/org/contracts/any-to-debugger.json +7 -1
  392. package/specialists/org/contracts/any-to-designer.json +7 -1
  393. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  394. package/specialists/org/contracts/any-to-explorer.json +13 -4
  395. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  396. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  397. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  398. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  399. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  400. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  401. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  402. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  403. package/specialists/org/contracts/architect-to-operations.json +17 -4
  404. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  405. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  406. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  407. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  408. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  409. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  410. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  411. package/specialists/org/contracts/operations-to-user.json +53 -0
  412. package/specialists/org/contracts/operations-triage-output.json +53 -0
  413. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  414. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  415. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  416. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  417. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  418. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  419. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  420. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  421. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  422. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  423. package/specialists/org/contracts/user-to-construct.json +11 -4
  424. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  425. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  426. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  427. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  428. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  429. package/specialists/org/groups/engineering-group.json +2 -6
  430. package/specialists/org/groups/governance-group.json +2 -3
  431. package/specialists/org/groups/operations-group.json +5 -8
  432. package/specialists/org/groups/product-group.json +4 -8
  433. package/specialists/org/groups/quality-group.json +3 -7
  434. package/specialists/org/groups/strategy-group.json +6 -9
  435. package/specialists/org/models.json.example +8 -0
  436. package/specialists/org/scopes/creative.json +6 -1
  437. package/specialists/org/scopes/operations.json +7 -1
  438. package/specialists/org/scopes/research.json +6 -1
  439. package/specialists/org/scopes/rnd.json +7 -1
  440. package/specialists/org/specialists/cx-architect.json +27 -8
  441. package/specialists/org/specialists/cx-designer.json +22 -8
  442. package/specialists/org/specialists/cx-engineer.json +71 -7
  443. package/specialists/org/specialists/cx-operations.json +89 -15
  444. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  445. package/specialists/org/specialists/cx-product-manager.json +28 -9
  446. package/specialists/org/specialists/cx-qa.json +16 -6
  447. package/specialists/org/specialists/cx-researcher.json +40 -7
  448. package/specialists/org/specialists/cx-reviewer.json +61 -11
  449. package/specialists/org/specialists/cx-security.json +30 -10
  450. package/specialists/org/teams/design-team.json +3 -4
  451. package/specialists/org/teams/engineering-team.json +3 -10
  452. package/specialists/org/teams/governance-team.json +3 -5
  453. package/specialists/org/teams/operations-team.json +6 -12
  454. package/specialists/org/teams/product-management-team.json +2 -3
  455. package/specialists/org/teams/quality-team.json +4 -12
  456. package/specialists/org/teams/research-team.json +3 -3
  457. package/specialists/org/teams/strategy-team.json +7 -14
  458. package/specialists/prompts/cx-architect.md +20 -1
  459. package/specialists/prompts/cx-data-analyst.md +1 -1
  460. package/specialists/prompts/cx-designer.md +12 -4
  461. package/specialists/prompts/cx-engineer.md +15 -1
  462. package/specialists/prompts/cx-operations.md +14 -2
  463. package/specialists/prompts/cx-orchestrator.md +9 -5
  464. package/specialists/prompts/cx-product-manager.md +5 -1
  465. package/specialists/prompts/cx-qa.md +6 -2
  466. package/specialists/prompts/cx-researcher.md +25 -30
  467. package/specialists/prompts/cx-reviewer.md +19 -1
  468. package/specialists/prompts/cx-security.md +5 -1
  469. package/templates/distribution/construct-brand.typ +123 -59
  470. package/templates/distribution/construct-decision.typ +6 -3
  471. package/templates/distribution/construct-pdf.typ +11 -4
  472. package/templates/distribution/construct-prd.typ +6 -3
  473. package/templates/distribution/construct-research.typ +7 -4
  474. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  475. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  476. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  477. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  478. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  479. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  480. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  481. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  482. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  483. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  484. package/specialists/org/specialists/cx-accessibility.json +0 -69
  485. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  486. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  487. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  488. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  489. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  490. package/specialists/org/specialists/cx-evaluator.json +0 -69
  491. package/specialists/org/specialists/cx-explorer.json +0 -69
  492. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  493. package/specialists/org/specialists/cx-oracle.json +0 -46
  494. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  495. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  496. package/specialists/org/specialists/cx-release-manager.json +0 -77
  497. package/specialists/org/specialists/cx-sre.json +0 -94
  498. package/specialists/org/specialists/cx-test-automation.json +0 -69
  499. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  500. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  501. package/specialists/org/teams/accessibility-team.json +0 -39
  502. package/specialists/org/teams/ux-research-team.json +0 -39
  503. package/specialists/prompts/cx-accessibility.md +0 -55
  504. package/specialists/prompts/cx-ai-engineer.md +0 -124
  505. package/specialists/prompts/cx-business-strategist.md +0 -58
  506. package/specialists/prompts/cx-data-engineer.md +0 -55
  507. package/specialists/prompts/cx-devil-advocate.md +0 -59
  508. package/specialists/prompts/cx-docs-keeper.md +0 -164
  509. package/specialists/prompts/cx-evaluator.md +0 -49
  510. package/specialists/prompts/cx-explorer.md +0 -71
  511. package/specialists/prompts/cx-legal-compliance.md +0 -58
  512. package/specialists/prompts/cx-oracle.md +0 -98
  513. package/specialists/prompts/cx-platform-engineer.md +0 -97
  514. package/specialists/prompts/cx-rd-lead.md +0 -59
  515. package/specialists/prompts/cx-release-manager.md +0 -54
  516. package/specialists/prompts/cx-sre.md +0 -111
  517. package/specialists/prompts/cx-test-automation.md +0 -55
  518. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  519. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -16,10 +16,25 @@
16
16
  * optional one. The returned store is always async-uniform so callers await it
17
17
  * regardless of backend; the filesystem and sqlite backends resolve synchronously
18
18
  * under the hood, so the default Mode-A run record is byte-for-byte unchanged.
19
+ *
20
+ * Tenant scoping (LMCP-H4, ADR-0057/A7 stage 1): the postgres backend is the
21
+ * one tier multiple tenants can share a physical table on, so resolveRunStore
22
+ * resolves the tenant context (resolveTenantContext) before constructing it
23
+ * and scopes every query by (project, tenant_id). Enterprise mode with no
24
+ * resolvable tenant id fails closed here — TenantResolutionError propagates
25
+ * uncaught rather than the resolver silently defaulting to a shared bucket.
26
+ * Filesystem/sqlite backends are already isolated per project directory and
27
+ * are not multi-tenant-shared, so they carry no tenant filter.
19
28
  */
20
29
 
21
30
  import { getDeploymentMode } from '../deployment-mode.mjs';
22
31
  import { createSqlClient } from '../storage/backend.mjs';
32
+ import { resolveTenantContext } from '../tenant/context.mjs';
33
+ import {
34
+ resolveStorageBackend,
35
+ UnregisteredStorageProviderError,
36
+ UnsupportedStorageProviderError,
37
+ } from '../storage/backend-registry.mjs';
23
38
  import { saveRun as fsSaveRun, loadRun as fsLoadRun, listRuns as fsListRuns } from './run-store.mjs';
24
39
  import { sqliteAvailable, createSqliteRunStore } from './run-store-sqlite.mjs';
25
40
  import { PostgresRunStore } from './run-store-postgres.mjs';
@@ -46,8 +61,8 @@ function sqliteStore(cwd) {
46
61
  };
47
62
  }
48
63
 
49
- function postgresStore({ sql, project }) {
50
- const store = new PostgresRunStore({ sql, project });
64
+ function postgresStore({ sql, project, tenantId }) {
65
+ const store = new PostgresRunStore({ sql, project, tenantId });
51
66
  let ensured = null;
52
67
  const ready = () => {
53
68
  if (!ensured) ensured = store.ensureSchema();
@@ -61,15 +76,32 @@ function postgresStore({ sql, project }) {
61
76
  };
62
77
  }
63
78
 
64
- function projectKey(config, cwd) {
79
+ /**
80
+ * The same project key resolveRunStore uses to scope Postgres rows —
81
+ * exported so other team-store consumers (trace-store.mjs, shared-memory.mjs)
82
+ * key their own tables identically instead of re-deriving a divergent value.
83
+ */
84
+ export function projectKey(config, cwd) {
65
85
  return config?.deployment?.projectName || cwd || 'default';
66
86
  }
67
87
 
68
88
  function chooseBackend({ config, env, cwd }) {
69
89
  const explicit = String(env?.[STORE_ENV_KEY] || config?.orchestration?.store || '').trim().toLowerCase();
70
- if (ORCHESTRATION_STORES.includes(explicit)) return explicit;
90
+ if (explicit) return resolveStorageBackend(explicit, { rootDir: cwd, env });
71
91
  const mode = getDeploymentMode(env, { cwd });
72
- return mode === 'solo' ? 'filesystem' : 'postgres';
92
+ return resolveStorageBackend(mode === 'solo' ? 'filesystem' : 'postgres', { rootDir: cwd, env });
93
+ }
94
+
95
+ function degradedFilesystem({ requested, reason, warning, provider = null, cwd }) {
96
+ return {
97
+ store: filesystemStore(cwd),
98
+ backend: 'filesystem',
99
+ requestedBackend: requested,
100
+ provider,
101
+ degraded: true,
102
+ degradedReason: reason,
103
+ warnings: [warning],
104
+ };
73
105
  }
74
106
 
75
107
  /**
@@ -83,20 +115,63 @@ function chooseBackend({ config, env, cwd }) {
83
115
  */
84
116
  export function resolveRunStore({ config = {}, env = process.env, cwd = process.cwd() } = {}) {
85
117
  const warnings = [];
86
- const requested = chooseBackend({ config, env, cwd });
118
+ let resolved;
119
+ try {
120
+ resolved = chooseBackend({ config, env, cwd });
121
+ } catch (err) {
122
+ if (err instanceof UnregisteredStorageProviderError) {
123
+ return degradedFilesystem({
124
+ cwd,
125
+ requested: err.backend,
126
+ reason: 'storage-backend-unregistered',
127
+ warning: `orchestration.store "${err.backend}" has no registered kind:'storage' provider; falling back to filesystem.`,
128
+ });
129
+ }
130
+ if (err instanceof UnsupportedStorageProviderError) {
131
+ return degradedFilesystem({
132
+ cwd,
133
+ requested: err.backend,
134
+ reason: 'storage-backend-unsupported',
135
+ warning: err.message,
136
+ });
137
+ }
138
+ throw err;
139
+ }
140
+
141
+ const requested = resolved.implementation;
87
142
 
88
143
  if (requested === 'sqlite') {
89
- if (sqliteAvailable()) return { store: sqliteStore(cwd), backend: 'sqlite', warnings };
144
+ if (sqliteAvailable()) return { store: sqliteStore(cwd), backend: resolved.backend, provider: resolved.provider, warnings };
90
145
  warnings.push('orchestration.store "sqlite" requires Node >=22.5 (node:sqlite unavailable); falling back to filesystem.');
91
- return { store: filesystemStore(cwd), backend: 'filesystem', warnings };
146
+ return {
147
+ store: filesystemStore(cwd),
148
+ backend: 'filesystem',
149
+ requestedBackend: resolved.backend,
150
+ provider: resolved.provider,
151
+ degraded: true,
152
+ degradedReason: 'sqlite-unavailable',
153
+ warnings,
154
+ };
92
155
  }
93
156
 
94
157
  if (requested === 'postgres') {
95
158
  const sql = createSqlClient(env);
96
- if (sql) return { store: postgresStore({ sql, project: projectKey(config, cwd) }), backend: 'postgres', warnings };
97
- warnings.push('orchestration.store "postgres" requires a reachable DATABASE_URL; falling back to filesystem.');
98
- return { store: filesystemStore(cwd), backend: 'filesystem', warnings };
159
+ if (sql) {
160
+ const mode = getDeploymentMode(env, { cwd });
161
+ const { tenantId } = resolveTenantContext({ env, config, mode });
162
+ return { store: postgresStore({ sql, project: projectKey(config, cwd), tenantId }), backend: resolved.backend, provider: resolved.provider, tenantId, warnings };
163
+ }
164
+ warnings.push(`orchestration.store "${resolved.backend}" requires a reachable DATABASE_URL; falling back to filesystem.`);
165
+ return {
166
+ store: filesystemStore(cwd),
167
+ backend: 'filesystem',
168
+ requestedBackend: resolved.backend,
169
+ provider: resolved.provider,
170
+ degraded: true,
171
+ degradedReason: 'postgres-unavailable',
172
+ warnings,
173
+ };
99
174
  }
100
175
 
101
- return { store: filesystemStore(cwd), backend: 'filesystem', warnings };
176
+ return { store: filesystemStore(cwd), backend: resolved.backend, provider: resolved.provider, warnings };
102
177
  }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * lib/orchestration/trace-store.mjs — team-shared trace event persistence (LMCP-G10).
3
+ *
4
+ * emitTraceEvent (lib/worker/trace.mjs) always writes the local, append-only
5
+ * `.cx/traces/<date>.jsonl` shard — that stays unchanged, on every deployment
6
+ * mode, with no credentials required. This module is the additional,
7
+ * postgres-backed channel a run's lifecycle/worker trace events feed so a
8
+ * team member on a different machine can read the SAME trace a worker on
9
+ * another machine emitted — the "run/task traces -> team store" half of the
10
+ * A1/A6 thread-vs-store boundary (see lib/storage/shared-memory.mjs for the
11
+ * long-term-memory half).
12
+ *
13
+ * Solo mode (no reachable Postgres) resolves to a `kind:'none'` store whose
14
+ * save/list are no-ops — the local JSONL trace already covers that case, and
15
+ * a team-shared read path with nothing durable behind it would be a silent
16
+ * lie rather than a real capability.
17
+ *
18
+ * Tenant scoping (LMCP-H4, ADR-0057/A7 stage 1): every row carries a
19
+ * `tenant_id` column and every query filters on it, matching the run store
20
+ * (run-store-postgres.mjs) and shared-memory store (shared-memory.mjs).
21
+ * resolveTraceStore resolves the tenant context up front and fails closed —
22
+ * TenantResolutionError propagates uncaught — when enterprise mode has no
23
+ * resolvable tenant id, rather than defaulting rows into a shared 'local'
24
+ * bucket. Stage 2 (physical per-tenant isolation, per-tenant encryption) is
25
+ * explicitly deferred and not advertised; this remains one shared table with
26
+ * row-level tenant scoping only.
27
+ */
28
+
29
+ import { createSqlClient } from '../storage/backend.mjs';
30
+ import { getDeploymentMode } from '../deployment-mode.mjs';
31
+ import { resolveTenantContext } from '../tenant/context.mjs';
32
+
33
+ function noneStore() {
34
+ return {
35
+ kind: 'none',
36
+ saveTraceEvent: async () => ({ ok: false, reason: 'no-team-store-configured' }),
37
+ listTeamTraces: async () => [],
38
+ };
39
+ }
40
+
41
+ function postgresStore(sql, defaultTenantId) {
42
+ let ensured = null;
43
+ const ready = () => {
44
+ if (!ensured) {
45
+ ensured = (async () => {
46
+ const { applyMigrations } = await import('../db/migrate.mjs');
47
+ await applyMigrations(sql);
48
+ })();
49
+ }
50
+ return ensured;
51
+ };
52
+
53
+ return {
54
+ kind: 'postgres',
55
+ tenantId: defaultTenantId,
56
+ async saveTraceEvent(event, { project, tenantId = defaultTenantId } = {}) {
57
+ if (!project) throw new Error('saveTraceEvent: project is required');
58
+ if (!event?.traceId || !event?.spanId) throw new Error('saveTraceEvent: event.traceId and event.spanId are required');
59
+ await ready();
60
+ await sql`
61
+ INSERT INTO construct_trace_events (
62
+ project, tenant_id, trace_id, span_id, parent_span_id, event_type, role, task_id, metadata, created_at
63
+ ) VALUES (
64
+ ${project}, ${tenantId}, ${event.traceId}, ${event.spanId}, ${event.parentSpanId || null},
65
+ ${event.eventType}, ${event.role || null}, ${event.taskId || null},
66
+ ${sql.json(event.metadata || {})}, ${event.createdAt || new Date().toISOString()}
67
+ )
68
+ ON CONFLICT (project, tenant_id, trace_id, span_id) DO NOTHING
69
+ `;
70
+ return { ok: true };
71
+ },
72
+ async listTeamTraces({ project, tenantId = defaultTenantId, traceId, limit = 100 } = {}) {
73
+ if (!project) throw new Error('listTeamTraces: project is required');
74
+ await ready();
75
+ const rows = traceId
76
+ ? await sql`
77
+ SELECT trace_id, span_id, parent_span_id, event_type, role, task_id, metadata, created_at
78
+ FROM construct_trace_events
79
+ WHERE project = ${project} AND tenant_id = ${tenantId} AND trace_id = ${traceId}
80
+ ORDER BY created_at ASC
81
+ LIMIT ${limit}
82
+ `
83
+ : await sql`
84
+ SELECT trace_id, span_id, parent_span_id, event_type, role, task_id, metadata, created_at
85
+ FROM construct_trace_events
86
+ WHERE project = ${project} AND tenant_id = ${tenantId}
87
+ ORDER BY created_at DESC
88
+ LIMIT ${limit}
89
+ `;
90
+ return rows.map((row) => ({
91
+ traceId: row.trace_id,
92
+ spanId: row.span_id,
93
+ parentSpanId: row.parent_span_id,
94
+ eventType: row.event_type,
95
+ role: row.role,
96
+ taskId: row.task_id,
97
+ metadata: row.metadata,
98
+ createdAt: row.created_at,
99
+ }));
100
+ },
101
+ };
102
+ }
103
+
104
+ /**
105
+ * resolveTraceStore(opts)
106
+ *
107
+ * `sql` accepts an injected client (matching lib/team/health.mjs's
108
+ * convention) so a caller — or a test backed by an in-memory fake — can
109
+ * supply a shared substrate directly instead of resolving one from env.
110
+ * Enterprise mode with no resolvable tenant id throws TenantResolutionError
111
+ * before any query runs (fail-closed).
112
+ *
113
+ * @param {{ env?: object, cwd?: string, config?: object, sql?: object }} [opts]
114
+ * @returns {{ kind: string, saveTraceEvent: Function, listTeamTraces: Function }}
115
+ */
116
+ export function resolveTraceStore({ env = process.env, cwd = process.cwd(), config = null, sql = null } = {}) {
117
+ const mode = getDeploymentMode(env, { cwd });
118
+ if (mode === 'solo' && !sql) return noneStore();
119
+
120
+ const ownSql = sql || createSqlClient(env);
121
+ if (!ownSql) return noneStore();
122
+
123
+ const { tenantId } = resolveTenantContext({ env, config, mode });
124
+ return postgresStore(ownSql, tenantId);
125
+ }
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { getSpecialist } from '../registry/loader.mjs';
20
+ import { resolveNonNegativeSetting } from '../env-config.mjs';
20
21
 
21
22
  const DEFAULT_MAX_USES = 5;
22
23
  const DEFAULT_MAX_RESULTS = 5;
@@ -39,11 +40,11 @@ export function resolveWebCapability({ family, env = process.env } = {}) {
39
40
  return { mode: 'governed' };
40
41
  }
41
42
  if (family === 'anthropic') {
42
- const maxUses = Number(env.CONSTRUCT_WORKER_WEB_MAX_USES) || DEFAULT_MAX_USES;
43
+ const maxUses = resolveNonNegativeSetting(env, 'CONSTRUCT_WORKER_WEB_MAX_USES', DEFAULT_MAX_USES);
43
44
  return { mode: 'provider-native', providerTool: 'anthropic', toolType: 'web_search_20250305', maxUses };
44
45
  }
45
46
  if (family === 'openrouter') {
46
- const maxResults = Number(env.CONSTRUCT_WORKER_WEB_MAX_RESULTS) || DEFAULT_MAX_RESULTS;
47
+ const maxResults = resolveNonNegativeSetting(env, 'CONSTRUCT_WORKER_WEB_MAX_RESULTS', DEFAULT_MAX_RESULTS);
47
48
  return {
48
49
  mode: 'provider-native',
49
50
  providerTool: 'openrouter',
@@ -0,0 +1,162 @@
1
+ /**
2
+ * lib/orchestration/worker-runtime.mjs — team worker identity + heartbeat.
3
+ *
4
+ * Durable liveness sidecar for queue leases. The module does not start a worker
5
+ * pool; it records lease ownership so status/doctor can distinguish live and
6
+ * expired workers.
7
+ */
8
+
9
+ import os from 'node:os';
10
+ import process from 'node:process';
11
+
12
+ import { applyMigrations } from '../db/migrate.mjs';
13
+
14
+ export const WORKER_ID_ENV_KEY = 'CONSTRUCT_WORKER_ID';
15
+ export const WORKER_TTL_ENV_KEY = 'CONSTRUCT_WORKER_TTL_SECONDS';
16
+ export const WORKER_DEFAULT_TTL_SECONDS = 120;
17
+
18
+ function defaultWorkerId(env = process.env) {
19
+ const explicit = env?.[WORKER_ID_ENV_KEY];
20
+ if (explicit && String(explicit).trim()) return String(explicit).trim();
21
+ return `${os.hostname()}:${process.pid}`;
22
+ }
23
+
24
+ function normalizeTtl(value) {
25
+ const n = Number(value);
26
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : WORKER_DEFAULT_TTL_SECONDS;
27
+ }
28
+
29
+ function normalizeCapabilities(capabilities = []) {
30
+ return Array.isArray(capabilities)
31
+ ? [...new Set(capabilities.map((c) => String(c).trim()).filter(Boolean))].sort()
32
+ : [];
33
+ }
34
+
35
+ export class WorkerRegistry {
36
+ constructor({ sql, project, tenantId = 'local' } = {}) {
37
+ if (!sql) throw new Error('WorkerRegistry: sql client is required');
38
+ if (!project) throw new Error('WorkerRegistry: project is required');
39
+ this.sql = sql;
40
+ this.project = project;
41
+ this.tenantId = tenantId;
42
+ this._ensured = null;
43
+ }
44
+
45
+ async ensureSchema() {
46
+ if (!this._ensured) this._ensured = applyMigrations(this.sql);
47
+ return this._ensured;
48
+ }
49
+
50
+ async register({
51
+ workerId,
52
+ env = process.env,
53
+ host = os.hostname(),
54
+ pid = process.pid,
55
+ capabilities = [],
56
+ ttlSeconds = env?.[WORKER_TTL_ENV_KEY],
57
+ metadata = {},
58
+ } = {}) {
59
+ await this.ensureSchema();
60
+ const id = workerId || defaultWorkerId(env);
61
+ const ttl = normalizeTtl(ttlSeconds);
62
+ const caps = normalizeCapabilities(capabilities);
63
+ await this.sql`
64
+ INSERT INTO construct_workers (
65
+ project, tenant_id, worker_id, host, pid, capabilities,
66
+ registered_at, heartbeat_at, lease_ttl_seconds, status, metadata
67
+ ) VALUES (
68
+ ${this.project}, ${this.tenantId}, ${id}, ${host || null}, ${Number(pid) || null},
69
+ ${this.sql.json(caps)}, now(), now(), ${ttl}, 'active', ${this.sql.json(metadata || {})}
70
+ )
71
+ ON CONFLICT (project, tenant_id, worker_id) DO UPDATE SET
72
+ host = EXCLUDED.host,
73
+ pid = EXCLUDED.pid,
74
+ capabilities = EXCLUDED.capabilities,
75
+ heartbeat_at = now(),
76
+ lease_ttl_seconds = EXCLUDED.lease_ttl_seconds,
77
+ status = 'active',
78
+ metadata = EXCLUDED.metadata
79
+ `;
80
+ return { workerId: id, project: this.project, tenantId: this.tenantId, host, pid, capabilities: caps, ttlSeconds: ttl };
81
+ }
82
+
83
+ async heartbeat(workerId, { ttlSeconds, renewQueue = null } = {}) {
84
+ if (!workerId) throw new Error('heartbeat: workerId is required');
85
+ await this.ensureSchema();
86
+ const ttl = normalizeTtl(ttlSeconds);
87
+ const rows = await this.sql`
88
+ UPDATE construct_workers
89
+ SET heartbeat_at = now(),
90
+ lease_ttl_seconds = ${ttl},
91
+ status = 'active'
92
+ WHERE project = ${this.project}
93
+ AND tenant_id = ${this.tenantId}
94
+ AND worker_id = ${workerId}
95
+ RETURNING worker_id, heartbeat_at, lease_ttl_seconds
96
+ `;
97
+ if (!rows[0]) return { workerId, renewed: false };
98
+ if (renewQueue && typeof renewQueue.heartbeat === 'function') {
99
+ await renewQueue.heartbeat(renewQueue.itemId, { workerId, leaseSeconds: ttl });
100
+ }
101
+ return {
102
+ workerId,
103
+ renewed: true,
104
+ heartbeatAt: rows[0].heartbeat_at,
105
+ ttlSeconds: rows[0].lease_ttl_seconds,
106
+ };
107
+ }
108
+
109
+ async deregister(workerId) {
110
+ if (!workerId) throw new Error('deregister: workerId is required');
111
+ await this.ensureSchema();
112
+ const rows = await this.sql`
113
+ UPDATE construct_workers
114
+ SET status = 'stopped'
115
+ WHERE project = ${this.project}
116
+ AND tenant_id = ${this.tenantId}
117
+ AND worker_id = ${workerId}
118
+ RETURNING worker_id
119
+ `;
120
+ return { workerId, stopped: Boolean(rows[0]) };
121
+ }
122
+
123
+ async list({ includeStale = true } = {}) {
124
+ await this.ensureSchema();
125
+ const rows = includeStale
126
+ ? await this.sql`
127
+ SELECT worker_id, host, pid, capabilities, registered_at, heartbeat_at,
128
+ lease_ttl_seconds, status, metadata,
129
+ (heartbeat_at + (lease_ttl_seconds * interval '1 second') < now()) AS stale
130
+ FROM construct_workers
131
+ WHERE project = ${this.project} AND tenant_id = ${this.tenantId}
132
+ ORDER BY heartbeat_at DESC, worker_id ASC
133
+ `
134
+ : await this.sql`
135
+ SELECT worker_id, host, pid, capabilities, registered_at, heartbeat_at,
136
+ lease_ttl_seconds, status, metadata,
137
+ (heartbeat_at + (lease_ttl_seconds * interval '1 second') < now()) AS stale
138
+ FROM construct_workers
139
+ WHERE project = ${this.project}
140
+ AND tenant_id = ${this.tenantId}
141
+ AND status = 'active'
142
+ AND heartbeat_at + (lease_ttl_seconds * interval '1 second') >= now()
143
+ ORDER BY heartbeat_at DESC, worker_id ASC
144
+ `;
145
+ return rows.map((row) => ({
146
+ workerId: row.worker_id,
147
+ host: row.host,
148
+ pid: row.pid,
149
+ capabilities: row.capabilities || [],
150
+ registeredAt: row.registered_at,
151
+ heartbeatAt: row.heartbeat_at,
152
+ ttlSeconds: row.lease_ttl_seconds,
153
+ status: row.status === 'active' && row.stale ? 'stale' : row.status,
154
+ stale: Boolean(row.stale),
155
+ metadata: row.metadata || {},
156
+ }));
157
+ }
158
+ }
159
+
160
+ export function createWorkerRegistry({ sql, project, tenantId } = {}) {
161
+ return new WorkerRegistry({ sql, project, tenantId });
162
+ }