@geraldmaron/construct 1.4.2 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (519) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1027 -64
  4. package/examples/distribution/sources/deck-one-pager.md +1 -1
  5. package/lib/acp/server.mjs +21 -7
  6. package/lib/adapters-sync.mjs +2 -1
  7. package/lib/audit-trail.mjs +185 -2
  8. package/lib/beads/auto-close.mjs +2 -1
  9. package/lib/beads/drift.mjs +2 -1
  10. package/lib/bridges/copilot-proxy.mjs +20 -5
  11. package/lib/certification/skill-inventory.mjs +10 -1
  12. package/lib/cli/approvals.mjs +147 -0
  13. package/lib/cli-commands.mjs +105 -14
  14. package/lib/comment-lint.mjs +127 -8
  15. package/lib/config/schema.mjs +6 -29
  16. package/lib/config/source-target-registry.mjs +70 -0
  17. package/lib/config/source-targets.mjs +179 -205
  18. package/lib/contracts/coverage.mjs +77 -0
  19. package/lib/contracts/validate.mjs +102 -13
  20. package/lib/contracts/violation-log.mjs +50 -4
  21. package/lib/db/migrate.mjs +69 -0
  22. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  23. package/lib/db/migrations/002_queue_provider.sql +50 -0
  24. package/lib/db/migrations/003_worker_registry.sql +17 -0
  25. package/lib/db/migrations/004_trace_events.sql +16 -0
  26. package/lib/db/migrations/005_shared_memory.sql +15 -0
  27. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  28. package/lib/decisions/enforced-baseline.json +0 -2
  29. package/lib/decisions/registry.mjs +3 -2
  30. package/lib/deployment/parity-contract.mjs +1 -1
  31. package/lib/deployment-mode.mjs +78 -2
  32. package/lib/diagram-export.mjs +10 -1
  33. package/lib/distill.mjs +5 -2
  34. package/lib/docs-verify.mjs +2 -1
  35. package/lib/doctor/cli.mjs +1 -0
  36. package/lib/doctor/command-on-path.mjs +24 -0
  37. package/lib/doctor/diagnosis.mjs +89 -0
  38. package/lib/doctor/embedding-health.mjs +50 -0
  39. package/lib/doctor/engine-health.mjs +93 -0
  40. package/lib/doctor/graph-validate.mjs +47 -0
  41. package/lib/doctor/index.mjs +18 -4
  42. package/lib/doctor/sidecar-providers.mjs +56 -0
  43. package/lib/doctor/watchers/consistency.mjs +93 -1
  44. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  45. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  46. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  47. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  48. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  49. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  50. package/lib/document-export.mjs +52 -27
  51. package/lib/document-extract/docling-client.mjs +9 -5
  52. package/lib/document-extract/docling-sidecar.py +30 -0
  53. package/lib/document-extract.mjs +1 -1
  54. package/lib/document-ingest.mjs +9 -0
  55. package/lib/embed/approval-queue.mjs +184 -88
  56. package/lib/embed/authority-guard.mjs +65 -2
  57. package/lib/embed/capability-jobs.mjs +365 -0
  58. package/lib/embed/capability-lifecycle.mjs +219 -0
  59. package/lib/embed/capability-loader.mjs +303 -0
  60. package/lib/embed/capability-runtime.mjs +58 -0
  61. package/lib/embed/cli.mjs +185 -8
  62. package/lib/embed/config.mjs +4 -0
  63. package/lib/embed/daemon.mjs +125 -6
  64. package/lib/embed/demand-fetch.mjs +190 -54
  65. package/lib/embed/inbox.mjs +12 -10
  66. package/lib/embed/presets/ops-triage.mjs +236 -0
  67. package/lib/embed/presets/pm-feedback.mjs +341 -0
  68. package/lib/embed/presets/tpm.mjs +401 -0
  69. package/lib/embed/providers/jira.mjs +72 -1
  70. package/lib/embed/providers/registry.mjs +140 -33
  71. package/lib/embedded-contract/capability.mjs +4 -0
  72. package/lib/embedded-contract/execution.mjs +1 -1
  73. package/lib/embedded-contract/model-resolve.mjs +53 -3
  74. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  75. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  76. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  77. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  78. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  79. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  80. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  81. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  82. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  83. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  84. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  85. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  86. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  88. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  90. package/lib/env-config.mjs +50 -9
  91. package/lib/extensions/index.mjs +27 -0
  92. package/lib/extensions/loader.mjs +120 -0
  93. package/lib/extensions/manifest-schema.mjs +141 -0
  94. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  95. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  96. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  97. package/lib/extensions/manifests/directory.manifest.json +12 -0
  98. package/lib/extensions/manifests/docling.manifest.json +37 -0
  99. package/lib/extensions/manifests/echo.manifest.json +8 -0
  100. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  101. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  102. package/lib/extensions/manifests/github.manifest.json +52 -0
  103. package/lib/extensions/manifests/linear.manifest.json +50 -0
  104. package/lib/extensions/manifests/local.manifest.json +12 -0
  105. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  106. package/lib/extensions/manifests/openai.manifest.json +12 -0
  107. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  108. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  109. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  110. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  113. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  114. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  115. package/lib/extensions/manifests/slack.manifest.json +58 -0
  116. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  117. package/lib/extensions/validate.mjs +175 -0
  118. package/lib/features.mjs +13 -9
  119. package/lib/flows/checkpoint.mjs +184 -0
  120. package/lib/flows/constants.mjs +27 -0
  121. package/lib/flows/define.mjs +146 -0
  122. package/lib/flows/engine.mjs +249 -0
  123. package/lib/flows/errors.mjs +19 -0
  124. package/lib/flows/index.mjs +27 -0
  125. package/lib/flows/joins.mjs +18 -0
  126. package/lib/flows/schema.mjs +90 -0
  127. package/lib/flows/state.mjs +31 -0
  128. package/lib/frameworks/loader.mjs +99 -0
  129. package/lib/frameworks/schema.mjs +157 -0
  130. package/lib/graph/build-from-corpus.mjs +67 -0
  131. package/lib/graph/build-from-embed.mjs +82 -0
  132. package/lib/graph/build-from-registry.mjs +164 -3
  133. package/lib/graph/cli.mjs +456 -12
  134. package/lib/graph/gap-queries.mjs +156 -0
  135. package/lib/graph/gaps.mjs +41 -0
  136. package/lib/graph/impacted.mjs +129 -0
  137. package/lib/graph/runtime-evidence.mjs +177 -0
  138. package/lib/graph/security-coverage.mjs +113 -0
  139. package/lib/graph/staleness.mjs +241 -10
  140. package/lib/graph/store.mjs +24 -7
  141. package/lib/graph/validate.mjs +161 -0
  142. package/lib/headhunt.mjs +9 -8
  143. package/lib/health-check.mjs +15 -7
  144. package/lib/hook-health.mjs +1 -1
  145. package/lib/hooks/agent-tracker.mjs +10 -4
  146. package/lib/hooks/edit-guard.mjs +3 -3
  147. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  148. package/lib/hooks/guard-bash.mjs +41 -15
  149. package/lib/hooks/mcp-health-check.mjs +11 -4
  150. package/lib/hooks/model-fallback.mjs +50 -6
  151. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  152. package/lib/hooks/pre-compact.mjs +181 -173
  153. package/lib/hooks/rule-verifier.mjs +2 -1
  154. package/lib/hooks/session-reflect.mjs +2 -1
  155. package/lib/hooks/session-start.mjs +3 -3
  156. package/lib/host-capabilities.mjs +13 -2
  157. package/lib/host-disposition.mjs +19 -7
  158. package/lib/identity.mjs +92 -0
  159. package/lib/ingest/degraded-extract.mjs +96 -0
  160. package/lib/ingest/docling-remote.mjs +2 -1
  161. package/lib/ingest/provider-extract.mjs +7 -19
  162. package/lib/ingest/sidecar-providers.mjs +196 -0
  163. package/lib/ingest-tooling.mjs +11 -5
  164. package/lib/init-unified.mjs +57 -45
  165. package/lib/init-update.mjs +2 -1
  166. package/lib/install/legacy-global-cleanup.mjs +25 -0
  167. package/lib/intake/daemon.mjs +99 -8
  168. package/lib/intake/git-queue.mjs +110 -38
  169. package/lib/intake/queue-registry.mjs +50 -0
  170. package/lib/intake/queue.mjs +133 -17
  171. package/lib/intake/session-prelude.mjs +57 -8
  172. package/lib/integrations/intake-integrations.mjs +61 -111
  173. package/lib/intent-classifier.mjs +43 -5
  174. package/lib/libreoffice-export.mjs +8 -8
  175. package/lib/logging/rotate.mjs +5 -4
  176. package/lib/mcp/broker.mjs +332 -17
  177. package/lib/mcp/denied-store.mjs +95 -0
  178. package/lib/mcp/destructive-gate.mjs +30 -0
  179. package/lib/mcp/dispatch-envelope.mjs +199 -0
  180. package/lib/mcp/memory-bridge.mjs +2 -1
  181. package/lib/mcp/server.mjs +154 -1411
  182. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  183. package/lib/mcp/tool-definitions-project.mjs +271 -0
  184. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  185. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  186. package/lib/mcp/tool-definitions.mjs +25 -0
  187. package/lib/mcp/tool-registry.mjs +107 -0
  188. package/lib/mcp/tool-safety.mjs +1 -0
  189. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  190. package/lib/mcp/tools/orchestration-run.mjs +165 -25
  191. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  192. package/lib/mcp/tools/provider-write.mjs +187 -0
  193. package/lib/mcp/tools/scope.mjs +0 -3
  194. package/lib/mcp/tools/skills.mjs +1 -1
  195. package/lib/mcp/tools/storage.mjs +0 -8
  196. package/lib/mcp/transport/auth.mjs +238 -0
  197. package/lib/mcp/transport/http.mjs +136 -0
  198. package/lib/mcp/transport/mode.mjs +31 -0
  199. package/lib/mcp/transport/stdio.mjs +22 -0
  200. package/lib/mcp-catalog.json +4 -4
  201. package/lib/mcp-manager.mjs +34 -23
  202. package/lib/mcp-platform-config.mjs +31 -7
  203. package/lib/mode-capabilities.mjs +109 -0
  204. package/lib/model-router.mjs +42 -9
  205. package/lib/models/catalog.mjs +40 -1
  206. package/lib/net-guard.mjs +247 -0
  207. package/lib/observation-store.mjs +6 -2
  208. package/lib/ollama/provision-context.mjs +2 -1
  209. package/lib/opencode-config.mjs +22 -3
  210. package/lib/opencode-runtime-plugin.mjs +120 -2
  211. package/lib/oracle/actions.mjs +204 -10
  212. package/lib/oracle/cli.mjs +24 -3
  213. package/lib/oracle/dispatch.mjs +2 -1
  214. package/lib/oracle/execute.mjs +2 -2
  215. package/lib/oracle/gaps.mjs +3 -3
  216. package/lib/oracle/heartbeat.mjs +53 -0
  217. package/lib/oracle/read-model.mjs +69 -13
  218. package/lib/oracle/reconcile.mjs +3 -46
  219. package/lib/oracle/routing.mjs +37 -31
  220. package/lib/oracle/synthesize.mjs +1 -1
  221. package/lib/orchestration/classification.mjs +434 -0
  222. package/lib/orchestration/delegation-flow.mjs +132 -0
  223. package/lib/orchestration/flow-selection.mjs +418 -0
  224. package/lib/orchestration/gates.mjs +244 -0
  225. package/lib/orchestration/host-sampling.mjs +121 -0
  226. package/lib/orchestration/policy-constants.mjs +48 -0
  227. package/lib/orchestration/provider-outcome.mjs +197 -0
  228. package/lib/orchestration/readiness.mjs +114 -13
  229. package/lib/orchestration/run-store-postgres.mjs +32 -26
  230. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  231. package/lib/orchestration/run-store.mjs +25 -12
  232. package/lib/orchestration/runtime.mjs +446 -39
  233. package/lib/orchestration/store.mjs +87 -12
  234. package/lib/orchestration/trace-store.mjs +125 -0
  235. package/lib/orchestration/web-capability.mjs +3 -2
  236. package/lib/orchestration/worker-runtime.mjs +162 -0
  237. package/lib/orchestration/worker.mjs +528 -89
  238. package/lib/orchestration-policy.mjs +67 -1111
  239. package/lib/packs/cli.mjs +144 -0
  240. package/lib/packs/core-pack.mjs +112 -0
  241. package/lib/packs/enablement.mjs +187 -0
  242. package/lib/packs/index.mjs +23 -0
  243. package/lib/packs/loader.mjs +176 -0
  244. package/lib/packs/manifest-schema.mjs +58 -0
  245. package/lib/packs/prompts.mjs +120 -0
  246. package/lib/packs/validate.mjs +208 -0
  247. package/lib/plugin-registry.mjs +4 -5
  248. package/lib/policy/audit-gate.mjs +42 -0
  249. package/lib/policy/consumption-budget.mjs +149 -0
  250. package/lib/policy/engine.mjs +81 -6
  251. package/lib/policy/role-authority.mjs +89 -0
  252. package/lib/prompt-composer.js +19 -3
  253. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  254. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  255. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  256. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  257. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  258. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  259. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  260. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  261. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  262. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  263. package/lib/providers/contract.mjs +207 -0
  264. package/lib/providers/credential-bootstrap.mjs +7 -4
  265. package/lib/providers/credential-sources.mjs +14 -2
  266. package/lib/providers/directory/index.mjs +273 -0
  267. package/lib/providers/feedback/index.mjs +392 -0
  268. package/lib/providers/filter-audit.mjs +68 -0
  269. package/lib/providers/filter-schema.mjs +54 -0
  270. package/lib/providers/github/index.mjs +31 -0
  271. package/lib/providers/instance-config.mjs +215 -0
  272. package/lib/providers/op-locate.mjs +96 -0
  273. package/lib/providers/op-run.mjs +64 -9
  274. package/lib/providers/registry.mjs +26 -3
  275. package/lib/providers/secret-audit-wiring.mjs +6 -0
  276. package/lib/providers/secret-resolver.mjs +240 -23
  277. package/lib/publish-template.mjs +6 -3
  278. package/lib/publish-tooling.mjs +4 -0
  279. package/lib/publish.mjs +32 -4
  280. package/lib/queue/pg-queue.mjs +395 -0
  281. package/lib/reflect.mjs +2 -1
  282. package/lib/registry/assemble.mjs +28 -2
  283. package/lib/registry/consolidation.mjs +8 -1
  284. package/lib/registry/custom-scaffold.mjs +200 -0
  285. package/lib/registry/custom-schema.mjs +137 -0
  286. package/lib/registry/loader.mjs +8 -3
  287. package/lib/registry/manifests/format-engines.default.json +15 -0
  288. package/lib/registry/manifests/surface-map.default.json +46 -0
  289. package/lib/registry/retired-paths.mjs +1 -0
  290. package/lib/registry/surface-map.mjs +100 -50
  291. package/lib/render-visual-check.mjs +240 -0
  292. package/lib/resources/budget.mjs +40 -17
  293. package/lib/roles/flavor-bindings.mjs +36 -14
  294. package/lib/roles/gateway.mjs +15 -8
  295. package/lib/roots.mjs +107 -0
  296. package/lib/runtime/uv-bootstrap.mjs +32 -6
  297. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  298. package/lib/runtime-env.mjs +5 -2
  299. package/lib/scheduler/index.mjs +8 -20
  300. package/lib/schema-infer.mjs +31 -2
  301. package/lib/scopes/lifecycle.mjs +29 -25
  302. package/lib/scopes/loader.mjs +49 -12
  303. package/lib/scopes/teams.mjs +1 -1
  304. package/lib/security/ingest-boundary.mjs +80 -0
  305. package/lib/security/recall-wrapper.mjs +99 -0
  306. package/lib/security/trust.mjs +132 -0
  307. package/lib/service-manager.mjs +38 -19
  308. package/lib/setup.mjs +41 -23
  309. package/lib/skills-apply.mjs +4 -2
  310. package/lib/specialists/postconditions.mjs +2 -2
  311. package/lib/state-root.mjs +147 -0
  312. package/lib/status.mjs +597 -6
  313. package/lib/storage/admin.mjs +77 -5
  314. package/lib/storage/backend-registry.mjs +73 -0
  315. package/lib/storage/backend.mjs +63 -9
  316. package/lib/storage/embeddings-openai.mjs +12 -2
  317. package/lib/storage/hybrid-query.mjs +11 -3
  318. package/lib/storage/retrieval-hardening.mjs +384 -0
  319. package/lib/storage/shared-memory.mjs +158 -0
  320. package/lib/storage/vector-client.mjs +69 -2
  321. package/lib/team/health.mjs +72 -0
  322. package/lib/telemetry/backends/local.mjs +9 -3
  323. package/lib/telemetry/client.mjs +3 -7
  324. package/lib/template-registry.mjs +10 -12
  325. package/lib/tenant/context.mjs +82 -0
  326. package/lib/tenant/isolation.mjs +129 -0
  327. package/lib/test-corpus-inventory.mjs +104 -2
  328. package/lib/uninstall/uninstall.mjs +78 -12
  329. package/lib/validator.mjs +4 -6
  330. package/lib/worker/entrypoint.mjs +2 -1
  331. package/lib/worker/run.mjs +2 -2
  332. package/lib/worker/trace.mjs +5 -11
  333. package/lib/workflow-state.mjs +52 -8
  334. package/lib/workflows/liveness.mjs +203 -0
  335. package/lib/workflows/loader.mjs +177 -0
  336. package/lib/workflows/manifest-schema.mjs +71 -0
  337. package/lib/workflows/surface-parity.mjs +118 -0
  338. package/lib/workflows/validate.mjs +127 -0
  339. package/lib/writes/control-plane.mjs +140 -0
  340. package/lib/writes/envelope.mjs +206 -0
  341. package/lib/writes/sent-log.mjs +86 -0
  342. package/lib/writes/write-intent.mjs +109 -0
  343. package/package.json +16 -8
  344. package/personas/construct.md +12 -3
  345. package/registry/capabilities.json +143 -36
  346. package/rules/common/comments.md +1 -0
  347. package/schemas/project-config.schema.json +0 -12
  348. package/schemas/unified-registry.schema.json +2 -4
  349. package/scripts/sync-specialists.mjs +284 -81
  350. package/skills/docs/adr-workflow.md +1 -1
  351. package/skills/docs/codebase-research-workflow.md +3 -3
  352. package/skills/docs/prd-workflow.md +5 -6
  353. package/skills/docs/runbook-workflow.md +2 -2
  354. package/skills/docs/user-research-workflow.md +3 -3
  355. package/skills/operating/fleet-health-routing.md +77 -0
  356. package/skills/roles/ai-engineer.md +1 -1
  357. package/skills/roles/architect.md +0 -1
  358. package/skills/roles/business-strategist.md +1 -1
  359. package/skills/roles/data-analyst.telemetry.md +1 -1
  360. package/skills/roles/data-engineer.md +1 -1
  361. package/skills/roles/data-engineer.pipeline.md +1 -1
  362. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  363. package/skills/roles/data-engineer.warehouse.md +1 -1
  364. package/skills/roles/designer.accessibility.md +1 -1
  365. package/skills/roles/designer.md +0 -1
  366. package/skills/roles/devil-advocate.md +1 -1
  367. package/skills/roles/docs-keeper.md +1 -1
  368. package/skills/roles/evaluator.md +1 -1
  369. package/skills/roles/explorer.md +1 -1
  370. package/skills/roles/platform-engineer.md +1 -1
  371. package/skills/roles/qa.ai-eval.md +1 -2
  372. package/skills/roles/qa.api-contract.md +0 -1
  373. package/skills/roles/qa.data-pipeline.md +0 -1
  374. package/skills/roles/qa.md +0 -1
  375. package/skills/roles/qa.web-ui.md +0 -1
  376. package/skills/roles/release-manager.md +1 -1
  377. package/skills/roles/researcher.md +0 -2
  378. package/skills/roles/reviewer.md +0 -3
  379. package/skills/roles/security.ai.md +1 -1
  380. package/skills/roles/security.legal-compliance.md +1 -1
  381. package/skills/roles/security.md +0 -1
  382. package/skills/roles/security.privacy.md +0 -1
  383. package/skills/roles/security.supply-chain.md +1 -1
  384. package/skills/roles/sre.md +1 -1
  385. package/skills/roles/test-automation.md +1 -1
  386. package/skills/roles/trace-reviewer.md +1 -1
  387. package/skills/roles/ux-researcher.md +1 -1
  388. package/specialists/artifact-manifest.json +36 -36
  389. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  390. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  391. package/specialists/org/contracts/any-to-debugger.json +7 -1
  392. package/specialists/org/contracts/any-to-designer.json +7 -1
  393. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  394. package/specialists/org/contracts/any-to-explorer.json +13 -4
  395. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  396. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  397. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  398. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  399. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  400. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  401. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  402. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  403. package/specialists/org/contracts/architect-to-operations.json +17 -4
  404. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  405. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  406. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  407. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  408. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  409. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  410. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  411. package/specialists/org/contracts/operations-to-user.json +53 -0
  412. package/specialists/org/contracts/operations-triage-output.json +53 -0
  413. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  414. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  415. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  416. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  417. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  418. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  419. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  420. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  421. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  422. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  423. package/specialists/org/contracts/user-to-construct.json +11 -4
  424. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  425. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  426. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  427. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  428. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  429. package/specialists/org/groups/engineering-group.json +2 -6
  430. package/specialists/org/groups/governance-group.json +2 -3
  431. package/specialists/org/groups/operations-group.json +5 -8
  432. package/specialists/org/groups/product-group.json +4 -8
  433. package/specialists/org/groups/quality-group.json +3 -7
  434. package/specialists/org/groups/strategy-group.json +6 -9
  435. package/specialists/org/models.json.example +8 -0
  436. package/specialists/org/scopes/creative.json +6 -1
  437. package/specialists/org/scopes/operations.json +7 -1
  438. package/specialists/org/scopes/research.json +6 -1
  439. package/specialists/org/scopes/rnd.json +7 -1
  440. package/specialists/org/specialists/cx-architect.json +27 -8
  441. package/specialists/org/specialists/cx-designer.json +22 -8
  442. package/specialists/org/specialists/cx-engineer.json +71 -7
  443. package/specialists/org/specialists/cx-operations.json +89 -15
  444. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  445. package/specialists/org/specialists/cx-product-manager.json +28 -9
  446. package/specialists/org/specialists/cx-qa.json +16 -6
  447. package/specialists/org/specialists/cx-researcher.json +40 -7
  448. package/specialists/org/specialists/cx-reviewer.json +61 -11
  449. package/specialists/org/specialists/cx-security.json +30 -10
  450. package/specialists/org/teams/design-team.json +3 -4
  451. package/specialists/org/teams/engineering-team.json +3 -10
  452. package/specialists/org/teams/governance-team.json +3 -5
  453. package/specialists/org/teams/operations-team.json +6 -12
  454. package/specialists/org/teams/product-management-team.json +2 -3
  455. package/specialists/org/teams/quality-team.json +4 -12
  456. package/specialists/org/teams/research-team.json +3 -3
  457. package/specialists/org/teams/strategy-team.json +7 -14
  458. package/specialists/prompts/cx-architect.md +20 -1
  459. package/specialists/prompts/cx-data-analyst.md +1 -1
  460. package/specialists/prompts/cx-designer.md +12 -4
  461. package/specialists/prompts/cx-engineer.md +15 -1
  462. package/specialists/prompts/cx-operations.md +14 -2
  463. package/specialists/prompts/cx-orchestrator.md +9 -5
  464. package/specialists/prompts/cx-product-manager.md +5 -1
  465. package/specialists/prompts/cx-qa.md +6 -2
  466. package/specialists/prompts/cx-researcher.md +25 -30
  467. package/specialists/prompts/cx-reviewer.md +19 -1
  468. package/specialists/prompts/cx-security.md +5 -1
  469. package/templates/distribution/construct-brand.typ +123 -59
  470. package/templates/distribution/construct-decision.typ +6 -3
  471. package/templates/distribution/construct-pdf.typ +11 -4
  472. package/templates/distribution/construct-prd.typ +6 -3
  473. package/templates/distribution/construct-research.typ +7 -4
  474. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  475. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  476. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  477. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  478. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  479. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  480. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  481. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  482. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  483. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  484. package/specialists/org/specialists/cx-accessibility.json +0 -69
  485. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  486. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  487. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  488. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  489. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  490. package/specialists/org/specialists/cx-evaluator.json +0 -69
  491. package/specialists/org/specialists/cx-explorer.json +0 -69
  492. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  493. package/specialists/org/specialists/cx-oracle.json +0 -46
  494. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  495. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  496. package/specialists/org/specialists/cx-release-manager.json +0 -77
  497. package/specialists/org/specialists/cx-sre.json +0 -94
  498. package/specialists/org/specialists/cx-test-automation.json +0 -69
  499. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  500. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  501. package/specialists/org/teams/accessibility-team.json +0 -39
  502. package/specialists/org/teams/ux-research-team.json +0 -39
  503. package/specialists/prompts/cx-accessibility.md +0 -55
  504. package/specialists/prompts/cx-ai-engineer.md +0 -124
  505. package/specialists/prompts/cx-business-strategist.md +0 -58
  506. package/specialists/prompts/cx-data-engineer.md +0 -55
  507. package/specialists/prompts/cx-devil-advocate.md +0 -59
  508. package/specialists/prompts/cx-docs-keeper.md +0 -164
  509. package/specialists/prompts/cx-evaluator.md +0 -49
  510. package/specialists/prompts/cx-explorer.md +0 -71
  511. package/specialists/prompts/cx-legal-compliance.md +0 -58
  512. package/specialists/prompts/cx-oracle.md +0 -98
  513. package/specialists/prompts/cx-platform-engineer.md +0 -97
  514. package/specialists/prompts/cx-rd-lead.md +0 -59
  515. package/specialists/prompts/cx-release-manager.md +0 -54
  516. package/specialists/prompts/cx-sre.md +0 -111
  517. package/specialists/prompts/cx-test-automation.md +0 -55
  518. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  519. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -0,0 +1,119 @@
1
+ /**
2
+ * lib/providers/contract/adapters/jira/transport.mjs — real Jira Cloud REST
3
+ * v3 transport for the governed write adapter.
4
+ *
5
+ * Implements the `jiraTransport` shape governed-write.mjs depends on:
6
+ * `fetchCreatemeta`, `createIssue`, `createComment`, `searchIssues`. Kept
7
+ * separate from governed-write.mjs so tests can inject a fake transport
8
+ * (tests/fakes/fake-jira.mjs) without any network dependency, and separate
9
+ * from the read-oriented adapter in ./index.mjs so this write path never
10
+ * shares mutable state with the read path.
11
+ *
12
+ * Auth: API token via JIRA_URL + JIRA_EMAIL + JIRA_TOKEN env vars, or
13
+ * passed directly via config.
14
+ */
15
+
16
+ import { AuthError, RateLimitError } from '../../errors.mjs';
17
+ import { guardedFetch } from '../../../../net-guard.mjs';
18
+
19
+ export function createJiraTransport(config = {}) {
20
+ const baseUrl = (config.baseUrl ?? process.env.JIRA_URL ?? '').replace(/\/$/, '');
21
+ const email = config.email ?? process.env.JIRA_EMAIL;
22
+ const token = config.token ?? process.env.JIRA_TOKEN;
23
+
24
+ if (!baseUrl || !email || !token) {
25
+ throw new AuthError(
26
+ 'Jira transport requires JIRA_URL, JIRA_EMAIL, and JIRA_TOKEN (or config.baseUrl/email/token)',
27
+ { provider: 'jira' },
28
+ );
29
+ }
30
+
31
+ const auth = `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
32
+
33
+ // Egress runs through the N7 guard (SSRF/rebinding). A self-hosted instance on
34
+ // a private address is the only legitimate private target, so it is an
35
+ // explicit, audited opt-in — never the default.
36
+ const allowPrivate = config.allowPrivateEgress ?? process.env.CONSTRUCT_NET_ALLOW_PRIVATE_EGRESS === '1';
37
+
38
+ async function request(path, { method = 'GET', body } = {}) {
39
+ const res = await guardedFetch(`${baseUrl}${path}`, {
40
+ method,
41
+ headers: {
42
+ Authorization: auth,
43
+ 'Content-Type': 'application/json',
44
+ Accept: 'application/json',
45
+ },
46
+ body: body ? JSON.stringify(body) : undefined,
47
+ }, { allowPrivate });
48
+
49
+ if (res.status === 429) {
50
+ const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60', 10);
51
+ throw new RateLimitError('Jira rate limit hit', { provider: 'jira', retryAfter });
52
+ }
53
+ if (!res.ok) {
54
+ const text = await res.text().catch(() => '');
55
+ const err = new Error(`Jira API ${method} ${path} failed: ${res.status} ${text.slice(0, 300)}`);
56
+ err.status = res.status;
57
+ throw err;
58
+ }
59
+ if (res.status === 204) return null;
60
+ return res.json();
61
+ }
62
+
63
+ return {
64
+ async fetchCreatemeta(projectKey, issueTypeName) {
65
+ const params = new URLSearchParams({
66
+ projectKeys: projectKey,
67
+ issuetypeNames: issueTypeName,
68
+ expand: 'projects.issuetypes.fields',
69
+ });
70
+ return request(`/rest/api/3/issue/createmeta?${params.toString()}`);
71
+ },
72
+
73
+ async createIssue({ project, issueType, summary, description, labels, assignee }) {
74
+ const body = {
75
+ fields: {
76
+ project: { key: project },
77
+ issuetype: { name: issueType ?? 'Task' },
78
+ summary,
79
+ ...(description !== undefined ? { description } : {}),
80
+ ...(assignee ? { assignee: { accountId: assignee } } : {}),
81
+ ...(labels ? { labels } : {}),
82
+ },
83
+ };
84
+ const data = await request('/rest/api/3/issue', { method: 'POST', body });
85
+ return {
86
+ id: data.id,
87
+ key: data.key,
88
+ url: `${baseUrl}/browse/${data.key}`,
89
+ };
90
+ },
91
+
92
+ async createComment(issueKey, adfBody) {
93
+ const data = await request(`/rest/api/3/issue/${issueKey}/comment`, {
94
+ method: 'POST',
95
+ body: { body: adfBody },
96
+ });
97
+ return {
98
+ id: data.id,
99
+ url: `${baseUrl}/browse/${issueKey}?focusedCommentId=${data.id}`,
100
+ };
101
+ },
102
+
103
+ async searchIssues(jql) {
104
+ const query = typeof jql === 'string' ? jql : (jql?.jql ?? '');
105
+ const data = await request('/rest/api/3/search', {
106
+ method: 'POST',
107
+ body: { jql: query, maxResults: 50, fields: ['summary', 'status'] },
108
+ });
109
+ return (data.issues ?? []).map((issue) => ({
110
+ id: issue.id,
111
+ key: issue.key,
112
+ summary: issue.fields?.summary,
113
+ url: `${baseUrl}/browse/${issue.key}`,
114
+ }));
115
+ },
116
+ };
117
+ }
118
+
119
+ export default createJiraTransport;
@@ -42,8 +42,23 @@
42
42
  *
43
43
  * Glob matching for `*Glob` fields follows minimatch-style single-segment
44
44
  * patterns where `*` matches any sequence of non-slash characters.
45
+ *
46
+ * ADR-0060 filter block (LMCP-B11): `validateFilterConfig(providerName,
47
+ * filter)` validates a `{ scope, predicates, nativeQuery }` block against
48
+ * `SCOPE_FIELDS_BY_PROVIDER` and the normalized predicate keys in
49
+ * `lib/providers/filter-schema.mjs`, throwing on any key the provider kind
50
+ * does not declare — fail closed at poll time, never a silent no-op.
51
+ * `matchesFilter(item, filter, providerName)` re-evaluates `scope` and
52
+ * `predicates` plane-side against a fetched item; this is the mandatory
53
+ * post-fetch backstop even when a provider pushed the same filter down
54
+ * server-side (Jira JQL, etc.). `filterHash(filter)` derives the stable
55
+ * digest recorded in the per-poll audit line.
45
56
  */
46
57
 
58
+ import crypto from 'node:crypto';
59
+
60
+ import { PREDICATE_KEYS, SCOPE_KEYS, STATUS_CATEGORIES, TOP_LEVEL_KEYS } from './filter-schema.mjs';
61
+
47
62
  export const ALLOWLIST_SCHEMA = Object.freeze({
48
63
  github: { fields: ['org', 'repoAllowlist', 'repoAllowGlob'] },
49
64
  'atlassian-jira': { fields: ['instance', 'projectAllowlist'] },
@@ -98,6 +113,198 @@ export function validateAllowlist(providerName, target, config = {}) {
98
113
  };
99
114
  }
100
115
 
116
+ // ─── ADR-0060 provider filter block (scope + predicates + nativeQuery) ──────
117
+
118
+ // Which `scope` container keys and passthrough capability each provider kind
119
+ // declares. Mirrors the dead *Allowlist fields ALLOWLIST_SCHEMA carried
120
+ // per-provider, now under the one normalized `scope` container.
121
+
122
+ export const SCOPE_FIELDS_BY_PROVIDER = Object.freeze({
123
+ github: { scope: ['repos', 'repoGlob'], nativeQuery: true },
124
+ 'atlassian-jira': { scope: ['projects'], nativeQuery: true },
125
+ jira: { scope: ['projects'], nativeQuery: true },
126
+ 'atlassian-confluence': { scope: ['spaces'], nativeQuery: false },
127
+ slack: { scope: ['channels'], nativeQuery: false },
128
+ salesforce: { scope: ['projects'], nativeQuery: true },
129
+ });
130
+
131
+ function isPlainObject(value) {
132
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
133
+ }
134
+
135
+ /**
136
+ * Validate an ADR-0060 filter block against the scope fields and predicate
137
+ * keys the given provider kind declares. Any unknown top-level key, unknown
138
+ * scope key, unknown predicate key, or `nativeQuery` on a provider that
139
+ * doesn't support passthrough throws — fail closed, never a silent no-op.
140
+ * Returns the block unchanged (for chaining) when it is valid.
141
+ */
142
+ export function validateFilterConfig(providerName, filter) {
143
+ if (filter == null) return filter;
144
+ if (!isPlainObject(filter)) {
145
+ throw new Error(`provider filter (${providerName}): filter must be an object`);
146
+ }
147
+
148
+ for (const key of Object.keys(filter)) {
149
+ if (!TOP_LEVEL_KEYS.includes(key)) {
150
+ throw new Error(`provider filter (${providerName}): unknown filter key "${key}". Allowed: ${TOP_LEVEL_KEYS.join(', ')}`);
151
+ }
152
+ }
153
+
154
+ const allowed = SCOPE_FIELDS_BY_PROVIDER[providerName] ?? { scope: SCOPE_KEYS, nativeQuery: true };
155
+
156
+ if (filter.scope !== undefined) {
157
+ if (!isPlainObject(filter.scope)) {
158
+ throw new Error(`provider filter (${providerName}): scope must be an object`);
159
+ }
160
+ for (const key of Object.keys(filter.scope)) {
161
+ if (!SCOPE_KEYS.includes(key)) {
162
+ throw new Error(`provider filter (${providerName}): unknown scope key "${key}". Allowed: ${SCOPE_KEYS.join(', ')}`);
163
+ }
164
+ if (!allowed.scope.includes(key)) {
165
+ throw new Error(`provider filter (${providerName}): scope key "${key}" is not supported by this provider kind. Allowed: ${allowed.scope.join(', ') || '(none)'}`);
166
+ }
167
+ if (!Array.isArray(filter.scope[key])) {
168
+ throw new Error(`provider filter (${providerName}): scope.${key} must be an array`);
169
+ }
170
+ }
171
+ }
172
+
173
+ if (filter.predicates !== undefined) {
174
+ if (!isPlainObject(filter.predicates)) {
175
+ throw new Error(`provider filter (${providerName}): predicates must be an object`);
176
+ }
177
+ for (const key of Object.keys(filter.predicates)) {
178
+ if (!PREDICATE_KEYS.includes(key)) {
179
+ throw new Error(`provider filter (${providerName}): unknown predicate key "${key}". Allowed: ${PREDICATE_KEYS.join(', ')}`);
180
+ }
181
+ if (key === 'statusCategory') {
182
+ const values = filter.predicates[key];
183
+ if (!Array.isArray(values) || values.some((v) => !STATUS_CATEGORIES.includes(v))) {
184
+ throw new Error(`provider filter (${providerName}): predicates.statusCategory must be an array drawn from ${STATUS_CATEGORIES.join(', ')}`);
185
+ }
186
+ } else if (key === 'updatedSince') {
187
+ if (typeof filter.predicates[key] !== 'string' || !filter.predicates[key]) {
188
+ throw new Error(`provider filter (${providerName}): predicates.updatedSince must be a non-empty string`);
189
+ }
190
+ } else if (!Array.isArray(filter.predicates[key])) {
191
+ throw new Error(`provider filter (${providerName}): predicates.${key} must be an array`);
192
+ }
193
+ }
194
+ }
195
+
196
+ if (filter.nativeQuery !== undefined) {
197
+ if (typeof filter.nativeQuery !== 'string' || !filter.nativeQuery) {
198
+ throw new Error(`provider filter (${providerName}): nativeQuery must be a non-empty string`);
199
+ }
200
+ if (!allowed.nativeQuery) {
201
+ throw new Error(`provider filter (${providerName}): nativeQuery passthrough is not supported by this provider kind`);
202
+ }
203
+ }
204
+
205
+ return filter;
206
+ }
207
+
208
+ /**
209
+ * Stable content hash of an effective filter block, recorded in the per-poll
210
+ * audit line so operators can tell whether the enforced filter changed
211
+ * between polls without diffing the full block.
212
+ */
213
+ export function filterHash(filter) {
214
+ const normalized = filter == null ? {} : filter;
215
+ return crypto.createHash('sha256').update(JSON.stringify(normalized, Object.keys(normalized).sort())).digest('hex').slice(0, 16);
216
+ }
217
+
218
+ function parseUpdatedSince(value) {
219
+ if (!value) return null;
220
+ const isoDuration = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?)?$/.exec(value);
221
+ if (isoDuration) {
222
+ const days = Number(isoDuration[1] ?? 0);
223
+ const hours = Number(isoDuration[2] ?? 0);
224
+ const minutes = Number(isoDuration[3] ?? 0);
225
+ const ms = ((days * 24 + hours) * 60 + minutes) * 60_000;
226
+ return Date.now() - ms;
227
+ }
228
+ const absolute = Date.parse(value);
229
+ return Number.isNaN(absolute) ? null : absolute;
230
+ }
231
+
232
+ function scopeContainerValue(item, key) {
233
+ if (key === 'projects') return item.project ?? null;
234
+ if (key === 'repos' || key === 'repoGlob') return item.repo ?? null;
235
+ if (key === 'channels') return item.channelId ?? item.channel ?? null;
236
+ if (key === 'spaces') return item.space ?? item.spaceKey ?? null;
237
+ return null;
238
+ }
239
+
240
+ function matchesScope(item, scope) {
241
+ if (!scope) return true;
242
+ for (const key of SCOPE_KEYS) {
243
+ const constraint = scope[key];
244
+ if (!Array.isArray(constraint) || constraint.length === 0) continue;
245
+ const value = scopeContainerValue(item, key);
246
+ if (value == null) return false;
247
+ if (key === 'repoGlob') {
248
+ if (!constraint.some((pattern) => globMatch(pattern, value))) return false;
249
+ } else if (!constraint.includes(value)) {
250
+ return false;
251
+ }
252
+ }
253
+ return true;
254
+ }
255
+
256
+ function matchesPredicates(item, predicates) {
257
+ if (!predicates) return true;
258
+
259
+ if (Array.isArray(predicates.assignee) && predicates.assignee.length > 0) {
260
+ if (!predicates.assignee.includes(item.assignee)) return false;
261
+ }
262
+ if (Array.isArray(predicates.statusCategory) && predicates.statusCategory.length > 0) {
263
+ const normalized = normalizeStatusCategory(item.statusCategory ?? item.status);
264
+ if (!predicates.statusCategory.includes(normalized)) return false;
265
+ }
266
+ if (Array.isArray(predicates.priority) && predicates.priority.length > 0) {
267
+ if (!predicates.priority.includes(item.priority)) return false;
268
+ }
269
+ if (Array.isArray(predicates.label) && predicates.label.length > 0) {
270
+ const itemLabels = Array.isArray(item.labels) ? item.labels : [];
271
+ if (!predicates.label.some((l) => itemLabels.includes(l))) return false;
272
+ }
273
+ if (typeof predicates.updatedSince === 'string' && predicates.updatedSince) {
274
+ const threshold = parseUpdatedSince(predicates.updatedSince);
275
+ const updatedAt = item.updatedAt ? Date.parse(item.updatedAt) : NaN;
276
+ if (threshold != null && (Number.isNaN(updatedAt) || updatedAt < threshold)) return false;
277
+ }
278
+ return true;
279
+ }
280
+
281
+ /**
282
+ * Normalize a provider-native status string to the ADR-0060 statusCategory
283
+ * enum (to-do | in-progress | done) so the same predicate expresses
284
+ * identically across Jira, GitHub, and Salesforce.
285
+ */
286
+ export function normalizeStatusCategory(status) {
287
+ if (!status) return null;
288
+ const s = String(status).toLowerCase();
289
+ if (/done|closed|resolved|complete/.test(s)) return 'done';
290
+ if (/progress|review|doing|active/.test(s)) return 'in-progress';
291
+ if (/to.?do|open|backlog|new|triage/.test(s)) return 'to-do';
292
+ return null;
293
+ }
294
+
295
+ /**
296
+ * Plane-side re-evaluation of an ADR-0060 filter against a fetched item.
297
+ * Mandatory universal fallback: runs even when the provider already pushed
298
+ * the same filter down server-side, and is the sole enforcement point for
299
+ * providers with no server-side filtering. `nativeQuery` has no plane-side
300
+ * equivalent (opaque passthrough) — its effect is assumed already applied
301
+ * by the provider fetch; `scope` and `predicates` are conjoined here.
302
+ */
303
+ export function matchesFilter(item, filter) {
304
+ if (filter == null) return true;
305
+ return matchesScope(item, filter.scope) && matchesPredicates(item, filter.predicates);
306
+ }
307
+
101
308
  export const CAPABILITIES = Object.freeze([
102
309
  'read',
103
310
  'search',
@@ -11,6 +11,7 @@ import fs from 'node:fs';
11
11
  import os from 'node:os';
12
12
  import { spawnSync } from 'node:child_process';
13
13
  import { hasAnySecret } from './secret-resolver.mjs';
14
+ import { locateOpBinary } from './op-locate.mjs';
14
15
  import { API_KEY_CREDENTIALS, primaryEnvVar } from './credential-catalog.mjs';
15
16
  import {
16
17
  discoverAlternateRawForCredential,
@@ -36,7 +37,9 @@ export function commandExists(command) {
36
37
  }
37
38
 
38
39
  function defaultOpRun(args) {
39
- return spawnSync('op', args, { encoding: 'utf8', timeout: 12_000 });
40
+ const opPath = locateOpBinary();
41
+ if (!opPath) return { status: 1, stdout: '', stderr: 'op not found' };
42
+ return spawnSync(opPath, args, { encoding: 'utf8', timeout: 12_000 });
40
43
  }
41
44
 
42
45
  export function listOpItems({ opRun = defaultOpRun } = {}) {
@@ -122,17 +125,17 @@ export function ensureConstructCredentials({
122
125
 
123
126
  const missing = missingCredentials({ env, cwd, home });
124
127
  if (missing.length === 0) {
125
- lastBootstrapResult = { linked: [], opAvailable: commandExists('op') };
128
+ lastBootstrapResult = { linked: [], opAvailable: locateOpBinary() !== null };
126
129
  return lastBootstrapResult;
127
130
  }
128
131
 
129
132
  if (!autoLink) {
130
- lastBootstrapResult = { linked: [], opAvailable: commandExists('op'), pending: missing.map((e) => e.id) };
133
+ lastBootstrapResult = { linked: [], opAvailable: locateOpBinary() !== null, pending: missing.map((e) => e.id) };
131
134
  return lastBootstrapResult;
132
135
  }
133
136
 
134
137
  const linked = [];
135
- const opAvailable = opRun !== defaultOpRun || commandExists('op');
138
+ const opAvailable = opRun !== defaultOpRun || locateOpBinary() !== null;
136
139
  if (!opAvailable) {
137
140
  lastBootstrapResult = { linked, opAvailable: false };
138
141
  return lastBootstrapResult;
@@ -15,8 +15,20 @@ export function openCodeConfigPath(homeDir = os.homedir()) {
15
15
  return path.join(homeDir, '.config', 'opencode', 'opencode.json');
16
16
  }
17
17
 
18
- function isPlaceholder(value) {
19
- return !value || String(value).includes('__OPENROUTER_API_KEY__');
18
+ // Matches the literal reference forms Construct and OpenCode write into
19
+ // config when no real credential is resolved yet, so presence checks don't
20
+ // mistake an unresolved placeholder for a configured key.
21
+ const PLACEHOLDER_PATTERNS = [
22
+ /^__[A-Z0-9_]+__$/,
23
+ /^\{env:[A-Z0-9_]+\}$/,
24
+ /^\$\{[A-Z0-9_]+\}$/,
25
+ /^\$\{env:[A-Z0-9_]+\}$/,
26
+ ];
27
+
28
+ export function isPlaceholder(value) {
29
+ if (!value) return true;
30
+ const trimmed = String(value).trim();
31
+ return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(trimmed));
20
32
  }
21
33
 
22
34
  export function readRawFromOpenCodeProvider(providerName, configPath = openCodeConfigPath()) {
@@ -0,0 +1,273 @@
1
+ /**
2
+ * lib/providers/directory/index.mjs — Local directory data-source provider.
3
+ *
4
+ * Provides read/search capabilities over a configured local directory with
5
+ * include/exclude glob patterns and file size limits. Security: all paths
6
+ * resolve and remain within the configured root; symlink escape and
7
+ * traversal (../) are rejected.
8
+ *
9
+ * Capabilities: read, search.
10
+ *
11
+ * Config (per call):
12
+ * - root: absolute or relative path to the directory root (required)
13
+ * - include: array of glob patterns (optional, defaults to ["**\/*"])
14
+ * - exclude: array of glob patterns to filter out (optional)
15
+ * - maxFileKB: integer max file size in KB (default 1024, max 10240)
16
+ *
17
+ * Glob support is the in-tree subset (ADR-0001, lib/rules-delivery.mjs):
18
+ * `**\/` spans directories, `*` stays within a segment, and a pattern without
19
+ * a slash matches basenames (`*.md` matches nested files).
20
+ */
21
+
22
+ import { existsSync, statSync, readdirSync, readFileSync } from 'node:fs';
23
+ import { resolve, relative, isAbsolute } from 'node:path';
24
+ import { globToRegExp } from '../../rules-delivery.mjs';
25
+
26
+ /**
27
+ * Verify that path is within root and not a symlink escape.
28
+ * Returns { ok: true } or { ok: false, reason: '...' }.
29
+ */
30
+ function validatePathSecurity(root, checkPath) {
31
+ try {
32
+ const realRoot = statSync(root).ino;
33
+ const stats = statSync(checkPath);
34
+
35
+ if (stats.isSymbolicLink()) {
36
+ return { ok: false, reason: 'symlink not allowed' };
37
+ }
38
+
39
+ const realPath = statSync(checkPath).ino;
40
+ if (realPath !== realRoot) {
41
+ const rel = relative(root, checkPath);
42
+ if (rel.startsWith('..')) {
43
+ return { ok: false, reason: 'path escape detected' };
44
+ }
45
+ }
46
+
47
+ return { ok: true };
48
+ } catch (err) {
49
+ return { ok: false, reason: err.message };
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Resolve and validate a path within root. Returns { ok, path } or
55
+ * { ok: false, reason }.
56
+ */
57
+ function resolvePath(root, candidate) {
58
+ const rootPath = isAbsolute(root) ? root : resolve(process.cwd(), root);
59
+
60
+ if (!existsSync(rootPath)) {
61
+ return { ok: false, reason: `root does not exist: ${rootPath}` };
62
+ }
63
+
64
+ const stat = statSync(rootPath);
65
+ if (!stat.isDirectory()) {
66
+ return { ok: false, reason: `root is not a directory: ${rootPath}` };
67
+ }
68
+
69
+ const resolved = isAbsolute(candidate)
70
+ ? candidate
71
+ : resolve(rootPath, candidate);
72
+
73
+ const validation = validatePathSecurity(rootPath, resolved);
74
+ if (!validation.ok) {
75
+ return { ok: false, reason: validation.reason };
76
+ }
77
+
78
+ return { ok: true, path: resolved, root: rootPath };
79
+ }
80
+
81
+ // A pattern without a slash tests the basename so `*.md` matches nested files,
82
+ // mirroring the matchBase behavior read() and search() have always documented.
83
+
84
+ function compileMatchers(patterns) {
85
+ const compiled = patterns.map((pat) => ({ re: globToRegExp(pat), baseOnly: !pat.includes('/') }));
86
+ return (relativePath, name) => compiled.some(({ re, baseOnly }) => re.test(baseOnly ? name : relativePath));
87
+ }
88
+
89
+ /**
90
+ * Walk directory recursively, matching include/exclude globs.
91
+ */
92
+ function walkDir(dirPath, { include = ['**/*'], exclude = [] }) {
93
+ const results = [];
94
+ const stat = statSync(dirPath);
95
+ if (!stat.isDirectory()) return results;
96
+
97
+ const includes = compileMatchers(include);
98
+ const excludes = compileMatchers(exclude);
99
+
100
+ function walk(dir, prefix = '') {
101
+ let entries;
102
+ try {
103
+ entries = readdirSync(dir, { withFileTypes: true });
104
+ } catch {
105
+ return;
106
+ }
107
+
108
+ for (const entry of entries) {
109
+ const fullPath = resolve(dir, entry.name);
110
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
111
+
112
+ if (entry.isDirectory()) {
113
+ walk(fullPath, relativePath);
114
+ } else if (entry.isFile()) {
115
+ if (includes(relativePath, entry.name) && !excludes(relativePath, entry.name)) {
116
+ results.push({ path: fullPath, relativePath, name: entry.name });
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ walk(dirPath);
123
+ return results;
124
+ }
125
+
126
+ /**
127
+ * Filter files by size constraint.
128
+ */
129
+ function filterBySize(items, maxFileSizeKB) {
130
+ const maxBytes = maxFileSizeKB * 1024;
131
+ return items.filter((item) => {
132
+ try {
133
+ const stat = statSync(item.path);
134
+ return stat.size <= maxBytes;
135
+ } catch {
136
+ return false;
137
+ }
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Read all matching files from the directory.
143
+ */
144
+ async function readAllFiles(config) {
145
+ const root = config?.root;
146
+ if (!root) throw new Error('directory.read: config.root required');
147
+
148
+ const { ok, path, root: rootPath } = resolvePath(root, '.');
149
+ if (!ok) throw new Error(`directory.read: ${path}`);
150
+
151
+ const include = Array.isArray(config.include) ? config.include : ['**/*'];
152
+ const exclude = Array.isArray(config.exclude) ? config.exclude : [];
153
+ const maxKB = Math.min(config?.maxFileKB || 1024, 10240);
154
+
155
+ const items = walkDir(rootPath, { include, exclude });
156
+ const filtered = filterBySize(items, maxKB);
157
+
158
+ return filtered.map((item) => ({
159
+ path: item.relativePath,
160
+ name: item.name,
161
+ mtime: statSync(item.path).mtimeMs,
162
+ size: statSync(item.path).size,
163
+ }));
164
+ }
165
+
166
+ /**
167
+ * Search for a substring within matching files.
168
+ */
169
+ async function searchFiles(config, query) {
170
+ if (!query || typeof query !== 'string') {
171
+ throw new Error('directory.search: query required (substring or glob pattern)');
172
+ }
173
+
174
+ const root = config?.root;
175
+ if (!root) throw new Error('directory.search: config.root required');
176
+
177
+ const { ok, path, root: rootPath } = resolvePath(root, '.');
178
+ if (!ok) throw new Error(`directory.search: ${path}`);
179
+
180
+ const include = Array.isArray(config.include) ? config.include : ['**/*'];
181
+ const exclude = Array.isArray(config.exclude) ? config.exclude : [];
182
+ const maxKB = Math.min(config?.maxFileKB || 1024, 10240);
183
+
184
+ const items = walkDir(rootPath, { include, exclude });
185
+ const filtered = filterBySize(items, maxKB);
186
+
187
+ const results = [];
188
+
189
+ for (const item of filtered) {
190
+ try {
191
+ const content = readFileSync(item.path, 'utf8');
192
+ if (content.includes(query)) {
193
+ results.push({
194
+ path: item.relativePath,
195
+ name: item.name,
196
+ mtime: statSync(item.path).mtimeMs,
197
+ size: statSync(item.path).size,
198
+ preview: content.slice(0, 200),
199
+ });
200
+ }
201
+ } catch {
202
+ // Skip files we cannot read
203
+ }
204
+ }
205
+
206
+ return results;
207
+ }
208
+
209
+ export function create({ env = process.env } = {}) {
210
+ return {
211
+ meta: {
212
+ id: 'directory',
213
+ displayName: 'Local Directory',
214
+ capabilities: ['read', 'search'],
215
+ description: 'Read and search files in a local directory.',
216
+ },
217
+
218
+ configSchema: {
219
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
220
+ type: 'object',
221
+ properties: {
222
+ root: {
223
+ type: 'string',
224
+ description: 'Path to the root directory (absolute or relative to cwd)',
225
+ },
226
+ include: {
227
+ type: 'array',
228
+ items: { type: 'string' },
229
+ description: 'Glob patterns to include (default: ["**/*"])',
230
+ default: ['**/*'],
231
+ },
232
+ exclude: {
233
+ type: 'array',
234
+ items: { type: 'string' },
235
+ description: 'Glob patterns to exclude (default: [])',
236
+ default: [],
237
+ },
238
+ maxFileKB: {
239
+ type: 'integer',
240
+ minimum: 1,
241
+ maximum: 10240,
242
+ default: 1024,
243
+ description: 'Maximum file size in KB (default 1024)',
244
+ },
245
+ },
246
+ required: ['root'],
247
+ },
248
+
249
+ async health(config) {
250
+ const root = config?.root;
251
+ if (!root) {
252
+ return { ok: false, detail: 'config.root not set' };
253
+ }
254
+
255
+ const { ok, reason, root: rootPath } = resolvePath(root, '.');
256
+ if (!ok) {
257
+ return { ok: false, detail: reason };
258
+ }
259
+
260
+ try {
261
+ const entries = readdirSync(rootPath);
262
+ return { ok: true, detail: `directory readable (${entries.length} entries)` };
263
+ } catch (err) {
264
+ return { ok: false, detail: err.message };
265
+ }
266
+ },
267
+
268
+ read: readAllFiles,
269
+ search: searchFiles,
270
+ };
271
+ }
272
+
273
+ export default create;