@geraldmaron/construct 1.4.2 → 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 (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 +55 -38
  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 +261 -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 +103 -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
@@ -31,17 +31,21 @@ import { dirname, join } from 'node:path';
31
31
  import { loadEmbedConfig } from './config.mjs';
32
32
  import { Scheduler } from './scheduler.mjs';
33
33
  import { rotateIfOversized } from '../logging/rotate.mjs';
34
+ import { resolveStatePath } from '../state-root.mjs';
34
35
  import { SnapshotEngine, renderMarkdown } from './snapshot.mjs';
35
36
  import { ApprovalQueue } from './approval-queue.mjs';
36
37
  import { dispatchOutputs } from './output.mjs';
37
38
  import { AuthorityGuard } from './authority-guard.mjs';
38
39
  import { ProviderRegistry } from './providers/registry.mjs';
39
40
  import { addObservation, searchObservations } from '../observation-store.mjs';
41
+ import { filterHash, matchesFilter, validateFilterConfig } from '../providers/contract.mjs';
42
+ import { recordFilterAudit } from '../providers/filter-audit.mjs';
40
43
  import { listSessions, loadSession } from '../session-store.mjs';
41
44
  import { syncEvalDatasets } from '../telemetry/eval-datasets.mjs';
42
45
  import { runLLMJudgeEvaluations } from '../telemetry/llm-judge.mjs';
43
46
  import { InboxWatcher } from './inbox.mjs';
44
47
  import { InboxLiveWatcher } from './inbox-live-watcher.mjs';
48
+ import { registerEmbedCapabilityJobs } from './capability-jobs.mjs';
45
49
  import { generateRoadmap } from './roadmap.mjs';
46
50
  import { runDocsLifecycle } from './docs-lifecycle.mjs';
47
51
  import { emitEmbedNotification, notifySlack } from './notifications.mjs';
@@ -49,6 +53,7 @@ import { intentToCategory } from './providers/slack.mjs';
49
53
  import { runTelemetrySetup } from '../telemetry/setup.mjs';
50
54
  import { emitTraceEvent, newTraceId, newSpanId } from '../worker/trace.mjs';
51
55
  import { doctorRoot } from '../config/xdg.mjs';
56
+ import { resolveNonNegativeSetting } from '../env-config.mjs';
52
57
 
53
58
  /**
54
59
  * Log a non-fatal error to stderr. Use instead of empty catch {} blocks
@@ -117,7 +122,7 @@ function resolveLockPath(env = process.env) {
117
122
  }
118
123
 
119
124
  function resolveDaemonStatePath(env = process.env) {
120
- return join(resolveRootDir(env), '.cx', 'runtime', 'embed-daemon.json');
125
+ return resolveStatePath(resolveRootDir(env), 'runtime', 'embed-daemon.json');
121
126
  }
122
127
 
123
128
  // ─── Self-repair helpers ────────────────────────────────────────────────────
@@ -221,12 +226,101 @@ function distillRecentSessions(rootDir) {
221
226
  return distilled;
222
227
  }
223
228
 
229
+ // ─── ADR-0060 filter enforcement (LMCP-B11) ─────────────────────────────────
230
+
231
+ /**
232
+ * Match each snapshot section back to the source record that produced it.
233
+ * Sections are pushed in config.sources order but a source with no
234
+ * registered provider is skipped upstream (lib/embed/snapshot.mjs), so
235
+ * position alone is unreliable — walk both lists in lockstep, consuming one
236
+ * unmatched source per section by provider name.
237
+ */
238
+ function matchSourceForSections(sources) {
239
+ const remaining = [...(sources ?? [])];
240
+ return (providerName) => {
241
+ const idx = remaining.findIndex((s) => s.provider === providerName);
242
+ if (idx === -1) return null;
243
+ return remaining.splice(idx, 1)[0];
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Enforce the ADR-0060 filter block on every snapshot section before any
249
+ * item reaches observation distillation. Validates the source's `filter`
250
+ * config first (fail closed: an unknown/invalid key throws, the section's
251
+ * items are dropped entirely, and the failure is recorded as a source
252
+ * error visible in the snapshot). Valid filters run the plane-side
253
+ * `matchesFilter` predicate over every item — the mandatory backstop even
254
+ * when the provider already pushed the filter down server-side — and a
255
+ * `{provider, instance, filterHash, matched, dropped}` audit line is
256
+ * appended per section, matched or not.
257
+ *
258
+ * Mutates `snapshot.sections[].items` in place and appends to
259
+ * `snapshot.errors` on a filter config failure. Returns nothing.
260
+ */
261
+ export function enforceSectionFilters(rootDir, snapshot, sources) {
262
+ const nextSourceFor = matchSourceForSections(sources);
263
+
264
+ for (const section of snapshot.sections ?? []) {
265
+ const source = nextSourceFor(section.provider);
266
+ const filter = source?.filter ?? null;
267
+ const fetched = section.items?.length ?? 0;
268
+
269
+ if (filter == null) {
270
+ recordFilterAudit(rootDir, {
271
+ provider: section.provider,
272
+ instance: source?.instance ?? null,
273
+ filterHash: null,
274
+ pushdown: false,
275
+ fetched,
276
+ matched: fetched,
277
+ });
278
+ continue;
279
+ }
280
+
281
+ try {
282
+ validateFilterConfig(section.provider, filter);
283
+ } catch (err) {
284
+ section.items = [];
285
+ snapshot.errors = snapshot.errors ?? [];
286
+ snapshot.errors.push({ source: section.provider, error: `filter config: ${err.message}` });
287
+ recordFilterAudit(rootDir, {
288
+ provider: section.provider,
289
+ instance: source?.instance ?? null,
290
+ filterHash: filterHash(filter),
291
+ scope: filter.scope ?? null,
292
+ predicates: filter.predicates ?? null,
293
+ nativeQuery: filter.nativeQuery ?? null,
294
+ pushdown: false,
295
+ fetched,
296
+ matched: 0,
297
+ });
298
+ continue;
299
+ }
300
+
301
+ const admitted = (section.items ?? []).filter((item) => matchesFilter(item, filter));
302
+ section.items = admitted;
303
+
304
+ recordFilterAudit(rootDir, {
305
+ provider: section.provider,
306
+ instance: source?.instance ?? null,
307
+ filterHash: filterHash(filter),
308
+ scope: filter.scope ?? null,
309
+ predicates: filter.predicates ?? null,
310
+ nativeQuery: filter.nativeQuery ?? null,
311
+ pushdown: Boolean(filter.nativeQuery) || Boolean(filter.scope?.projects?.length),
312
+ fetched,
313
+ matched: admitted.length,
314
+ });
315
+ }
316
+ }
317
+
224
318
  /**
225
319
  * Distill snapshot items (issues, PRs, messages) into the observation store.
226
320
  * Skips items already recorded in this snapshot cycle using a dedup key.
227
321
  * Returns the count of new observations written.
228
322
  */
229
- function distillSnapshotItems(rootDir, sections) {
323
+ export async function distillSnapshotItems(rootDir, sections) {
230
324
  let written = 0;
231
325
  for (const section of sections ?? []) {
232
326
  for (const item of section.items ?? []) {
@@ -235,7 +329,7 @@ function distillSnapshotItems(rootDir, sections) {
235
329
  if (!dedupKey) continue;
236
330
 
237
331
  // Check if we already have a recent observation for this exact item key
238
- const existing = searchObservations(rootDir, dedupKey, { limit: 1 });
332
+ const existing = await searchObservations(rootDir, dedupKey, { limit: 1 });
239
333
  if (existing.some((o) => o.tags?.includes(`item:${dedupKey}`))) continue;
240
334
 
241
335
  const summary = buildItemSummary(item, section.provider);
@@ -363,6 +457,7 @@ export class EmbedDaemon {
363
457
  #daemonStatePath = null;
364
458
  #inboxWatcher = null;
365
459
  #inboxLiveWatcher = null;
460
+ #capabilityIds = [];
366
461
  #providerHealth = new ProviderHealthTracker();
367
462
  #daemonTraceId = null;
368
463
 
@@ -429,6 +524,11 @@ export class EmbedDaemon {
429
524
  this.#config.snapshot.intervalMs,
430
525
  async () => {
431
526
  const snapshot = await this.#snapshotEngine.generate();
527
+
528
+ // ADR-0060: drop out-of-filter items before anything downstream
529
+ // (outputs, distillation, roadmap) can see them.
530
+ enforceSectionFilters(this.#rootDir, snapshot, this.#config.sources);
531
+
432
532
  this.#lastSnapshot = snapshot;
433
533
  await dispatchOutputs(snapshot, this.#config.outputs, this.#registry, this.#authorityGuard, this.#rootDir);
434
534
  process.stderr.write(`[embed] Snapshot generated at ${snapshot.completedAt} — ${snapshot.summary.totalItems} items, ${snapshot.summary.errorCount} errors\n`);
@@ -457,7 +557,7 @@ export class EmbedDaemon {
457
557
  }
458
558
 
459
559
  // Distill snapshot items (issues, PRs, messages) into observation store
460
- const itemsWritten = distillSnapshotItems(this.#rootDir, snapshot.sections ?? []);
560
+ const itemsWritten = await distillSnapshotItems(this.#rootDir, snapshot.sections ?? []);
461
561
  if (itemsWritten) {
462
562
  process.stderr.write(`[embed] Distilled ${itemsWritten} snapshot item(s) into observation store\n`);
463
563
  }
@@ -564,9 +664,9 @@ export class EmbedDaemon {
564
664
  60_000,
565
665
  async () => {
566
666
  const logPath = join(doctorRoot(), 'runtime', 'embed-daemon.log');
567
- const capMb = Number(process.env.CONSTRUCT_EMBED_LOG_MAX_MB) || 50;
667
+ const capMb = resolveNonNegativeSetting(process.env, 'CONSTRUCT_EMBED_LOG_MAX_MB', 50);
568
668
  const maxBytes = Math.floor(capMb * 1024 * 1024);
569
- const maxSegments = Number(process.env.CONSTRUCT_EMBED_LOG_MAX_SEGMENTS) || 5;
669
+ const maxSegments = resolveNonNegativeSetting(process.env, 'CONSTRUCT_EMBED_LOG_MAX_SEGMENTS', 5);
570
670
  await rotateIfOversized(logPath, { maxBytes, maxSegments, gzip: true });
571
671
  },
572
672
  { runImmediately: true },
@@ -1125,6 +1225,24 @@ this.#scheduler.register(
1125
1225
  );
1126
1226
  }
1127
1227
 
1228
+ // ── Job 16: embed-capabilities (ADR-0061 / LMCP-P2/F5) ───────────────────
1229
+ // One scheduled job per enabled embed-capability manifest — exactly the
1230
+ // enabled set, read fresh at startup so `construct embed enable/disable`
1231
+ // takes effect on the next daemon restart. The job body (capability-jobs.mjs
1232
+ // runCapabilityTick) composes the bound provider slice off this daemon's
1233
+ // own last snapshot, asks workflow-invoke.mjs for the plan, and enqueues
1234
+ // any resulting proposals into this same approval queue — never through
1235
+ // a provider write adapter. No reasoningExecutor is wired in here because
1236
+ // no in-process/external reasoning engine ships in this repo yet; every
1237
+ // tick is therefore an honest skipped-with-reason(reasoning-executor-not-available)
1238
+ // until one is wired in, per ADR-0061 §3.
1239
+ this.#capabilityIds = registerEmbedCapabilityJobs(this.#scheduler, {
1240
+ rootDir: this.#rootDir,
1241
+ env: this.#env,
1242
+ getSnapshot: () => this.#lastSnapshot,
1243
+ approvalQueue: this.#approvalQueue,
1244
+ });
1245
+
1128
1246
  this.#scheduler.start();
1129
1247
 
1130
1248
  // Reactive inbox watching: low-latency trigger backed by the scheduler
@@ -1200,6 +1318,7 @@ this.#scheduler.register(
1200
1318
  schedulerTasks: this.#scheduler?.status() ?? [],
1201
1319
  providerHealth: this.#providerHealth.summary(),
1202
1320
  inboxDirs: this.#inboxWatcher?.dirs() ?? [],
1321
+ embedCapabilities: this.#capabilityIds,
1203
1322
  };
1204
1323
  }
1205
1324
 
@@ -15,15 +15,20 @@
15
15
  * Does NOT require the daemon to be running.
16
16
  */
17
17
 
18
+ import { existsSync } from 'node:fs';
18
19
  import { homedir } from 'node:os';
19
20
  import { join } from 'node:path';
20
21
  import { loadConstructEnv } from '../env-config.mjs';
21
22
  import { addObservation } from '../observation-store.mjs';
22
23
  import { loadProjectConfig } from '../config/project-config.mjs';
24
+ import { configDir } from '../config/xdg.mjs';
25
+ import { loadEmbedConfig } from './config.mjs';
26
+ import { matchesFilter, validateFilterConfig } from '../providers/contract.mjs';
23
27
  import {
24
28
  resolveEffectiveSourceTargetsFromConfig,
25
29
  resolveKnownSourcesFromTargets,
26
30
  } from '../config/source-targets.mjs';
31
+ import { getSourceTargetDescriptor, renderTemplate } from '../config/source-target-registry.mjs';
27
32
 
28
33
  // ─── Self-query detection ────────────────────────────────────────────────────
29
34
 
@@ -136,6 +141,75 @@ export function matchSourceFromQuery(query, env = process.env, cwd = process.cwd
136
141
  return matches[0];
137
142
  }
138
143
 
144
+ // ─── ADR-0060 filter enforcement (construct-737t) ────────────────────────────
145
+
146
+ /**
147
+ * Load embed.yaml's sources[] list — the only place an ADR-0060 filter block
148
+ * lives (ADR-0060 §2; enforced on the poll path by daemon.mjs's
149
+ * enforceSectionFilters). demand-fetch resolves its own source/target
150
+ * records from construct.config.json and legacy env vars, which carry no
151
+ * filter field, so this reads the same embed config the daemon would load,
152
+ * purely to look up a governing filter. Missing/unreadable embed.yaml is not
153
+ * an error here — demand-fetch works without the daemon running.
154
+ */
155
+ function loadEmbedSourcesForFilter(env) {
156
+ try {
157
+ const configPath = join(configDir(undefined, env), 'embed.yaml');
158
+ if (!existsSync(configPath)) return [];
159
+ return loadEmbedConfig(configPath).sources ?? [];
160
+ } catch {
161
+ return [];
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Find the embed.yaml source entry that declares `ref` among its repo/
167
+ * project/channel/team values for `provider`, using the same plural-array/
168
+ * singular-scalar fallback (`opts.repos ?? [opts.repo]`) the real providers
169
+ * use to resolve their own config. A `ref` of null (a broad, unscoped fetch
170
+ * spanning every configured value) never resolves to one governing source,
171
+ * so it is left unfiltered rather than guessing.
172
+ */
173
+ function findGoverningEmbedSource(embedSources, provider, ref) {
174
+ if (ref == null) return null;
175
+ const descriptor = getSourceTargetDescriptor(provider);
176
+ const embed = descriptor?.embed;
177
+ if (!embed) return null;
178
+
179
+ const pluralField = embed.valueKind === 'array' ? embed.valueField : `${embed.valueField}s`;
180
+ const singularField = embed.valueKind === 'array' ? embed.valueField.replace(/s$/, '') : embed.valueField;
181
+ const target = String(ref).trim().toLowerCase();
182
+
183
+ return embedSources.find((src) => {
184
+ if (src.provider !== provider) return false;
185
+ const declared = Array.isArray(src[pluralField])
186
+ ? src[pluralField]
187
+ : (src[singularField] ? [src[singularField]] : []);
188
+ return declared.some((v) => String(v).trim().toLowerCase() === target);
189
+ }) ?? null;
190
+ }
191
+
192
+ /**
193
+ * Apply the governing embed.yaml filter (if any) to items already fetched
194
+ * from `provider`/`ref`, mirroring daemon.mjs's enforceSectionFilters so the
195
+ * on-demand path and the poll path never disagree on which items are in
196
+ * scope. Fails closed on an invalid filter config: every item is dropped and
197
+ * the reason surfaces as an error, never silently ingested.
198
+ */
199
+ function applyEmbedFilter(items, provider, ref, env) {
200
+ const source = findGoverningEmbedSource(loadEmbedSourcesForFilter(env), provider, ref);
201
+ const filter = source?.filter ?? null;
202
+ if (filter == null) return { items, error: null };
203
+
204
+ try {
205
+ validateFilterConfig(provider, filter);
206
+ } catch (err) {
207
+ return { items: [], error: err.message };
208
+ }
209
+
210
+ return { items: items.filter((item) => matchesFilter(item, filter)), error: null };
211
+ }
212
+
139
213
  // ─── On-demand fetch ─────────────────────────────────────────────────────────
140
214
 
141
215
  /**
@@ -261,7 +335,7 @@ export async function demandFetch({ query, rootDir, env, cwd, teamId = null, tar
261
335
 
262
336
  // Separate real items from error sentinels the provider may have returned
263
337
  const errorItems = rawItems.filter(i => i.type === 'error');
264
- const dataItems = rawItems.filter(i => i.type !== 'error');
338
+ let dataItems = rawItems.filter(i => i.type !== 'error');
265
339
 
266
340
  if (!dataItems.length && errorItems.length) {
267
341
  const errMsg = errorItems.map(i => i.message).join('; ');
@@ -275,6 +349,21 @@ export async function demandFetch({ query, rootDir, env, cwd, teamId = null, tar
275
349
  };
276
350
  }
277
351
 
352
+ // ADR-0060: drop out-of-scope items the same way enforceSectionFilters does
353
+ // on the poll path, so a source whose refs/repo list is broader than its
354
+ // filter.scope narrows identically on the on-demand path.
355
+ const filtered = applyEmbedFilter(dataItems, match.provider, match.ref, resolvedEnv);
356
+ if (filtered.error) {
357
+ return {
358
+ ok: false,
359
+ reason: 'filter_config_error',
360
+ message: `${match.display} filter config error: ${filtered.error}`,
361
+ items: [],
362
+ match,
363
+ };
364
+ }
365
+ dataItems = filtered.items;
366
+
278
367
  // Write each item as an observation so future queries find them in the knowledge base
279
368
  let written = 0;
280
369
  for (const item of dataItems) {
@@ -376,7 +465,22 @@ async function demandFetchAll({ query, rootDir, env, cwd } = {}) {
376
465
  if (!readCalls.length) continue;
377
466
  for (const { ref, opts, display } of readCalls) {
378
467
  try {
379
- const items = await provider.read(ref, opts);
468
+ const rawItems = await provider.read(ref, opts) ?? [];
469
+
470
+ // ADR-0060: same governing-source filter lookup as the single-match
471
+ // path, keyed on this source's own ref so a broader universal fetch
472
+ // narrows identically to what enforceSectionFilters does at poll
473
+ // time. Error sentinels bypass the filter — matchesFilter would
474
+ // otherwise drop them for lacking a scope field, hiding a real
475
+ // provider error behind a filter that was never meant to judge it.
476
+ const errorSentinels = rawItems.filter((i) => i.type === 'error');
477
+ const filtered = applyEmbedFilter(rawItems.filter((i) => i.type !== 'error'), providerName, src.ref, env);
478
+ if (filtered.error) {
479
+ errors.push({ provider: providerName, ref: src.ref ?? null, error: `filter config: ${filtered.error}` });
480
+ continue;
481
+ }
482
+ const items = [...errorSentinels, ...filtered.items];
483
+
380
484
  if (items?.length) {
381
485
  allItems.push(...items);
382
486
  for (const item of items) {
@@ -431,78 +535,94 @@ async function demandFetchAll({ query, rootDir, env, cwd } = {}) {
431
535
  /**
432
536
  * Build a list of { ref, opts } pairs to pass to provider.read(ref, opts).
433
537
  *
538
+ * Dispatches on the matched provider's manifest-derived `sourceTarget.demandFetch`
539
+ * descriptor (lib/config/source-target-registry.mjs) rather than naming
540
+ * providers — a provider whose descriptor declares `queryDynamicJql` gets
541
+ * jira's recency-guarded/targeted JQL construction, one with `queryOptsKind:
542
+ * 'scalar'` gets linear's single-value opts, one with `querySupported: false`
543
+ * (or no `demandFetch` block at all) is skipped, and everything else gets
544
+ * github's fixed-array-opts, multi-ref shape.
545
+ *
434
546
  * @param {{ provider: string, ref: string|null, display: string }} match
435
547
  * @param {object} env
436
548
  * @returns {{ ref: string, opts: object, display: string }[]}
437
549
  */
438
550
  function buildReadCalls(match, env, cwd = process.cwd()) {
439
- if (match.provider === 'github') {
440
- const opts = { repos: [match.ref], limit: 25 };
441
- return [
442
- { ref: 'meta', opts, display: match.display },
443
- { ref: 'readme', opts, display: match.display },
444
- { ref: 'docs', opts, display: match.display },
445
- { ref: 'prs', opts, display: match.display },
446
- { ref: 'issues', opts, display: match.display },
447
- { ref: 'commits', opts, display: match.display },
448
- ];
449
- }
450
- if (match.provider === 'jira') {
551
+ const descriptor = getSourceTargetDescriptor(match.provider);
552
+ const df = descriptor?.demandFetch;
553
+ if (!df || df.querySupported === false) return [];
554
+
555
+ if (df.queryDynamicJql) {
556
+ const dj = df.queryDynamicJql;
451
557
  const { config } = loadProjectConfig(cwd, env);
452
- const targets = resolveEffectiveSourceTargetsFromConfig(config, env)
453
- .filter((t) => t.provider === 'jira')
454
- .map((t) => t.selector.project);
455
- const projects = targets.length
456
- ? targets
457
- : (env.JIRA_PROJECTS ?? '').split(',').map((p) => p.trim()).filter(Boolean);
458
- const recencyDays = parseInt(env.JIRA_FETCH_RECENCY_DAYS, 10) || 30;
558
+ const projects = resolveEffectiveSourceTargetsFromConfig(config, env)
559
+ .filter((t) => t.provider === match.provider)
560
+ .map((t) => t.selector[descriptor.selector.field]);
561
+ const projectList = projects.length
562
+ ? projects
563
+ : (env[dj.legacyProjectsEnvVar] ?? '').split(',').map((p) => p.trim()).filter(Boolean);
564
+ const recencyDays = parseInt(env[dj.recencyEnvVar], 10) || dj.recencyDefaultDays;
459
565
 
460
566
  let jql;
461
567
  if (match.ref) {
462
- // Targeted project fetch — no recency cutoff, user asked for this project
463
- jql = `project = "${match.ref}" AND statusCategory != Done ORDER BY updated DESC`;
464
- } else if (projects.length > 0) {
465
- // Broad/fallback fetch — scoped to configured projects with recency guard
466
- const projectList = projects.map(p => `"${p}"`).join(', ');
467
- jql = `project IN (${projectList}) AND updated >= -${recencyDays}d ORDER BY updated DESC`;
568
+ // Targeted fetch — no recency cutoff, user asked for this value specifically
569
+ jql = renderTemplate(dj.singleTemplate, { value: match.ref });
570
+ } else if (projectList.length > 0) {
571
+ // Broad/fallback fetch — scoped to configured values with recency guard
572
+ const quoted = projectList.map((p) => `"${p}"`).join(', ');
573
+ jql = renderTemplate(dj.broadTemplate, { values: quoted, recencyDays });
468
574
  } else {
469
- // No JIRA_PROJECTS configured — skip Jira entirely rather than defaulting to user-scoped
575
+ // Nothing configured — skip rather than defaulting to an unscoped query
470
576
  return [];
471
577
  }
472
- return [{ ref: 'issues', opts: { jql, limit: 50 }, display: match.display }];
578
+ return [{ ref: df.targetRefs[0], opts: { jql, limit: df.queryDefaultLimit }, display: match.display }];
473
579
  }
474
- if (match.provider === 'linear') {
475
- return [{ ref: 'issues', opts: { team: match.ref ?? null, limit: 50 }, display: match.display }];
580
+
581
+ if (df.queryOptsKind === 'scalar') {
582
+ const value = df.queryOptsNullable ? (match.ref ?? null) : match.ref;
583
+ return [{ ref: df.queryRefs[0], opts: { [df.queryOptsField]: value, limit: df.queryDefaultLimit }, display: match.display }];
476
584
  }
477
- return [];
585
+
586
+ const opts = { [df.queryOptsField]: [match.ref], limit: df.queryDefaultLimit };
587
+ return df.queryRefs.map((ref) => ({ ref, opts, display: match.display }));
478
588
  }
479
589
 
480
590
  // Read calls for one resolved team source, honoring the target's own filters
481
591
  // (jira jql, github refs/limit, slack oldest) rather than the generic defaults.
592
+ // Same manifest-derived dispatch as buildReadCalls, keyed on target.provider.
482
593
 
483
594
  function buildReadCallsForTarget(target) {
595
+ const descriptor = getSourceTargetDescriptor(target.provider);
484
596
  const sel = target.selector ?? {};
485
597
  const f = target.filters ?? {};
486
- const display = `${target.provider}/${sel.repo ?? sel.project ?? sel.team ?? sel.channel ?? target.id}`;
487
- switch (target.provider) {
488
- case 'github': {
489
- const opts = { repos: [sel.repo], limit: Number.isInteger(f.limit) ? f.limit : 25 };
490
- const refs = Array.isArray(f.refs) ? f.refs : ['meta', 'readme', 'docs', 'prs', 'issues', 'commits'];
491
- return refs.map((ref) => ({ ref, opts, display }));
492
- }
493
- case 'jira': {
494
- const jql = typeof f.jql === 'string'
495
- ? f.jql
496
- : `project = "${sel.project}" AND statusCategory != Done ORDER BY updated DESC`;
497
- return [{ ref: 'issues', opts: { jql, limit: Number.isInteger(f.limit) ? f.limit : 50 }, display }];
498
- }
499
- case 'linear':
500
- return [{ ref: 'issues', opts: { team: sel.team, limit: Number.isInteger(f.limit) ? f.limit : 50 }, display }];
501
- case 'slack':
502
- return [{ ref: 'messages', opts: { channels: [sel.channel], oldest: Number.isInteger(f.oldest) ? f.oldest : 86400, limit: Number.isInteger(f.limit) ? f.limit : 50 }, display }];
503
- default:
504
- return [];
598
+ const display = `${target.provider}/${descriptor ? (sel[descriptor.selector.field] ?? target.id) : target.id}`;
599
+ const df = descriptor?.demandFetch;
600
+ if (!df) return [];
601
+
602
+ const limit = Number.isInteger(f.limit) ? f.limit : df.targetDefaultLimit;
603
+
604
+ if (df.targetJqlTemplate) {
605
+ const jql = typeof f[df.targetJqlFilterKey] === 'string'
606
+ ? f[df.targetJqlFilterKey]
607
+ : renderTemplate(df.targetJqlTemplate, { value: sel[descriptor.selector.field] });
608
+ return [{ ref: df.targetRefs[0], opts: { jql, limit }, display }];
609
+ }
610
+
611
+ if (df.targetOptsKind === 'scalar') {
612
+ return [{ ref: df.targetRefs[0], opts: { [df.targetOptsField]: sel[descriptor.selector.field], limit }, display }];
613
+ }
614
+
615
+ if (df.targetRefsFilterKey) {
616
+ const refs = Array.isArray(f[df.targetRefsFilterKey]) ? f[df.targetRefsFilterKey] : df.targetRefs;
617
+ const opts = { [df.targetOptsField]: [sel[descriptor.selector.field]], limit };
618
+ return refs.map((ref) => ({ ref, opts, display }));
505
619
  }
620
+
621
+ const opts = { [df.targetOptsField]: [sel[descriptor.selector.field]], limit };
622
+ for (const key of df.targetExtraFilterKeys ?? []) {
623
+ if (key === 'oldest') opts.oldest = Number.isInteger(f.oldest) ? f.oldest : df.targetOldestDefault;
624
+ }
625
+ return [{ ref: df.targetRefs[0], opts, display }];
506
626
  }
507
627
 
508
628
  // Drive the fetch from a team's resolved sources: one provider read per target,
@@ -526,11 +646,27 @@ async function demandFetchTeam({ teamId, targets, rootDir, env, providerRegistry
526
646
  errors.push({ provider: target.provider, targetId: target.id, message: `provider "${target.provider}" not registered` });
527
647
  continue;
528
648
  }
649
+ const targetDescriptor = getSourceTargetDescriptor(target.provider);
650
+ const targetRef = targetDescriptor ? (target.selector ?? {})[targetDescriptor.selector.field] : null;
529
651
  for (const { ref, opts, display } of buildReadCallsForTarget(target)) {
530
652
  try {
531
- const items = await provider.read(ref, opts);
532
- for (const item of items ?? []) {
533
- if (item.type === 'error') { errors.push(item); continue; }
653
+ const rawItems = await provider.read(ref, opts) ?? [];
654
+
655
+ // ADR-0060: same governing-source filter lookup, keyed on the team
656
+ // target's own selector value, so a team-scoped fetch narrows
657
+ // identically to what enforceSectionFilters does at poll time. Error
658
+ // sentinels bypass the filter — they never carry a scope field, so
659
+ // matchesFilter would otherwise drop them and hide a real fetch error.
660
+ const errorSentinels = rawItems.filter((i) => i.type === 'error');
661
+ const filtered = applyEmbedFilter(rawItems.filter((i) => i.type !== 'error'), target.provider, targetRef, env);
662
+ if (filtered.error) {
663
+ errors.push({ provider: target.provider, targetId: target.id, message: `filter config: ${filtered.error}` });
664
+ continue;
665
+ }
666
+
667
+ for (const item of errorSentinels) errors.push(item);
668
+
669
+ for (const item of filtered.items) {
534
670
  allItems.push(item);
535
671
  try {
536
672
  addObservation(rootDir, {
@@ -16,8 +16,9 @@
16
16
  * two stats, so a partially-written drop is never consumed mid-write.
17
17
  *
18
18
  * State tracking:
19
- * <rootDir>/.cx/runtime/inbox-state.json maps filePath → processedAt
20
- * Prevents re-processing the same file across daemon restarts.
19
+ * <stateRoot>/runtime/inbox-state.json (ADR-0066: machine-scoped, not
20
+ * project-local) — maps filePath → processedAt. Prevents re-processing the
21
+ * same file across daemon restarts.
21
22
  *
22
23
  * Usage:
23
24
  * const watcher = new InboxWatcher({ rootDir, env });
@@ -40,8 +41,9 @@ import { loadManifest, recordFile, hasFile, sha256Of, MANIFEST_REL_PATH } from '
40
41
  import { gatherAttribution } from '../intake/attribution.mjs';
41
42
  import { acquirePollLock, releasePollLock } from '../intake/poll-lock.mjs';
42
43
  import { doctorRoot } from '../config/xdg.mjs';
44
+ import { resolveStatePath } from '../state-root.mjs';
43
45
 
44
- const STATE_FILE = '.cx/runtime/inbox-state.json';
46
+ const STATE_FILE_REL = join('runtime', 'inbox-state.json');
45
47
  const ROOT_INBOX_SUBDIR = 'inbox';
46
48
  const STAGING_SUBDIR = '.staging';
47
49
  const MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB hard cap
@@ -116,26 +118,26 @@ function pruneStaleState(rootDir, state) {
116
118
  // ─── State helpers ────────────────────────────────────────────────────────────
117
119
 
118
120
  function statePath(rootDir) {
119
- return join(rootDir, STATE_FILE);
121
+ return resolveStatePath(rootDir, STATE_FILE_REL);
120
122
  }
121
123
 
122
124
  function readState(rootDir) {
125
+ if (!shouldCreateCx(rootDir)) return {};
123
126
  const p = statePath(rootDir);
124
127
  if (!existsSync(p)) return {};
125
128
  try { return JSON.parse(readFileSync(p, 'utf8')); } catch (err) { process.stderr.write('[inbox.mjs] readState: ' + (err?.message ?? String(err)) + '\n'); return {}; }
126
129
  }
127
130
 
128
131
  function writeState(rootDir, state) {
129
- const p = statePath(rootDir);
130
-
131
- // Only create .cx directories if this is an initialized project
132
+ // Only persist state if this is an initialized project — an uninitialized
133
+ // cwd shouldn't accumulate machine-scoped state for a directory that isn't
134
+ // really a Construct project.
135
+
132
136
  if (shouldCreateCx(rootDir)) {
133
137
  ensureCxDir(rootDir);
134
- mkdirSync(join(rootDir, '.cx', 'runtime'), { recursive: true });
138
+ const p = statePath(rootDir);
135
139
  writeFileSync(p, JSON.stringify(state, null, 2) + '\n');
136
140
  }
137
- // If not an initialized project, don't create .cx - state will be lost
138
- // but that's okay since we shouldn't be processing files in uninitialized dirs
139
141
  }
140
142
 
141
143
  // ─── Directory resolution ─────────────────────────────────────────────────────