@geraldmaron/construct 1.4.1 → 1.5.0

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 (556) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1029 -65
  4. package/bin/construct-postinstall.mjs +27 -2
  5. package/config/tag-vocabulary.json +264 -0
  6. package/examples/distribution/sources/deck-one-pager.md +1 -1
  7. package/lib/acp/server.mjs +21 -7
  8. package/lib/adapters-sync.mjs +2 -1
  9. package/lib/audit-trail.mjs +185 -2
  10. package/lib/beads/auto-close.mjs +2 -1
  11. package/lib/beads/drift.mjs +2 -1
  12. package/lib/bridges/copilot-proxy.mjs +20 -5
  13. package/lib/certification/skill-inventory.mjs +10 -1
  14. package/lib/cli/approvals.mjs +147 -0
  15. package/lib/cli-commands.mjs +105 -14
  16. package/lib/comment-lint.mjs +127 -8
  17. package/lib/config/schema.mjs +7 -30
  18. package/lib/config/source-target-registry.mjs +70 -0
  19. package/lib/config/source-targets.mjs +179 -205
  20. package/lib/contracts/coverage.mjs +77 -0
  21. package/lib/contracts/validate.mjs +102 -13
  22. package/lib/contracts/violation-log.mjs +50 -4
  23. package/lib/db/migrate.mjs +69 -0
  24. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  25. package/lib/db/migrations/002_queue_provider.sql +50 -0
  26. package/lib/db/migrations/003_worker_registry.sql +17 -0
  27. package/lib/db/migrations/004_trace_events.sql +16 -0
  28. package/lib/db/migrations/005_shared_memory.sql +15 -0
  29. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  30. package/lib/decisions/enforced-baseline.json +0 -2
  31. package/lib/decisions/registry.mjs +3 -2
  32. package/lib/deployment/parity-contract.mjs +1 -1
  33. package/lib/deployment-mode.mjs +78 -2
  34. package/lib/diagram-export.mjs +10 -1
  35. package/lib/distill.mjs +5 -2
  36. package/lib/docs-verify.mjs +2 -1
  37. package/lib/doctor/cli.mjs +1 -0
  38. package/lib/doctor/command-on-path.mjs +24 -0
  39. package/lib/doctor/diagnosis.mjs +109 -0
  40. package/lib/doctor/embedding-health.mjs +50 -0
  41. package/lib/doctor/engine-health.mjs +93 -0
  42. package/lib/doctor/graph-validate.mjs +47 -0
  43. package/lib/doctor/index.mjs +23 -4
  44. package/lib/doctor/sidecar-providers.mjs +56 -0
  45. package/lib/doctor/watchers/consistency.mjs +93 -1
  46. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  47. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  48. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  49. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  50. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  51. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  52. package/lib/document-export.mjs +52 -27
  53. package/lib/document-extract/docling-client.mjs +9 -5
  54. package/lib/document-extract/docling-sidecar.py +30 -0
  55. package/lib/document-extract.mjs +1 -1
  56. package/lib/document-ingest.mjs +9 -0
  57. package/lib/embed/approval-queue.mjs +184 -88
  58. package/lib/embed/authority-guard.mjs +65 -2
  59. package/lib/embed/capability-jobs.mjs +365 -0
  60. package/lib/embed/capability-lifecycle.mjs +219 -0
  61. package/lib/embed/capability-loader.mjs +303 -0
  62. package/lib/embed/capability-runtime.mjs +58 -0
  63. package/lib/embed/cli.mjs +185 -8
  64. package/lib/embed/config.mjs +4 -0
  65. package/lib/embed/daemon.mjs +125 -6
  66. package/lib/embed/demand-fetch.mjs +190 -54
  67. package/lib/embed/inbox.mjs +12 -10
  68. package/lib/embed/presets/ops-triage.mjs +236 -0
  69. package/lib/embed/presets/pm-feedback.mjs +341 -0
  70. package/lib/embed/presets/tpm.mjs +401 -0
  71. package/lib/embed/providers/jira.mjs +72 -1
  72. package/lib/embed/providers/registry.mjs +140 -33
  73. package/lib/embed/worker.mjs +5 -0
  74. package/lib/embedded-contract/capability.mjs +4 -0
  75. package/lib/embedded-contract/execution.mjs +1 -1
  76. package/lib/embedded-contract/model-resolve.mjs +53 -3
  77. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  78. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  79. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  80. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  81. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  82. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  83. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  84. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  85. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  86. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  88. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  90. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  91. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  92. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  93. package/lib/env-config.mjs +60 -11
  94. package/lib/export-validate.mjs +34 -2
  95. package/lib/extensions/index.mjs +27 -0
  96. package/lib/extensions/loader.mjs +120 -0
  97. package/lib/extensions/manifest-schema.mjs +141 -0
  98. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  99. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  100. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  101. package/lib/extensions/manifests/directory.manifest.json +12 -0
  102. package/lib/extensions/manifests/docling.manifest.json +37 -0
  103. package/lib/extensions/manifests/echo.manifest.json +8 -0
  104. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  105. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  106. package/lib/extensions/manifests/github.manifest.json +52 -0
  107. package/lib/extensions/manifests/linear.manifest.json +50 -0
  108. package/lib/extensions/manifests/local.manifest.json +12 -0
  109. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  110. package/lib/extensions/manifests/openai.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  113. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  114. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  115. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  116. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  117. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  118. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  119. package/lib/extensions/manifests/slack.manifest.json +58 -0
  120. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  121. package/lib/extensions/validate.mjs +175 -0
  122. package/lib/features.mjs +13 -9
  123. package/lib/flows/checkpoint.mjs +184 -0
  124. package/lib/flows/constants.mjs +27 -0
  125. package/lib/flows/define.mjs +146 -0
  126. package/lib/flows/engine.mjs +249 -0
  127. package/lib/flows/errors.mjs +19 -0
  128. package/lib/flows/index.mjs +27 -0
  129. package/lib/flows/joins.mjs +18 -0
  130. package/lib/flows/schema.mjs +90 -0
  131. package/lib/flows/state.mjs +31 -0
  132. package/lib/frameworks/loader.mjs +99 -0
  133. package/lib/frameworks/schema.mjs +157 -0
  134. package/lib/graph/build-from-corpus.mjs +67 -0
  135. package/lib/graph/build-from-embed.mjs +82 -0
  136. package/lib/graph/build-from-registry.mjs +164 -3
  137. package/lib/graph/cli.mjs +456 -12
  138. package/lib/graph/gap-queries.mjs +156 -0
  139. package/lib/graph/gaps.mjs +41 -0
  140. package/lib/graph/impacted.mjs +129 -0
  141. package/lib/graph/runtime-evidence.mjs +177 -0
  142. package/lib/graph/security-coverage.mjs +113 -0
  143. package/lib/graph/staleness.mjs +241 -10
  144. package/lib/graph/store.mjs +24 -7
  145. package/lib/graph/validate.mjs +161 -0
  146. package/lib/headhunt.mjs +9 -8
  147. package/lib/health-check.mjs +15 -7
  148. package/lib/hook-health.mjs +1 -1
  149. package/lib/hooks/agent-tracker.mjs +10 -4
  150. package/lib/hooks/edit-guard.mjs +3 -3
  151. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  152. package/lib/hooks/guard-bash.mjs +41 -15
  153. package/lib/hooks/mcp-health-check.mjs +11 -4
  154. package/lib/hooks/model-fallback.mjs +50 -6
  155. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  156. package/lib/hooks/pre-compact.mjs +181 -173
  157. package/lib/hooks/rule-verifier.mjs +2 -1
  158. package/lib/hooks/session-reflect.mjs +2 -1
  159. package/lib/hooks/session-start.mjs +3 -3
  160. package/lib/host/readiness.mjs +109 -0
  161. package/lib/host-capabilities.mjs +13 -2
  162. package/lib/host-disposition.mjs +29 -8
  163. package/lib/identity.mjs +92 -0
  164. package/lib/ingest/degraded-extract.mjs +96 -0
  165. package/lib/ingest/docling-remote.mjs +2 -1
  166. package/lib/ingest/provider-extract.mjs +7 -19
  167. package/lib/ingest/sidecar-providers.mjs +196 -0
  168. package/lib/ingest-tooling.mjs +11 -5
  169. package/lib/init-unified.mjs +55 -38
  170. package/lib/init-update.mjs +2 -1
  171. package/lib/install/legacy-global-cleanup.mjs +25 -0
  172. package/lib/install/stage-project.mjs +41 -4
  173. package/lib/intake/daemon.mjs +99 -8
  174. package/lib/intake/git-queue.mjs +110 -38
  175. package/lib/intake/prepare.mjs +2 -0
  176. package/lib/intake/queue-registry.mjs +50 -0
  177. package/lib/intake/queue.mjs +133 -17
  178. package/lib/intake/session-prelude.mjs +57 -8
  179. package/lib/integrations/intake-integrations.mjs +61 -111
  180. package/lib/intent-classifier.mjs +43 -5
  181. package/lib/libreoffice-export.mjs +8 -8
  182. package/lib/logging/rotate.mjs +5 -4
  183. package/lib/mcp/broker.mjs +332 -17
  184. package/lib/mcp/denied-store.mjs +95 -0
  185. package/lib/mcp/destructive-approval.mjs +57 -0
  186. package/lib/mcp/destructive-gate.mjs +30 -0
  187. package/lib/mcp/dispatch-envelope.mjs +199 -0
  188. package/lib/mcp/memory-bridge.mjs +2 -1
  189. package/lib/mcp/server.mjs +185 -1268
  190. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  191. package/lib/mcp/tool-definitions-project.mjs +271 -0
  192. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  193. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  194. package/lib/mcp/tool-definitions.mjs +25 -0
  195. package/lib/mcp/tool-rate-limit.mjs +47 -0
  196. package/lib/mcp/tool-registry.mjs +107 -0
  197. package/lib/mcp/tool-safety.mjs +95 -0
  198. package/lib/mcp/tool-surface-parity.mjs +60 -0
  199. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  200. package/lib/mcp/tools/orchestration-run.mjs +197 -19
  201. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  202. package/lib/mcp/tools/project.mjs +25 -8
  203. package/lib/mcp/tools/provider-write.mjs +187 -0
  204. package/lib/mcp/tools/scope.mjs +0 -3
  205. package/lib/mcp/tools/skills.mjs +1 -1
  206. package/lib/mcp/tools/storage.mjs +2 -2
  207. package/lib/mcp/tools/web-search-governance.mjs +96 -0
  208. package/lib/mcp/tools/web-search.mjs +5 -76
  209. package/lib/mcp/transport/auth.mjs +238 -0
  210. package/lib/mcp/transport/http.mjs +136 -0
  211. package/lib/mcp/transport/mode.mjs +31 -0
  212. package/lib/mcp/transport/stdio.mjs +22 -0
  213. package/lib/mcp-catalog.json +4 -4
  214. package/lib/mcp-manager.mjs +34 -23
  215. package/lib/mcp-platform-config.mjs +53 -20
  216. package/lib/mode-capabilities.mjs +109 -0
  217. package/lib/model-router.mjs +42 -9
  218. package/lib/models/catalog.mjs +40 -1
  219. package/lib/net-guard.mjs +247 -0
  220. package/lib/observation-store.mjs +6 -2
  221. package/lib/ollama/provision-context.mjs +2 -1
  222. package/lib/opencode-config.mjs +22 -3
  223. package/lib/opencode-runtime-plugin.mjs +120 -2
  224. package/lib/oracle/actions.mjs +204 -10
  225. package/lib/oracle/cli.mjs +24 -3
  226. package/lib/oracle/daemon-entry.mjs +6 -0
  227. package/lib/oracle/dispatch.mjs +2 -1
  228. package/lib/oracle/execute.mjs +2 -2
  229. package/lib/oracle/gaps.mjs +3 -3
  230. package/lib/oracle/heartbeat.mjs +53 -0
  231. package/lib/oracle/read-model.mjs +69 -13
  232. package/lib/oracle/reconcile.mjs +3 -46
  233. package/lib/oracle/routing.mjs +37 -31
  234. package/lib/oracle/synthesize.mjs +1 -1
  235. package/lib/orchestration/classification.mjs +434 -0
  236. package/lib/orchestration/delegation-flow.mjs +132 -0
  237. package/lib/orchestration/flow-selection.mjs +418 -0
  238. package/lib/orchestration/gates.mjs +244 -0
  239. package/lib/orchestration/host-sampling.mjs +121 -0
  240. package/lib/orchestration/policy-constants.mjs +48 -0
  241. package/lib/orchestration/provider-outcome.mjs +197 -0
  242. package/lib/orchestration/readiness.mjs +114 -13
  243. package/lib/orchestration/run-store-postgres.mjs +32 -26
  244. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  245. package/lib/orchestration/run-store.mjs +25 -12
  246. package/lib/orchestration/runtime.mjs +508 -62
  247. package/lib/orchestration/store.mjs +87 -12
  248. package/lib/orchestration/trace-store.mjs +125 -0
  249. package/lib/orchestration/web-capability.mjs +60 -0
  250. package/lib/orchestration/worker-runtime.mjs +162 -0
  251. package/lib/orchestration/worker.mjs +763 -80
  252. package/lib/orchestration-policy.mjs +67 -1111
  253. package/lib/output-quality.mjs +61 -2
  254. package/lib/packs/cli.mjs +144 -0
  255. package/lib/packs/core-pack.mjs +112 -0
  256. package/lib/packs/enablement.mjs +187 -0
  257. package/lib/packs/index.mjs +23 -0
  258. package/lib/packs/loader.mjs +176 -0
  259. package/lib/packs/manifest-schema.mjs +58 -0
  260. package/lib/packs/prompts.mjs +120 -0
  261. package/lib/packs/validate.mjs +208 -0
  262. package/lib/path-policy.mjs +56 -0
  263. package/lib/plugin-registry.mjs +4 -5
  264. package/lib/policy/audit-gate.mjs +42 -0
  265. package/lib/policy/consumption-budget.mjs +149 -0
  266. package/lib/policy/engine.mjs +81 -6
  267. package/lib/policy/role-authority.mjs +89 -0
  268. package/lib/prompt-composer.js +19 -3
  269. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  270. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  271. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  272. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  273. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  274. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  275. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  276. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  277. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  278. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  279. package/lib/providers/contract.mjs +207 -0
  280. package/lib/providers/credential-bootstrap.mjs +7 -4
  281. package/lib/providers/credential-sources.mjs +14 -2
  282. package/lib/providers/directory/index.mjs +261 -0
  283. package/lib/providers/feedback/index.mjs +392 -0
  284. package/lib/providers/filter-audit.mjs +68 -0
  285. package/lib/providers/filter-schema.mjs +54 -0
  286. package/lib/providers/github/index.mjs +31 -0
  287. package/lib/providers/instance-config.mjs +215 -0
  288. package/lib/providers/op-locate.mjs +96 -0
  289. package/lib/providers/op-run.mjs +64 -9
  290. package/lib/providers/registry.mjs +26 -3
  291. package/lib/providers/secret-audit-wiring.mjs +41 -18
  292. package/lib/providers/secret-resolver.mjs +263 -31
  293. package/lib/publish-template.mjs +6 -3
  294. package/lib/publish-tooling.mjs +4 -0
  295. package/lib/publish.mjs +32 -4
  296. package/lib/queue/pg-queue.mjs +395 -0
  297. package/lib/reflect.mjs +2 -1
  298. package/lib/registry/assemble.mjs +28 -2
  299. package/lib/registry/catalog.mjs +6 -0
  300. package/lib/registry/consolidation.mjs +8 -1
  301. package/lib/registry/custom-scaffold.mjs +200 -0
  302. package/lib/registry/custom-schema.mjs +137 -0
  303. package/lib/registry/loader.mjs +15 -4
  304. package/lib/registry/manifests/format-engines.default.json +15 -0
  305. package/lib/registry/manifests/surface-map.default.json +46 -0
  306. package/lib/registry/retired-paths.mjs +1 -0
  307. package/lib/registry/surface-map.mjs +100 -50
  308. package/lib/registry/validate.mjs +3 -3
  309. package/lib/render-visual-check.mjs +240 -0
  310. package/lib/resources/budget.mjs +40 -17
  311. package/lib/roles/flavor-bindings.mjs +36 -14
  312. package/lib/roles/gateway.mjs +15 -8
  313. package/lib/roots.mjs +107 -0
  314. package/lib/runtime/uv-bootstrap.mjs +32 -6
  315. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  316. package/lib/runtime-env.mjs +5 -2
  317. package/lib/sandbox.mjs +1 -1
  318. package/lib/scheduler/index.mjs +8 -20
  319. package/lib/schema-infer.mjs +31 -2
  320. package/lib/scopes/lifecycle.mjs +29 -25
  321. package/lib/scopes/loader.mjs +49 -12
  322. package/lib/scopes/teams.mjs +1 -1
  323. package/lib/security/ingest-boundary.mjs +80 -0
  324. package/lib/security/recall-wrapper.mjs +99 -0
  325. package/lib/security/trust.mjs +132 -0
  326. package/lib/service-manager.mjs +93 -24
  327. package/lib/setup.mjs +41 -23
  328. package/lib/skills-apply.mjs +4 -2
  329. package/lib/specialists/postconditions.mjs +2 -2
  330. package/lib/state-root.mjs +147 -0
  331. package/lib/status.mjs +597 -6
  332. package/lib/storage/admin.mjs +84 -7
  333. package/lib/storage/backend-registry.mjs +73 -0
  334. package/lib/storage/backend.mjs +63 -9
  335. package/lib/storage/embeddings-openai.mjs +12 -2
  336. package/lib/storage/hybrid-query.mjs +11 -3
  337. package/lib/storage/retrieval-hardening.mjs +384 -0
  338. package/lib/storage/shared-memory.mjs +158 -0
  339. package/lib/storage/vector-client.mjs +69 -2
  340. package/lib/team/health.mjs +72 -0
  341. package/lib/telemetry/backends/local.mjs +9 -3
  342. package/lib/telemetry/client.mjs +3 -7
  343. package/lib/template-registry.mjs +10 -12
  344. package/lib/tenant/context.mjs +82 -0
  345. package/lib/tenant/isolation.mjs +129 -0
  346. package/lib/test-corpus-inventory.mjs +103 -2
  347. package/lib/uninstall/uninstall.mjs +78 -12
  348. package/lib/validator.mjs +4 -6
  349. package/lib/worker/entrypoint.mjs +2 -1
  350. package/lib/worker/run.mjs +2 -2
  351. package/lib/worker/trace.mjs +5 -11
  352. package/lib/workflow-state.mjs +52 -8
  353. package/lib/workflows/liveness.mjs +203 -0
  354. package/lib/workflows/loader.mjs +177 -0
  355. package/lib/workflows/manifest-schema.mjs +71 -0
  356. package/lib/workflows/surface-parity.mjs +118 -0
  357. package/lib/workflows/validate.mjs +127 -0
  358. package/lib/writes/control-plane.mjs +140 -0
  359. package/lib/writes/envelope.mjs +206 -0
  360. package/lib/writes/sent-log.mjs +86 -0
  361. package/lib/writes/write-intent.mjs +109 -0
  362. package/package.json +20 -7
  363. package/personas/construct.md +12 -3
  364. package/registry/agent-manifest.json +117 -0
  365. package/registry/capabilities.json +1987 -0
  366. package/rules/common/comments.md +1 -0
  367. package/schemas/brand-voice.schema.json +24 -0
  368. package/schemas/capability-registry.schema.json +72 -0
  369. package/schemas/certification-run.schema.json +130 -0
  370. package/schemas/demo-recording.schema.json +46 -0
  371. package/schemas/eval-dataset.schema.json +79 -0
  372. package/schemas/execution-capability-profile.schema.json +46 -0
  373. package/schemas/execution-policy.schema.json +114 -0
  374. package/schemas/improvement-proposal.schema.json +65 -0
  375. package/schemas/mcp-tool-output.schema.json +61 -0
  376. package/schemas/platform-capabilities.schema.json +83 -0
  377. package/schemas/project-config.schema.json +215 -0
  378. package/schemas/project-demo.schema.json +60 -0
  379. package/schemas/provider-behavior-matrix.schema.json +91 -0
  380. package/schemas/scope.schema.json +197 -0
  381. package/schemas/specialist-trace.schema.json +107 -0
  382. package/schemas/team.schema.json +99 -0
  383. package/schemas/unified-registry.schema.json +546 -0
  384. package/scripts/sync-specialists.mjs +336 -106
  385. package/skills/docs/adr-workflow.md +1 -1
  386. package/skills/docs/codebase-research-workflow.md +3 -3
  387. package/skills/docs/prd-workflow.md +5 -6
  388. package/skills/docs/runbook-workflow.md +2 -2
  389. package/skills/docs/user-research-workflow.md +3 -3
  390. package/skills/operating/fleet-health-routing.md +77 -0
  391. package/skills/roles/ai-engineer.md +1 -1
  392. package/skills/roles/architect.md +0 -1
  393. package/skills/roles/business-strategist.md +1 -1
  394. package/skills/roles/data-analyst.telemetry.md +1 -1
  395. package/skills/roles/data-engineer.md +1 -1
  396. package/skills/roles/data-engineer.pipeline.md +1 -1
  397. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  398. package/skills/roles/data-engineer.warehouse.md +1 -1
  399. package/skills/roles/designer.accessibility.md +1 -1
  400. package/skills/roles/designer.md +0 -1
  401. package/skills/roles/devil-advocate.md +1 -1
  402. package/skills/roles/docs-keeper.md +1 -1
  403. package/skills/roles/evaluator.md +1 -1
  404. package/skills/roles/explorer.md +1 -1
  405. package/skills/roles/platform-engineer.md +1 -1
  406. package/skills/roles/qa.ai-eval.md +1 -2
  407. package/skills/roles/qa.api-contract.md +0 -1
  408. package/skills/roles/qa.data-pipeline.md +0 -1
  409. package/skills/roles/qa.md +0 -1
  410. package/skills/roles/qa.web-ui.md +0 -1
  411. package/skills/roles/release-manager.md +1 -1
  412. package/skills/roles/researcher.md +0 -2
  413. package/skills/roles/reviewer.md +0 -3
  414. package/skills/roles/security.ai.md +1 -1
  415. package/skills/roles/security.legal-compliance.md +1 -1
  416. package/skills/roles/security.md +0 -1
  417. package/skills/roles/security.privacy.md +0 -1
  418. package/skills/roles/security.supply-chain.md +1 -1
  419. package/skills/roles/sre.md +1 -1
  420. package/skills/roles/test-automation.md +1 -1
  421. package/skills/roles/trace-reviewer.md +1 -1
  422. package/skills/roles/ux-researcher.md +1 -1
  423. package/specialists/artifact-manifest.json +36 -36
  424. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  425. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  426. package/specialists/org/contracts/any-to-debugger.json +7 -1
  427. package/specialists/org/contracts/any-to-designer.json +7 -1
  428. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  429. package/specialists/org/contracts/any-to-explorer.json +13 -4
  430. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  431. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  432. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  433. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  434. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  435. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  436. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  437. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  438. package/specialists/org/contracts/architect-to-operations.json +17 -4
  439. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  440. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  441. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  442. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  443. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  444. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  445. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  446. package/specialists/org/contracts/operations-to-user.json +53 -0
  447. package/specialists/org/contracts/operations-triage-output.json +53 -0
  448. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  449. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  450. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  451. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  452. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  453. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  454. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  455. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  456. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  457. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  458. package/specialists/org/contracts/user-to-construct.json +11 -4
  459. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  460. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  461. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  462. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  463. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  464. package/specialists/org/groups/engineering-group.json +2 -6
  465. package/specialists/org/groups/governance-group.json +2 -3
  466. package/specialists/org/groups/operations-group.json +5 -8
  467. package/specialists/org/groups/product-group.json +4 -8
  468. package/specialists/org/groups/quality-group.json +3 -7
  469. package/specialists/org/groups/strategy-group.json +6 -9
  470. package/specialists/org/models.json.example +8 -0
  471. package/specialists/org/scopes/creative.json +6 -1
  472. package/specialists/org/scopes/operations.json +7 -1
  473. package/specialists/org/scopes/research.json +6 -1
  474. package/specialists/org/scopes/rnd.json +7 -1
  475. package/specialists/org/specialists/cx-architect.json +27 -8
  476. package/specialists/org/specialists/cx-designer.json +22 -8
  477. package/specialists/org/specialists/cx-engineer.json +71 -7
  478. package/specialists/org/specialists/cx-operations.json +89 -15
  479. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  480. package/specialists/org/specialists/cx-product-manager.json +28 -9
  481. package/specialists/org/specialists/cx-qa.json +16 -6
  482. package/specialists/org/specialists/cx-researcher.json +41 -7
  483. package/specialists/org/specialists/cx-reviewer.json +61 -11
  484. package/specialists/org/specialists/cx-security.json +30 -10
  485. package/specialists/org/teams/design-team.json +3 -4
  486. package/specialists/org/teams/engineering-team.json +3 -10
  487. package/specialists/org/teams/governance-team.json +3 -5
  488. package/specialists/org/teams/operations-team.json +6 -12
  489. package/specialists/org/teams/product-management-team.json +2 -3
  490. package/specialists/org/teams/quality-team.json +4 -12
  491. package/specialists/org/teams/research-team.json +3 -3
  492. package/specialists/org/teams/strategy-team.json +7 -14
  493. package/specialists/prompts/cx-architect.md +20 -1
  494. package/specialists/prompts/cx-data-analyst.md +1 -1
  495. package/specialists/prompts/cx-designer.md +12 -4
  496. package/specialists/prompts/cx-engineer.md +15 -1
  497. package/specialists/prompts/cx-operations.md +14 -2
  498. package/specialists/prompts/cx-orchestrator.md +9 -5
  499. package/specialists/prompts/cx-product-manager.md +5 -1
  500. package/specialists/prompts/cx-qa.md +6 -2
  501. package/specialists/prompts/cx-researcher.md +27 -31
  502. package/specialists/prompts/cx-reviewer.md +19 -1
  503. package/specialists/prompts/cx-security.md +5 -1
  504. package/templates/distribution/construct-brand.typ +123 -59
  505. package/templates/distribution/construct-decision.typ +6 -3
  506. package/templates/distribution/construct-pdf.typ +11 -4
  507. package/templates/distribution/construct-prd.typ +6 -3
  508. package/templates/distribution/construct-research.typ +7 -4
  509. package/vendor/pandoc-ext/README.md +3 -0
  510. package/vendor/pandoc-ext/diagram.lua +687 -0
  511. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  512. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  513. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  514. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  515. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  516. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  517. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  518. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  519. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  520. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  521. package/specialists/org/specialists/cx-accessibility.json +0 -69
  522. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  523. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  524. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  525. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  526. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  527. package/specialists/org/specialists/cx-evaluator.json +0 -69
  528. package/specialists/org/specialists/cx-explorer.json +0 -69
  529. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  530. package/specialists/org/specialists/cx-oracle.json +0 -46
  531. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  532. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  533. package/specialists/org/specialists/cx-release-manager.json +0 -77
  534. package/specialists/org/specialists/cx-sre.json +0 -94
  535. package/specialists/org/specialists/cx-test-automation.json +0 -69
  536. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  537. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  538. package/specialists/org/teams/accessibility-team.json +0 -39
  539. package/specialists/org/teams/ux-research-team.json +0 -39
  540. package/specialists/prompts/cx-accessibility.md +0 -55
  541. package/specialists/prompts/cx-ai-engineer.md +0 -124
  542. package/specialists/prompts/cx-business-strategist.md +0 -58
  543. package/specialists/prompts/cx-data-engineer.md +0 -55
  544. package/specialists/prompts/cx-devil-advocate.md +0 -59
  545. package/specialists/prompts/cx-docs-keeper.md +0 -164
  546. package/specialists/prompts/cx-evaluator.md +0 -49
  547. package/specialists/prompts/cx-explorer.md +0 -71
  548. package/specialists/prompts/cx-legal-compliance.md +0 -58
  549. package/specialists/prompts/cx-oracle.md +0 -98
  550. package/specialists/prompts/cx-platform-engineer.md +0 -97
  551. package/specialists/prompts/cx-rd-lead.md +0 -59
  552. package/specialists/prompts/cx-release-manager.md +0 -54
  553. package/specialists/prompts/cx-sre.md +0 -111
  554. package/specialists/prompts/cx-test-automation.md +0 -55
  555. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  556. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -0,0 +1,50 @@
1
+ /**
2
+ * lib/intake/queue-registry.mjs — kind:'queue' provider resolution.
3
+ *
4
+ * Substrate owns the queue/run-store CONTRACT; git-queue is the zero-dependency
5
+ * DEFAULT provider and Postgres is an OPTIONAL provider. Selection is explicit:
6
+ * a concrete queue backend is chosen by resolving an installed kind:'queue'
7
+ * manifest through the extension registry (construct-9oi4.7.11, supersedes the
8
+ * ADR-0051 git-vs-postgres framing). No silent postgres->git alias exists — a
9
+ * caller that names a backend with no registered kind:'queue' provider gets a
10
+ * typed error, not a swallowed downgrade.
11
+ *
12
+ * Resolution surface:
13
+ * listQueueProviders({ rootDir, env }) → manifest[] of kind:'queue', merged
14
+ * across builtin/user/project tiers (project wins).
15
+ * findQueueProvider(backend, { rootDir, env }) → manifest | null for a backend id.
16
+ */
17
+
18
+ import { loadManifestsFromDir, mergeManifests, resolveManifestDirs } from '../extensions/loader.mjs';
19
+
20
+ /**
21
+ * The queue backend ids that are always resolvable without a registered
22
+ * manifest. git is the default provider; filesystem is the solo-mode default
23
+ * provider. Both ship in-tree, so they never require an extension manifest.
24
+ */
25
+ export const BUILTIN_QUEUE_BACKENDS = ['git', 'filesystem'];
26
+
27
+ // Load every kind:'queue' manifest across the three tiers and merge by id so a
28
+ // project- or user-level Postgres queue provider overrides a builtin of the
29
+ // same id. Only manifests declaring kind:'queue' are returned.
30
+
31
+ export function listQueueProviders({ rootDir = process.cwd(), env = process.env } = {}) {
32
+ const homeDir = env?.HOME || env?.USERPROFILE || undefined;
33
+ const dirs = resolveManifestDirs({ rootDir, homeDir });
34
+ const builtin = loadManifestsFromDir(dirs.builtin).manifests;
35
+ const user = loadManifestsFromDir(dirs.user).manifests;
36
+ const project = loadManifestsFromDir(dirs.project).manifests;
37
+ return mergeManifests(builtin, user, project).filter((m) => m.kind === 'queue');
38
+ }
39
+
40
+ /**
41
+ * findQueueProvider(backend, opts)
42
+ *
43
+ * Returns the kind:'queue' manifest whose id matches `backend`, or null when no
44
+ * such provider is registered. Callers use the null result to distinguish an
45
+ * unregistered backend (a hard, explicit error) from a resolvable one.
46
+ */
47
+ export function findQueueProvider(backend, { rootDir = process.cwd(), env = process.env } = {}) {
48
+ if (!backend) return null;
49
+ return listQueueProviders({ rootDir, env }).find((m) => m.id === backend) || null;
50
+ }
@@ -3,14 +3,20 @@
3
3
  *
4
4
  * The IntakeQueue contract carries the R&D intake packets from the embed
5
5
  * daemon (writer) to the agent in the user's editor (reader/processor).
6
- * Solo mode is backed by the filesystem under `.cx/intake/`; team and
7
- * enterprise modes are backed by Postgres with row-locked worker claims.
8
- * Callers never branch on backend they ask for a queue via
9
- * `createIntakeQueue(rootDir, env)` and use the same six methods.
6
+ * Substrate owns the queue CONTRACT; git-queue is the zero-dependency default
7
+ * kind:'queue' provider and Postgres is an optional kind:'queue' provider
8
+ * selected through the extension registry (construct-9oi4.7.11). Solo mode
9
+ * resolves to the filesystem provider under `.cx/intake/`; team and enterprise
10
+ * default to the Postgres provider. Callers never branch on backend — they ask for a
11
+ * queue via `createIntakeQueue(rootDir, env)` and use the same six methods.
12
+ * The returned instance also carries `.tenantId` (ADR-0057/A7: resolved once
13
+ * from CONSTRUCT_TENANT_ID/config, 'local' default outside enterprise mode)
14
+ * so a caller can stamp it onto entries without re-resolving it.
10
15
  *
11
16
  * Queue entry shape:
12
17
  * {
13
18
  * id, createdAt, status: 'pending'|'processed'|'skipped',
19
+ * tenantId?,
14
20
  * intake: { sourcePath, outputPath, characters, knowledgeSubdir },
15
21
  * triage: { intakeType, rdStage, primaryOwner, recommendedChain,
16
22
  * recommendedAction, risk, requiresApproval, confidence,
@@ -37,19 +43,60 @@ import path from 'node:path';
37
43
 
38
44
  import { FilesystemIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir } from './filesystem-queue.mjs';
39
45
  import { GitIntakeQueue } from './git-queue.mjs';
40
- import { getDeploymentMode } from '../deployment-mode.mjs';
46
+ import {
47
+ getDeploymentMode,
48
+ requireTeamCapabilityOrDegrade,
49
+ } from '../deployment-mode.mjs';
50
+ import { createSqlClient } from '../storage/backend.mjs';
51
+ import { findQueueProvider, BUILTIN_QUEUE_BACKENDS } from './queue-registry.mjs';
52
+ import { resolveTenantContext, TENANT_ID_ENV_KEY, DEFAULT_TENANT_ID } from '../tenant/context.mjs';
53
+ import { PostgresIntakeQueue } from '../queue/pg-queue.mjs';
41
54
 
42
55
  export { FilesystemIntakeQueue, GitIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir };
43
56
 
44
57
  export const INTAKE_QUEUE_BACKEND_ENV_KEY = 'CONSTRUCT_INTAKE_QUEUE_BACKEND';
45
58
  export const INTAKE_PROJECT_ENV_KEY = 'CONSTRUCT_PROJECT_NAME';
46
59
  export const INTAKE_TENANT_ENV_KEY = 'CONSTRUCT_TENANT_ID';
60
+ export const INTAKE_QUEUE_NAME_ENV_KEY = 'CONSTRUCT_INTAKE_QUEUE_NAME';
61
+ export const INTAKE_QUEUE_LEASE_SECONDS_ENV_KEY = 'CONSTRUCT_QUEUE_LEASE_SECONDS';
47
62
 
48
- function resolveBackend(env) {
63
+ // Re-exported so a caller can resolve the same tenant value the factory
64
+ // stamps on the queue without importing lib/tenant/context.mjs directly.
65
+ export { TENANT_ID_ENV_KEY, DEFAULT_TENANT_ID } from '../tenant/context.mjs';
66
+
67
+ // Thrown when a caller names a queue backend that is neither a builtin provider
68
+ // (git/filesystem) nor a registered kind:'queue' extension. The reframing
69
+ // removed the silent postgres->git alias, so an unknown backend is a hard,
70
+ // explicit error rather than a swallowed downgrade.
71
+
72
+ export class UnregisteredQueueProviderError extends Error {
73
+ constructor(backend) {
74
+ super(
75
+ `Queue backend '${backend}' has no registered kind:'queue' provider. ` +
76
+ `Builtin providers: ${BUILTIN_QUEUE_BACKENDS.join(', ')}. ` +
77
+ `Install a kind:'queue' extension (e.g. a Postgres provider) to select it.`,
78
+ );
79
+ this.name = 'UnregisteredQueueProviderError';
80
+ this.backend = backend;
81
+ }
82
+ }
83
+
84
+ // Selection resolves through the registry by kind. An explicit backend override
85
+ // wins; otherwise deployment mode picks the default provider (solo→filesystem,
86
+ // team/enterprise→postgres). A non-builtin backend must resolve to a registered
87
+ // kind:'queue' provider or the call fails explicitly.
88
+
89
+ function resolveBackend(env, rootDir) {
49
90
  const override = env?.[INTAKE_QUEUE_BACKEND_ENV_KEY];
50
- if (override === 'filesystem' || override === 'git') return override;
51
- const mode = getDeploymentMode(env);
52
- return mode === 'solo' ? 'filesystem' : 'git';
91
+ const requested = override && String(override).trim()
92
+ ? String(override).trim()
93
+ : (getDeploymentMode(env, { cwd: rootDir }) === 'solo' ? 'filesystem' : 'postgres');
94
+
95
+ if (BUILTIN_QUEUE_BACKENDS.includes(requested)) return { backend: requested, provider: null };
96
+
97
+ const provider = findQueueProvider(requested, { rootDir, env });
98
+ if (!provider) throw new UnregisteredQueueProviderError(requested);
99
+ return { backend: requested, provider };
53
100
  }
54
101
 
55
102
  function resolveProject(rootDir, env) {
@@ -58,20 +105,89 @@ function resolveProject(rootDir, env) {
58
105
  return path.basename(path.resolve(rootDir)).trim() || 'construct';
59
106
  }
60
107
 
108
+ // Resolves the tenant context (config + env, validated) for queue scoping.
109
+ // Reads CONSTRUCT_TENANT_ID directly here — the factory is a plumbing
110
+ // boundary, so it does not load construct.config.json itself and passes no
111
+ // config, which mirrors solo/team's config-optional path. Enterprise callers
112
+ // that need config-sourced tenantId should resolve it once at startup via
113
+ // lib/deployment-mode.mjs#validateTenantAtStartup and pass the result through
114
+ // opts.tenantId instead of relying on this fallback.
115
+
116
+ export function resolveIntakeTenantId(env = process.env, mode = getDeploymentMode(env)) {
117
+ return resolveTenantContext({ env, mode }).tenantId;
118
+ }
119
+
61
120
  /**
62
121
  * Create an IntakeQueue instance for the given project root.
63
122
  * Backend selection: CONSTRUCT_INTAKE_QUEUE_BACKEND override wins; otherwise
64
- * solo mode → filesystem, team/enterprise → git. The git
65
- * backend uses the filesystem and git for state synchronization.
123
+ * solo mode → filesystem, team/enterprise → postgres. A backend that is neither builtin nor a registered
124
+ * kind:'queue' provider throws UnregisteredQueueProviderError there is no
125
+ * silent postgres->git alias.
126
+ *
127
+ * The returned instance carries a `.tenantId` (opts.tenantId wins, otherwise
128
+ * resolved from CONSTRUCT_TENANT_ID/config via resolveIntakeTenantId) so a
129
+ * caller building a queue entry can stamp tenantId without re-deriving it —
130
+ * the queue classes themselves stay tenant-agnostic (LMCP-H1; tenant-scoped
131
+ * storage is LMCP-H4, not this bead).
66
132
  */
67
133
  export function createIntakeQueue(rootDir, env = process.env, opts = {}) {
68
- const backend = opts.backend || resolveBackend(env);
69
- if (backend === 'filesystem') return new FilesystemIntakeQueue(rootDir);
70
- if (backend === 'git' || backend === 'postgres') {
71
- // 'postgres' aliases to 'git' so existing configs keep resolving
134
+ const resolved = opts.backend
135
+ ? (BUILTIN_QUEUE_BACKENDS.includes(opts.backend)
136
+ ? { backend: opts.backend, provider: null }
137
+ : (findQueueProvider(opts.backend, { rootDir, env })
138
+ ? { backend: opts.backend, provider: findQueueProvider(opts.backend, { rootDir, env }) }
139
+ : (() => { throw new UnregisteredQueueProviderError(opts.backend); })()))
140
+ : resolveBackend(env, rootDir);
72
141
 
142
+ const tenantId = opts.tenantId ?? resolveIntakeTenantId(env, getDeploymentMode(env, { cwd: rootDir }));
143
+
144
+ if (resolved.backend === 'filesystem') {
145
+ const queue = new FilesystemIntakeQueue(rootDir);
146
+ queue.tenantId = tenantId;
147
+ return queue;
148
+ }
149
+ if (resolved.backend === 'git') {
73
150
  const project = opts.project ?? resolveProject(rootDir, env);
74
- return new GitIntakeQueue({ project, rootDir });
151
+ const queue = new GitIntakeQueue({ project, rootDir });
152
+ queue.tenantId = tenantId;
153
+ return queue;
75
154
  }
76
- throw new Error(`Unknown intake queue backend: ${backend}`);
155
+
156
+ if (resolved.backend === 'postgres' || resolved.provider?.id === 'postgres') {
157
+ const sql = opts.sql ?? createSqlClient(env);
158
+ if (!sql) {
159
+ const mode = getDeploymentMode(env, { cwd: rootDir });
160
+ if (mode === 'solo') {
161
+ throw new Error(
162
+ `Queue backend '${resolved.backend}' requires DATABASE_URL or CONSTRUCT_DATABASE_URL; ` +
163
+ `no Postgres SQL client is configured.`,
164
+ );
165
+ }
166
+ requireTeamCapabilityOrDegrade('postgres-queue', false, env, { cwd: rootDir });
167
+ const queue = new GitIntakeQueue({ project: opts.project ?? resolveProject(rootDir, env), rootDir });
168
+ queue.tenantId = tenantId;
169
+ queue.backend = 'git';
170
+ queue.requestedBackend = resolved.backend;
171
+ queue.degraded = true;
172
+ queue.degradedReason = 'postgres-unavailable';
173
+ return queue;
174
+ }
175
+ requireTeamCapabilityOrDegrade('postgres-queue', true, env, { cwd: rootDir });
176
+ return new PostgresIntakeQueue({
177
+ sql,
178
+ project: opts.project ?? resolveProject(rootDir, env),
179
+ tenantId,
180
+ queueName: opts.queueName ?? env?.[INTAKE_QUEUE_NAME_ENV_KEY] ?? 'intake',
181
+ leaseSeconds: opts.leaseSeconds ?? env?.[INTAKE_QUEUE_LEASE_SECONDS_ENV_KEY],
182
+ });
183
+ }
184
+
185
+ // A registered kind:'queue' provider was selected but no in-tree instantiation
186
+ // path exists yet. The selection is honest — it names the resolved provider
187
+ // rather than silently downgrading to git.
188
+
189
+ throw new Error(
190
+ `Queue backend '${resolved.backend}' resolves to registered kind:'queue' provider ` +
191
+ `'${resolved.provider?.id}' but no in-tree instantiation path is wired yet.`,
192
+ );
77
193
  }
@@ -13,12 +13,14 @@
13
13
  import { createIntakeQueue } from './queue.mjs';
14
14
  import { formatTriageLine } from './classify.mjs';
15
15
  import { isBrokered } from '../mcp/broker.mjs';
16
+ import { getDeploymentMode } from '../deployment-mode.mjs';
16
17
  import { getRebrand } from '../scopes/rebrand.mjs';
17
18
  import { homedir as osHomedir } from 'node:os';
18
19
  import { readLastTick } from '../oracle/index.mjs';
19
20
  import { listPending } from '../oracle/actions.mjs';
20
21
  import { readLatestVerdict } from '../oracle/verdicts.mjs';
21
22
  import { isVerdictOnlyGap } from '../oracle/policy.mjs';
23
+ import { heartbeatFreshness, formatAge } from '../oracle/heartbeat.mjs';
22
24
 
23
25
  export function buildIntakePrelude({ cwd, env = process.env } = {}) {
24
26
  if (!cwd) return '';
@@ -41,9 +43,9 @@ export function buildIntakePrelude({ cwd, env = process.env } = {}) {
41
43
  }
42
44
  }
43
45
 
44
- export function buildBrokerStatusLine({ env = process.env } = {}) {
45
- const active = isBrokered(env);
46
- const mode = env?.CONSTRUCT_DEPLOYMENT_MODE || 'solo';
46
+ export function buildBrokerStatusLine({ env = process.env, cwd } = {}) {
47
+ const active = isBrokered(env, { cwd });
48
+ const mode = getDeploymentMode(env, { cwd });
47
49
  if (!active) {
48
50
  return `MCP broker: off · deployment mode: ${mode} (set CONSTRUCT_MCP_BROKER=on to engage in solo mode).`;
49
51
  }
@@ -52,7 +54,7 @@ export function buildBrokerStatusLine({ env = process.env } = {}) {
52
54
 
53
55
  export function buildSessionPrelude({ cwd, env = process.env } = {}) {
54
56
  const intake = buildIntakePrelude({ cwd, env });
55
- const broker = buildBrokerStatusLine({ env });
57
+ const broker = buildBrokerStatusLine({ env, cwd });
56
58
  const oracle = buildOraclePrelude({ cwd, env });
57
59
  if (!intake && !broker && !oracle) return '';
58
60
  const parts = [];
@@ -62,6 +64,26 @@ export function buildSessionPrelude({ cwd, env = process.env } = {}) {
62
64
  return parts.join('\n\n');
63
65
  }
64
66
 
67
+ /**
68
+ * lib/oracle/synthesize.mjs renders gap detail text against a 24h lookback
69
+ * window ("in the last 24h"); a verdict older than that window describes a
70
+ * period disjoint from the present, so the same threshold marks it stale.
71
+ * Missing/unparseable `at` resolves to Infinity (stale, not fresh) rather
72
+ * than defaulting to zero age.
73
+ */
74
+ const VERDICT_STALE_MS = 24 * 60 * 60 * 1000;
75
+
76
+ function verdictFreshness(verdict, now = Date.now()) {
77
+ const at = verdict?.at;
78
+ const parsed = at ? Date.parse(at) : NaN;
79
+ const ageMs = Number.isFinite(parsed) ? now - parsed : Infinity;
80
+ return {
81
+ ageMs,
82
+ stale: !Number.isFinite(ageMs) || ageMs > VERDICT_STALE_MS,
83
+ asOf: Number.isFinite(parsed) ? new Date(parsed).toISOString() : null,
84
+ };
85
+ }
86
+
65
87
  /**
66
88
  * Oracle verdict + pending queue for session-start (mirror doctor warnings).
67
89
  */
@@ -75,7 +97,19 @@ export function buildOraclePrelude({ cwd, env = process.env, homeDir } = {}) {
75
97
  const pending = listPending(cwd).filter((p) => p.status === 'pending');
76
98
  const v = verdict?.verdict ?? last?.verdict ?? 'unknown';
77
99
  if (v === 'healthy' && pending.length === 0) return '';
78
- const lines = [`\n## Oracle overseer · verdict: **${v}**`];
100
+ const source = verdict ?? last;
101
+ const { ageMs, stale, asOf } = verdictFreshness(source);
102
+ const verdictHead = v === 'unknown'
103
+ ? `\n## Oracle overseer · verdict: **${v}**`
104
+ : `\n## Oracle overseer · verdict: **${v}** (as of ${asOf ?? 'unknown'}, ${formatAge(ageMs)} ago)`;
105
+ const lines = [verdictHead];
106
+ if (v !== 'unknown' && stale) {
107
+ lines.push(`- STALE: verdict is older than ${formatAge(VERDICT_STALE_MS)}; gap details below describe the verdict date, not now.`);
108
+ }
109
+ const heartbeat = heartbeatFreshness(last);
110
+ if (heartbeat.stale && pending.length > 0) {
111
+ lines.push(`- [high] oracle-producer-stalled: no oracle tick in ${formatAge(heartbeat.ageMs)} while ${pending.length} approval(s) wait — the daemon looks stalled, not idle; run \`construct oracle status\` / \`construct oracle start\`.`);
112
+ }
79
113
  const gapSource = verdict?.gaps ?? last?.gaps ?? [];
80
114
  const top = gapSource.filter((g) => g.severity === 'high').slice(0, 3);
81
115
  for (const g of top) {
@@ -83,7 +117,7 @@ export function buildOraclePrelude({ cwd, env = process.env, homeDir } = {}) {
83
117
  const hint = g.id === 'beads-hygiene'
84
118
  ? 'run `construct beads drift`'
85
119
  : g.id === 'workflow-misaligned'
86
- ? 'run `construct workflow init` or `construct init`'
120
+ ? 'run `construct workflow new` or `construct init`'
87
121
  : 'see `construct oracle gaps`';
88
122
  lines.push(`- [${g.severity}] ${g.id} (verdict-only): ${g.detail} — ${hint}`);
89
123
  } else {
@@ -102,7 +136,7 @@ export function buildOraclePrelude({ cwd, env = process.env, homeDir } = {}) {
102
136
 
103
137
  export function readOracleDockState({ cwd, env = process.env, homeDir } = {}) {
104
138
  if (!cwd || env.CONSTRUCT_ORACLE === 'off' || env.CONSTRUCT_ORACLE === '0') {
105
- return { visible: false, verdict: null, pendingCount: 0, topGaps: [], summary: '' };
139
+ return { visible: false, verdict: null, pendingCount: 0, topGaps: [], summary: '', asOf: null, stale: false, producerStalled: false, producerAgeMs: null };
106
140
  }
107
141
  try {
108
142
  const home = homeDir ?? osHomedir();
@@ -113,23 +147,38 @@ export function readOracleDockState({ cwd, env = process.env, homeDir } = {}) {
113
147
  const gapSource = verdictDoc?.gaps ?? last?.gaps ?? [];
114
148
  const topGaps = gapSource.filter((g) => g.severity === 'high').slice(0, 3);
115
149
  const visible = verdict !== 'healthy' || pending.length > 0;
150
+ const { stale, asOf } = verdictFreshness(verdictDoc ?? last);
151
+ const heartbeat = heartbeatFreshness(last);
152
+ const producerStalled = heartbeat.stale && pending.length > 0;
116
153
  const parts = [`verdict ${verdict}`];
117
154
  if (pending.length) parts.push(`${pending.length} pending`);
155
+ if (verdict !== 'unknown' && stale) parts.push('STALE');
156
+ if (producerStalled) parts.push('PRODUCER STALLED');
118
157
  return {
119
158
  visible,
120
159
  verdict,
121
160
  pendingCount: pending.length,
122
161
  topGaps,
123
162
  summary: parts.join(' · '),
163
+ asOf,
164
+ stale: verdict !== 'unknown' && stale,
165
+ producerStalled,
166
+ producerAgeMs: heartbeat.ageMs,
124
167
  };
125
168
  } catch {
126
- return { visible: false, verdict: null, pendingCount: 0, topGaps: [], summary: '' };
169
+ return { visible: false, verdict: null, pendingCount: 0, topGaps: [], summary: '', asOf: null, stale: false, producerStalled: false, producerAgeMs: null };
127
170
  }
128
171
  }
129
172
 
130
173
  export function formatOracleDockDetail(state) {
131
174
  if (!state?.visible) return 'Oracle: healthy — no pending approvals.';
132
175
  const lines = [`Oracle overseer · ${state.summary}`];
176
+ if (state.stale) {
177
+ lines.push(` STALE: verdict as of ${state.asOf ?? 'unknown'}; gap details describe the verdict date, not now.`);
178
+ }
179
+ if (state.producerStalled) {
180
+ lines.push(` PRODUCER STALLED: no oracle tick in ${formatAge(state.producerAgeMs)} while ${state.pendingCount} approval(s) wait — the daemon may be down.`);
181
+ }
133
182
  for (const g of state.topGaps || []) {
134
183
  if (isVerdictOnlyGap(g)) {
135
184
  lines.push(` [${g.severity}] ${g.id} (verdict-only): ${g.detail}`);
@@ -19,7 +19,7 @@ import { join } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
20
  import { spawnSync } from 'node:child_process';
21
21
  import { configDir } from '../config/xdg.mjs';
22
- import { resolveOpRef } from '../providers/secret-resolver.mjs';
22
+ import { resolveOpRef, hasSecret, hasAnySecret, peekRawCredential } from '../providers/secret-resolver.mjs';
23
23
 
24
24
  // ── Multi-method auth helpers ────────────────────────────────────────────
25
25
 
@@ -588,8 +588,13 @@ export function tagIntakeWithExternalRef(rootDir, intakeId, system, externalUrl,
588
588
  }
589
589
 
590
590
  /**
591
- * Synchronous credential resolver for use in constructors and sync contexts.
592
- * Mirrors the async resolveCredential above. Checks env → .env → config.env → shell rc.
591
+ * Credential resolver for the actual publish paths (createGitHubIssue,
592
+ * createJiraTicket, publishArtifactToConfluence) that need a real, materialized
593
+ * value to make an API call. Checks env → .env → config.env → shell rc,
594
+ * resolving a 1Password op:// reference through resolveOpRef when the rc tier
595
+ * carries one. Not for status/detection — detectIntegrationConfig below uses
596
+ * hasSecret/hasAnySecret/peekRawCredential instead, which never resolve op:// or
597
+ * source a shell rc file (ADR-0049 §2: presence-first in short-lived paths).
593
598
  */
594
599
  function resolveCredentialSync(varName, homeDir) {
595
600
  if (process.env[varName]) return process.env[varName];
@@ -640,126 +645,71 @@ function resolveCredentialSync(varName, homeDir) {
640
645
  // ── Configuration detection ──────────────────────────────────────────────
641
646
 
642
647
  /**
643
- * Check which integrations are configured.
644
- * Scopes environment, .env files, ~/.construct/config.env, and shell rc
645
- * files (including 1Password op:// refs).
648
+ * Check which integrations are configured. A status/detection surface must
649
+ * never materialize a credential (ADR-0049 §2: "never to merely list or
650
+ * check") this reports presence by shape only: env var set, a project/config/
651
+ * home .env entry exists, or a shell rc `export VAR=...` line matches. It never
652
+ * calls resolveOpRef and never sources a shell rc file (contrast
653
+ * resolveCredentialSync above, used only when an actual API call needs the
654
+ * real value). `gh auth status` / `gh repo view` are read-only CLI queries,
655
+ * not secret materialization — gh manages its own token internally and never
656
+ * hands Construct a plaintext value here.
646
657
  *
647
658
  * @param {object} [opts]
648
659
  * @param {string} [opts.homeDir]
649
- * @returns {{ github: boolean, jira: boolean, confluence: boolean, githubToken: boolean, jiraToken: boolean, confluenceToken: boolean }}
660
+ * @returns {{ github: boolean, jira: boolean, confluence: boolean, githubMethod: string|null, githubRepo: string|null, jiraHost: string|null, confluenceSpace: string|null }}
650
661
  */
651
662
  export function detectIntegrationConfig({ homeDir } = {}) {
652
663
  const hd = homeDir || homedir();
653
664
 
654
- // GitHub — try gh CLI first, then env vars
655
- const ghAuth = resolveGitHubAuth(hd);
656
-
657
- // Jira
658
- const jiraHost = resolveCredentialSync('JIRA_HOST', hd);
659
- const jiraUser = resolveCredentialSync('JIRA_USER', hd);
660
- const jiraToken = resolveCredentialSync('JIRA_API_TOKEN', hd);
661
- const jiraProject = resolveCredentialSync('JIRA_PROJECT', hd);
662
-
663
- // Confluence
664
- const confHost = resolveCredentialSync('CONFLUENCE_HOST', hd);
665
- const confUser = resolveCredentialSync('CONFLUENCE_USER', hd);
666
- const confToken = resolveCredentialSync('CONFLUENCE_API_TOKEN', hd);
667
- const confSpace = resolveCredentialSync('CONFLUENCE_SPACE', hd);
668
-
669
- return {
670
- github: ghAuth.method === 'gh' || Boolean(ghAuth.token && ghAuth.repo),
671
- githubMethod: ghAuth.method || null,
672
- githubRepo: ghAuth.repo || null,
673
- jira: Boolean(jiraHost && jiraUser && jiraToken && jiraProject),
674
- jiraHost: jiraHost || null,
675
- confluence: Boolean(confHost && confUser && confToken && confSpace),
676
- confluenceSpace: confSpace || null,
677
- };
678
- }
679
-
680
- /**
681
- * Resolve a credential from env, .env files, config.env, or shell rc files.
682
- * Mirrors the approach in lib/health-check.mjs.
683
- */
684
- function resolveCredential(varName, homeDir) {
685
- if (process.env[varName]) return process.env[varName];
686
-
687
- // Project .env
665
+ // GitHub
666
+ let ghCliAuthenticated = false;
688
667
  try {
689
- const projectEnv = join(process.cwd(), '.env');
690
- if (existsSync(projectEnv)) {
691
- const content = readFileSync(projectEnv, 'utf8');
692
- const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
693
- if (m) return m[1].trim();
694
- }
695
- } catch { /* skip */ }
668
+ const status = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 5000 });
669
+ ghCliAuthenticated = status.status === 0;
670
+ } catch { /* gh not available */ }
696
671
 
697
- // ~/.env
698
- try {
699
- const homeEnv = join(homeDir, '.env');
700
- if (existsSync(homeEnv)) {
701
- const content = readFileSync(homeEnv, 'utf8');
702
- const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
703
- if (m) return m[1].trim();
704
- }
705
- } catch { /* skip */ }
672
+ const githubTokenPresent = hasAnySecret(['GITHUB_TOKEN', 'GH_TOKEN'], { home: hd });
673
+ let githubRepo = process.env.GITHUB_REPO || peekRawCredential('GITHUB_REPO', { home: hd })?.raw || null;
674
+ let githubMethod = null;
706
675
 
707
- // config.env in the XDG config dir
708
- try {
709
- const cfgPath = join(configDir(homeDir), 'config.env');
710
- if (existsSync(cfgPath)) {
711
- const content = readFileSync(cfgPath, 'utf8');
712
- const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
713
- if (m) return m[1].trim();
676
+ if (ghCliAuthenticated) {
677
+ githubMethod = 'gh';
678
+ if (!githubRepo) {
679
+ try {
680
+ const remote = spawnSync('gh', ['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner'], { encoding: 'utf8', timeout: 5000 });
681
+ if (remote.status === 0 && remote.stdout?.trim()) githubRepo = remote.stdout.trim();
682
+ } catch { /* gh not available */ }
714
683
  }
715
- } catch { /* skip */ }
684
+ } else if (githubTokenPresent) {
685
+ githubMethod = 'env';
686
+ }
716
687
 
717
- // Shell rc files (including 1Password op:// refs and command substitutions)
718
- try {
719
- const shellFiles = [join(homeDir, '.zshrc'), join(homeDir, '.bashrc'), join(homeDir, '.bash_profile'), join(homeDir, '.profile')];
720
- for (const rcPath of shellFiles) {
721
- if (!existsSync(rcPath)) continue;
722
- try {
723
- const content = readFileSync(rcPath, 'utf8');
724
- // Direct export (simple values only, not command substitutions)
725
- const directRe = new RegExp(`^\\s*export\\s+${varName}="?([^"'\\$\\(]+)"?$`, 'm');
726
- const directMatch = content.match(directRe);
727
- if (directMatch) return directMatch[1].trim();
728
-
729
- // op:// ref — try 1Password CLI
730
- const opRe = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
731
- const opMatch = content.match(opRe);
732
- if (opMatch) {
733
- // op read failures often mean the rc-file field name doesn't match the
734
- // actual 1Password item field; surface them under the debug flag.
735
- try {
736
- return resolveOpRef(opMatch[1]);
737
- } catch (err) {
738
- if (process.env.CONSTRUCT_DEBUG_CREDENTIALS === '1') {
739
- process.stderr.write(`[credentials] op read failed for ${varName}: ${String(err.message).slice(0, 200)}\n`);
740
- }
741
- }
742
- }
688
+ // Jira
689
+ const jiraConfigured = Boolean(
690
+ hasSecret('JIRA_HOST', { home: hd })
691
+ && hasSecret('JIRA_USER', { home: hd })
692
+ && hasSecret('JIRA_API_TOKEN', { home: hd })
693
+ && hasSecret('JIRA_PROJECT', { home: hd }),
694
+ );
695
+ const jiraHost = peekRawCredential('JIRA_HOST', { home: hd })?.raw || null;
743
696
 
744
- // If the value uses command substitution ($(...)), try evaluating via
745
- // a shell that sources the rc file. This catches any $(...) pattern
746
- // including op read with different field names or other secret managers.
747
- const cmdSubRe = new RegExp(`^export\\s+${varName}="?\\$\\((.+)\\)"?`, 'm');
748
- const cmdMatch = content.match(cmdSubRe);
749
- if (cmdMatch) {
750
- try {
751
- const shellResult = spawnSync('zsh', ['-c', `source "${rcPath}" 2>/dev/null; echo "\${${varName}}"`], {
752
- encoding: 'utf8', timeout: 5000,
753
- });
754
- if (shellResult.status === 0 && shellResult.stdout?.trim()) {
755
- const val = shellResult.stdout.trim().split('\n').pop()?.trim();
756
- if (val && !val.includes('ERROR') && !val.includes('could not read')) return val;
757
- }
758
- } catch { /* shell eval failed */ }
759
- }
760
- } catch { /* skip unreadable */ }
761
- }
762
- } catch { /* skip */ }
697
+ // Confluence
698
+ const confluenceConfigured = Boolean(
699
+ hasSecret('CONFLUENCE_HOST', { home: hd })
700
+ && hasSecret('CONFLUENCE_USER', { home: hd })
701
+ && hasSecret('CONFLUENCE_API_TOKEN', { home: hd })
702
+ && hasSecret('CONFLUENCE_SPACE', { home: hd }),
703
+ );
704
+ const confluenceSpace = peekRawCredential('CONFLUENCE_SPACE', { home: hd })?.raw || null;
763
705
 
764
- return null;
706
+ return {
707
+ github: ghCliAuthenticated || Boolean(githubTokenPresent && githubRepo),
708
+ githubMethod,
709
+ githubRepo,
710
+ jira: jiraConfigured,
711
+ jiraHost,
712
+ confluence: confluenceConfigured,
713
+ confluenceSpace,
714
+ };
765
715
  }
@@ -27,6 +27,7 @@
27
27
  import crypto from 'node:crypto';
28
28
  import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig } from './model-router.mjs';
29
29
  import { getUserEnvPath } from './env-config.mjs';
30
+ import { extractOpRef, hasSecret, resolveOpRefAsync, resolveSecretAsync } from './providers/secret-resolver.mjs';
30
31
  import { logIntentVerification } from './telemetry/intent-verifications.mjs';
31
32
 
32
33
  export const INTENT_VERIFY_TIMEOUT_MS = 3000;
@@ -66,12 +67,49 @@ export function resetCache() {
66
67
  CACHE.clear();
67
68
  }
68
69
 
70
+ async function resolveSecretOrNull(varName, { env = process.env, allowAmbient = true } = {}) {
71
+ try {
72
+ return await resolveSecretAsync(varName, { env, allowAmbient });
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ async function materializeConfiguredKey(rawValue) {
79
+ if (typeof rawValue !== 'string' || rawValue.length === 0) return null;
80
+ const ref = extractOpRef(rawValue);
81
+ try {
82
+ return ref ? await resolveOpRefAsync(ref) : rawValue;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ async function resolveOpenRouterApiKey() {
89
+ const envKey = await resolveSecretOrNull('OPENROUTER_API_KEY');
90
+ if (envKey) return envKey;
91
+ try {
92
+ return await materializeConfiguredKey(readOpenRouterApiKeyFromOpenCodeConfig());
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ async function resolveAnthropicApiKey() {
99
+ return resolveSecretOrNull('ANTHROPIC_API_KEY');
100
+ }
101
+
69
102
  function isApiKeyConfigured() {
70
- if (process.env.ANTHROPIC_API_KEY) return true;
71
- if (process.env.OPENROUTER_API_KEY) return true;
103
+ // Presence gate only — hasSecret detects a configured key (including an op://
104
+ // reference) without materializing it, so deciding whether to run the background
105
+ // intent verifier never spawns `op read` or a 1Password biometric prompt on the
106
+ // routing hot path. Materialization happens lazily in defaultModelCaller when a
107
+ // verification call is actually made.
108
+ if (hasSecret('ANTHROPIC_API_KEY')) return true;
109
+ if (hasSecret('OPENROUTER_API_KEY')) return true;
72
110
  try {
73
111
  if (readOpenRouterApiKeyFromOpenCodeConfig()) return true;
74
- } catch { /* ignore */ }
112
+ } catch { /* OpenCode provider config is optional */ }
75
113
  return false;
76
114
  }
77
115
 
@@ -115,8 +153,8 @@ async function fetchWithTimeout(url, options, timeoutMs = INTENT_VERIFY_TIMEOUT_
115
153
  }
116
154
 
117
155
  async function defaultModelCaller({ system, user }) {
118
- const orKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
119
- const anthropicKey = process.env.ANTHROPIC_API_KEY;
156
+ const orKey = await resolveOpenRouterApiKey();
157
+ const anthropicKey = await resolveAnthropicApiKey();
120
158
  if (!orKey && !anthropicKey) throw new Error('no model api key configured');
121
159
 
122
160
  const envPath = getUserEnvPath();