@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,109 @@
1
+ /**
2
+ * Deployment-mode capability registry.
3
+ *
4
+ * Maps each deployment mode (solo / team / enterprise) to its declared capabilities
5
+ * and their current implementation status. This is the single authoritative source
6
+ * for what a mode promises versus what is actually wired up at runtime.
7
+ *
8
+ * Status values:
9
+ * 'implemented' — fully wired; production-safe.
10
+ * 'stub' — code path exists but returns null / falls back silently.
11
+ * 'not-implemented' — no code path exists yet; will throw or degrade if invoked.
12
+ *
13
+ * How to add a new capability:
14
+ * 1. Add an entry to the relevant mode array in CAPABILITY_REGISTRY below.
15
+ * 2. Set status to 'not-implemented' or 'stub' until the implementation lands.
16
+ * 3. Flip to 'implemented' in the same PR that wires the real code.
17
+ * 4. Add a CHANGELOG entry referencing this file and the wiring commit.
18
+ */
19
+
20
+ export const CAPABILITY_REGISTRY = {
21
+ solo: [
22
+ { id: 'filesystem-queue', label: 'Filesystem task queue', status: 'implemented' },
23
+ { id: 'local-memory', label: 'Local memory', status: 'implemented' },
24
+ { id: 'embedded-lancedb', label: 'Embedded LanceDB vector store', status: 'implemented' },
25
+ { id: 'direct-mcp', label: 'Direct MCP dispatch', status: 'implemented' },
26
+ ],
27
+ team: [
28
+ { id: 'postgres-queue', label: 'Postgres task queue', status: 'implemented' },
29
+ { id: 'shared-memory', label: 'Shared memory store', status: 'stub' },
30
+ { id: 'worker-heartbeat', label: 'Worker identity and heartbeat', status: 'implemented' },
31
+ { id: 'docker-workers', label: 'Docker worker pool', status: 'not-implemented' },
32
+ { id: 'central-telemetry', label: 'Central telemetry', status: 'stub' },
33
+ { id: 'brokered-mcp', label: 'Brokered MCP dispatch', status: 'stub' },
34
+ ],
35
+ enterprise: [
36
+ { id: 'tenant-isolation', label: 'Tenant isolation', status: 'not-implemented' },
37
+ { id: 'rbac', label: 'RBAC/ABAC', status: 'not-implemented' },
38
+ { id: 'worker-heartbeat', label: 'Worker identity and heartbeat', status: 'implemented' },
39
+ { id: 'isolated-workers', label: 'Isolated worker containers', status: 'not-implemented' },
40
+ { id: 'signed-mcp-allowlists', label: 'Signed MCP allowlists', status: 'not-implemented' },
41
+ { id: 'mandatory-audit', label: 'Mandatory audit log', status: 'implemented' },
42
+ ],
43
+ };
44
+
45
+ // Returns the capability list for the given mode, or an empty array for unknown modes.
46
+
47
+ export function getCapabilities(mode) {
48
+ return CAPABILITY_REGISTRY[mode] ?? [];
49
+ }
50
+
51
+ // Aggregates the capability list into a single readiness label.
52
+ // 'fully-implemented' — every capability is 'implemented'.
53
+ // 'degraded' — at least one is 'stub' (and none are 'not-implemented').
54
+ // 'unsupported' — at least one is 'not-implemented'.
55
+
56
+ export function getModeCapabilityStatus(mode) {
57
+ const caps = getCapabilities(mode);
58
+ if (caps.length === 0) return 'unsupported';
59
+ if (caps.some(c => c.status === 'not-implemented')) return 'unsupported';
60
+ if (caps.some(c => c.status === 'stub')) return 'degraded';
61
+ return 'fully-implemented';
62
+ }
63
+
64
+ // Returns capabilities whose status is not 'implemented'.
65
+
66
+ export function getUnsupportedCapabilities(mode) {
67
+ return getCapabilities(mode).filter(c => c.status !== 'implemented');
68
+ }
69
+
70
+ // The three valid status values, as a constant so callers don't string-compare.
71
+
72
+ export const CAPABILITY_STATUSES = ['implemented', 'stub', 'not-implemented'];
73
+
74
+ // ADR-0057 (LMCP-A7) partitions enterprise capabilities beyond the three raw
75
+ // statuses above into four status-output categories: 'active' (implemented),
76
+ // 'fail-closed' (architecturally non-trivial — must hard-error rather than
77
+ // degrade silently), 'later' (no near-term implementation path, no runtime
78
+ // effect), and 'absent' (not advertised at all, the default for any id in
79
+ // neither set). construct status and construct doctor both key off this so a
80
+ // fail-closed gap is never visually or behaviorally confused with a later one.
81
+
82
+ export const ENTERPRISE_FAIL_CLOSED_IDS = new Set(['tenant-isolation', 'isolated-workers']);
83
+ export const ENTERPRISE_LATER_IDS = new Set(['rbac', 'signed-mcp-allowlists']);
84
+
85
+ export function categorizeEnterpriseCapability(cap) {
86
+ if (cap.status === 'implemented') return 'active';
87
+ if (ENTERPRISE_FAIL_CLOSED_IDS.has(cap.id)) return 'fail-closed';
88
+ if (ENTERPRISE_LATER_IDS.has(cap.id)) return 'later';
89
+ return 'absent';
90
+ }
91
+
92
+ // Test-only injection: temporarily append a capability to the given mode(s) and return a cleanup fn.
93
+
94
+ export function _injectCapabilityForTesting({ id, label = id, modes = {} }) {
95
+ const injected = [];
96
+ for (const [mode, status] of Object.entries(modes)) {
97
+ if (!CAPABILITY_REGISTRY[mode]) CAPABILITY_REGISTRY[mode] = [];
98
+ const entry = { id, label, status };
99
+ CAPABILITY_REGISTRY[mode].push(entry);
100
+ injected.push({ mode, entry });
101
+ }
102
+ return function cleanup() {
103
+ for (const { mode, entry } of injected) {
104
+ const arr = CAPABILITY_REGISTRY[mode];
105
+ const idx = arr.indexOf(entry);
106
+ if (idx !== -1) arr.splice(idx, 1);
107
+ }
108
+ };
109
+ }
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import fs from "node:fs";
25
25
  import path from "node:path";
26
+ import { fileURLToPath } from "node:url";
26
27
  import { spawnSync } from "child_process";
27
28
  import { hasAnySecret } from "./providers/secret-resolver.mjs";
28
29
  import { isOllamaModelInstalled, toOllamaNativeModelId } from "./ollama/installed-models.mjs";
@@ -177,6 +178,21 @@ export function selectLocalEditorModel(candidates = []) {
177
178
  * - `resolve`: Function that maps selected tier values to concrete model IDs.
178
179
  *
179
180
  * Families are consulted in order; the first match wins.
181
+ *
182
+ * FALLBACK CATALOG ONLY (construct-at27): these dated ids are the last-resort
183
+ * defaults when a tier is unconfigured — runtime call sites must prefer the
184
+ * user's configured/selected models and degrade gracefully when a catalog id
185
+ * has been retired by the provider; never treat an entry here as guaranteed
186
+ * to exist.
187
+ *
188
+ * OpenRouter's free-tier rate limit is enforced per account across every
189
+ * `:free`-suffixed model, not per model id — pinning a different `:free`
190
+ * model does not avoid a 429 by itself. The `:free` ids below are
191
+ * cross-checked against a live `GET https://openrouter.ai/api/v1/models`
192
+ * response (construct-r7bp) so at least one alternate candidate remains
193
+ * live if a specific model is retired or under its own provider-side quota;
194
+ * durable retry/fallback handling of the shared account limit is
195
+ * construct-5wkl's scope, not this catalog.
180
196
  */
181
197
  const PROVIDER_FAMILY_TIERS = [
182
198
  {
@@ -236,7 +252,7 @@ const PROVIDER_FAMILY_TIERS = [
236
252
  options: {
237
253
  reasoning: ["openrouter/deepseek/deepseek-r1", "openrouter/deepseek/deepseek-v3"],
238
254
  standard: ["openrouter/deepseek/deepseek-v3", "openrouter/deepseek/deepseek-r1"],
239
- fast: ["openrouter/qwen/qwen3-coder:free", "openrouter/deepseek/deepseek-v3"],
255
+ fast: ["openrouter/qwen/qwen3-coder:free", "openrouter/meta-llama/llama-3.3-70b-instruct:free", "openrouter/deepseek/deepseek-v3"],
240
256
  },
241
257
  },
242
258
  {
@@ -246,12 +262,12 @@ const PROVIDER_FAMILY_TIERS = [
246
262
  resolve: ({ reasoning, standard, fast }) => ({
247
263
  reasoning: reasoning ?? "openrouter/qwen/qwen3-coder",
248
264
  standard: standard ?? "openrouter/qwen/qwen3-coder:free",
249
- fast: fast ?? "openrouter/qwen/qwen2.5-coder-32b-instruct",
265
+ fast: fast ?? "openrouter/qwen/qwen-2.5-coder-32b-instruct",
250
266
  }),
251
267
  options: {
252
268
  reasoning: ["openrouter/qwen/qwen3-coder", "openrouter/qwen/qwen3-coder:free"],
253
- standard: ["openrouter/qwen/qwen3-coder:free", "openrouter/qwen/qwen3-coder"],
254
- fast: ["openrouter/qwen/qwen2.5-coder-32b-instruct", "openrouter/qwen/qwen3-coder:free"],
269
+ standard: ["openrouter/qwen/qwen3-coder:free", "openrouter/qwen/qwen3-next-80b-a3b-instruct:free", "openrouter/qwen/qwen3-coder"],
270
+ fast: ["openrouter/qwen/qwen-2.5-coder-32b-instruct", "openrouter/qwen/qwen3-coder:free"],
255
271
  },
256
272
  },
257
273
  {
@@ -313,7 +329,7 @@ const PROVIDER_FAMILY_TIERS = [
313
329
  }),
314
330
  options: {
315
331
  reasoning: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder", "openrouter/deepseek/deepseek-r1"],
316
- standard: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder:free", "openrouter/google/gemini-2.0-flash-001"],
332
+ standard: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder:free", "openrouter/openai/gpt-oss-120b:free", "openrouter/google/gemini-2.0-flash-001"],
317
333
  fast: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder:free", "openrouter/meta-llama/llama-3.3-70b-instruct:free"],
318
334
  },
319
335
  },
@@ -413,8 +429,10 @@ function isProviderConfigured(familyId, env, { allowAmbient = true } = {}) {
413
429
  const varNames = PROVIDER_ENV_MAP[familyId];
414
430
  if (!varNames?.length) return false;
415
431
 
416
- // Special case: Ollama on default port (no OLLAMA_BASE_URL needed)
417
- if (familyId === 'ollama') {
432
+ // Special case: Ollama on default port (no OLLAMA_BASE_URL needed). Ambient-gated:
433
+ // a hermetic caller (allowAmbient: false functional tests, credential-fallback
434
+ // resolution against a synthetic env) must not probe the machine's local daemon.
435
+ if (familyId === 'ollama' && allowAmbient) {
418
436
  try {
419
437
  const r = spawnSync('curl', ['-s', '--connect-timeout', '1', '-o', '/dev/null', '-w', '%{http_code}', 'http://localhost:11434/api/tags'], { encoding: 'utf8', timeout: 3000 });
420
438
  if (r.status === 0 && r.stdout?.trim() === '200') return true;
@@ -431,8 +449,9 @@ function isProviderConfigured(familyId, env, { allowAmbient = true } = {}) {
431
449
  if (hasAnySecret(varNames, { env, allowAmbient })) return true;
432
450
 
433
451
  // GitHub Copilot authenticates via OAuth rather than an API key: a stored
434
- // device-flow credential, or a gh CLI session as a fallback, both count.
435
- if (familyId === 'github-copilot') {
452
+ // device-flow credential, or a gh CLI session as a fallback, both count. Both
453
+ // checks read machine-local state outside `env`, so they are ambient-gated too.
454
+ if (familyId === 'github-copilot' && allowAmbient) {
436
455
  if (hasCopilotCredential()) return true;
437
456
  try {
438
457
  const r = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 3000 });
@@ -1282,6 +1301,20 @@ export function setModelWithTierInference(envPath, tier, modelId, registryModels
1282
1301
  return resolved;
1283
1302
  }
1284
1303
 
1304
+ // Optional operator-supplied tier defaults, resolved under the toolkit dir
1305
+ // (CX_TOOLKIT_DIR wins, else the installed package root). No active registry
1306
+ // ships — a starter template lives at specialists/org/models.json.example — so
1307
+ // an unconfigured machine degrades honestly (no implicit model defaults) rather
1308
+ // than resolving a model whose credentials may be absent. A dropped-in
1309
+ // models.json supplies registry-sourced tier defaults that an env pin overrides.
1310
+
1311
+ const MODEL_REGISTRY_MODULE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
1312
+
1313
+ export function defaultModelRegistryPath(env = process.env) {
1314
+ const root = env.CX_TOOLKIT_DIR || MODEL_REGISTRY_MODULE_ROOT;
1315
+ return path.join(root, "specialists", "org", "models.json");
1316
+ }
1317
+
1285
1318
  export function resolveModelTiers(options = {}) {
1286
1319
  const {
1287
1320
  env = process.env,
@@ -11,9 +11,10 @@ import { loadRegistry } from '../registry/loader.mjs';
11
11
  import path from 'node:path';
12
12
  import os from 'node:os';
13
13
  import { pollFreeModels } from '../model-free-selector.mjs';
14
- import { resolveFirstSecret } from '../providers/secret-resolver.mjs';
14
+ import { resolveFirstSecret, hasAnySecret } from '../providers/secret-resolver.mjs';
15
15
  import { loadProjectConfig } from '../config/project-config.mjs';
16
16
  import { doctorRoot } from '../config/xdg.mjs';
17
+ import { loadManifestsFromDir, mergeManifests, resolveManifestDirs } from '../extensions/loader.mjs';
17
18
 
18
19
  export const MODEL_VISIBILITY_MODES = ['all_configured', 'tier_defaults', 'explicit'];
19
20
 
@@ -210,6 +211,44 @@ export function loadModelsCatalogContext({ cwd = process.cwd(), env = process.en
210
211
  return { modelsConfig, registryModels, liveModels };
211
212
  }
212
213
 
214
+ // Bridge onto the unified extension registry (LMCP-B1/B4): model provider
215
+ // families register `kind: 'model'` manifests under lib/extensions/manifests/
216
+ // alongside data-source/queue/storage manifests, so `construct provider list`
217
+ // can enumerate them through the same substrate. This is additive — it does
218
+ // not replace PROVIDER_FAMILY_TIERS (lib/model-router.mjs), which still owns
219
+ // tier resolution, live polling, and visibility filtering above.
220
+
221
+ export const MODEL_MANIFEST_KIND = 'model';
222
+
223
+ export function listModelProviderManifests({ rootDir = process.cwd(), homeDir } = {}) {
224
+ const dirs = resolveManifestDirs({ rootDir, homeDir });
225
+ const builtin = loadManifestsFromDir(dirs.builtin).manifests;
226
+ const user = loadManifestsFromDir(dirs.user).manifests;
227
+ const project = loadManifestsFromDir(dirs.project).manifests;
228
+ return mergeManifests(builtin, user, project).filter((m) => m.kind === MODEL_MANIFEST_KIND);
229
+ }
230
+
231
+ export function describeModelProviders({ rootDir = process.cwd(), homeDir, env = process.env, allowAmbient = true } = {}) {
232
+ return listModelProviderManifests({ rootDir, homeDir }).map((manifest) => {
233
+ const secretEnvKeys = Array.isArray(manifest.secretEnvKeys) ? manifest.secretEnvKeys : [];
234
+ const configured = secretEnvKeys.length === 0 || hasAnySecret(secretEnvKeys, { env, allowAmbient });
235
+ return {
236
+ id: manifest.id,
237
+ displayName: manifest.docs || manifest.id,
238
+ description: manifest.docs || null,
239
+ capabilities: Array.isArray(manifest.capabilities) ? manifest.capabilities : [],
240
+ kind: manifest.kind,
241
+ source: 'model-catalog',
242
+ health: {
243
+ ok: configured,
244
+ detail: configured
245
+ ? 'credentials configured'
246
+ : `missing env: ${secretEnvKeys.join(', ') || 'unknown'}`,
247
+ },
248
+ };
249
+ });
250
+ }
251
+
213
252
  export function isModelVisible(modelId, {
214
253
  visibility = DEFAULT_MODELS_CONFIG.visibility,
215
254
  registryModels = {},
@@ -0,0 +1,247 @@
1
+ /**
2
+ * lib/net-guard.mjs — shared SSRF / DNS-rebinding egress guard (LMCP-N7).
3
+ *
4
+ * Every outbound HTTP call Construct makes on behalf of a manifest, provider,
5
+ * or url-type MCP entry is attacker-influenced in its destination: a malicious
6
+ * manifest URL or a rebinding DNS record can point an otherwise-trusted call at
7
+ * a loopback or private-range service and pivot into the host. This guard is
8
+ * the single chokepoint that prevents that.
9
+ *
10
+ * Two properties, enforced together:
11
+ * 1. Address policy — the destination hostname is resolved and EVERY returned
12
+ * address is classified; loopback, private, link-local (incl. cloud
13
+ * metadata 169.254.169.254), CGNAT, and unspecified ranges are denied by
14
+ * default. `allowPrivate: true` is the explicit, audited opt-out for
15
+ * on-host/dev use.
16
+ * 2. Rebinding pinning — the address validated at check time is the exact
17
+ * address the socket connects to. `resolveGuardedTarget` resolves ONCE and
18
+ * returns a pinned IP; `guardedFetch` connects to that pinned IP with the
19
+ * original hostname preserved as TLS servername + Host header, so a DNS
20
+ * record that flips to a private address between check and connect can
21
+ * never move the connection — the socket never re-resolves.
22
+ *
23
+ * The security decision lives entirely in the pure, network-free
24
+ * `classifyAddress` / `resolveGuardedTarget` (DNS is injectable), so the policy
25
+ * is unit-testable without a socket; `guardedFetch` is the thin I/O shell.
26
+ */
27
+
28
+ import { request as httpsRequest } from 'node:https';
29
+ import { request as httpRequest } from 'node:http';
30
+ import { lookup as dnsLookup } from 'node:dns';
31
+ import { isIP } from 'node:net';
32
+ import { promisify } from 'node:util';
33
+ import { gunzipSync, inflateSync, brotliDecompressSync } from 'node:zlib';
34
+
35
+ const dnsLookupAll = promisify(dnsLookup);
36
+
37
+ export const DEFAULT_ALLOWED_SCHEMES = Object.freeze(['https:', 'http:']);
38
+
39
+ /** Named egress-policy rejection; `.reason` is a stable machine tag. */
40
+ export class SsrfBlockedError extends Error {
41
+ constructor(message, reason) {
42
+ super(message);
43
+ this.name = 'SsrfBlockedError';
44
+ this.reason = reason;
45
+ }
46
+ }
47
+
48
+ function ipv4ToInt(ip) {
49
+ const parts = ip.split('.').map((n) => Number(n));
50
+ if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
51
+ return ((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
52
+ }
53
+
54
+ function inCidr4(ipInt, baseIp, prefix) {
55
+ const base = ipv4ToInt(baseIp);
56
+ const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
57
+ return (ipInt & mask) === (base & mask);
58
+ }
59
+
60
+ const BLOCKED_V4 = [
61
+ ['0.0.0.0', 8, 'unspecified'],
62
+ ['10.0.0.0', 8, 'private-address'],
63
+ ['100.64.0.0', 10, 'cgnat'],
64
+ ['127.0.0.0', 8, 'loopback'],
65
+ ['169.254.0.0', 16, 'link-local'],
66
+ ['172.16.0.0', 12, 'private-address'],
67
+ ['192.0.0.0', 24, 'ietf-protocol'],
68
+ ['192.168.0.0', 16, 'private-address'],
69
+ ['198.18.0.0', 15, 'benchmark'],
70
+ ['224.0.0.0', 4, 'multicast'],
71
+ ['240.0.0.0', 4, 'reserved'],
72
+ ];
73
+
74
+ /**
75
+ * Classify a raw IP literal. Returns a stable reason tag when the address is in
76
+ * a range that must not be reached from an attacker-influenced URL, or null
77
+ * when the address is a routable public address.
78
+ *
79
+ * @param {string} ip
80
+ * @returns {string|null}
81
+ */
82
+ export function classifyAddress(ip) {
83
+ const family = isIP(ip);
84
+ if (family === 4) {
85
+ const asInt = ipv4ToInt(ip);
86
+ if (asInt === null) return 'unparseable';
87
+ for (const [base, prefix, reason] of BLOCKED_V4) {
88
+ if (inCidr4(asInt, base, prefix)) return reason;
89
+ }
90
+ return null;
91
+ }
92
+ if (family === 6) {
93
+ const norm = ip.toLowerCase().replace(/^\[|\]$/g, '');
94
+ if (norm === '::1') return 'loopback';
95
+ if (norm === '::' ) return 'unspecified';
96
+ if (norm.startsWith('fe80')) return 'link-local';
97
+ if (norm.startsWith('fc') || norm.startsWith('fd')) return 'unique-local';
98
+ // IPv4-mapped (::ffff:a.b.c.d) is classified on its embedded v4 address.
99
+ const mapped = norm.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
100
+ if (mapped) return classifyAddress(mapped[1]);
101
+ return null;
102
+ }
103
+ return 'not-an-ip';
104
+ }
105
+
106
+ /**
107
+ * Validate a URL's scheme + host and resolve its hostname to a single pinned,
108
+ * policy-checked address. Pure with respect to the network: DNS is injected via
109
+ * `lookup`. Throws SsrfBlockedError on any violation.
110
+ *
111
+ * @param {string} rawUrl
112
+ * @param {object} [opts]
113
+ * @param {(hostname: string) => Promise<Array<{address: string, family: number}>>} [opts.lookup]
114
+ * @param {boolean} [opts.allowPrivate] - permit private/loopback ranges (audited opt-out)
115
+ * @param {string[]} [opts.allowedSchemes]
116
+ * @returns {Promise<{ url: URL, pinnedAddress: string, family: number, port: number, servername: string }>}
117
+ */
118
+ export async function resolveGuardedTarget(rawUrl, opts = {}) {
119
+ const { allowPrivate = false, allowedSchemes = DEFAULT_ALLOWED_SCHEMES } = opts;
120
+ const lookup = opts.lookup ?? ((hostname) => dnsLookupAll(hostname, { all: true }));
121
+
122
+ let url;
123
+ try {
124
+ url = new URL(rawUrl);
125
+ } catch {
126
+ throw new SsrfBlockedError(`net-guard: not a valid URL: ${String(rawUrl).slice(0, 120)}`, 'invalid-url');
127
+ }
128
+
129
+ if (!allowedSchemes.includes(url.protocol)) {
130
+ throw new SsrfBlockedError(`net-guard: scheme '${url.protocol}' is not allowed`, 'blocked-scheme');
131
+ }
132
+
133
+ const hostname = url.hostname.replace(/^\[|\]$/g, '');
134
+
135
+ // A URL that already carries an IP literal skips DNS but is still classified —
136
+ // http://127.0.0.1 must be blocked exactly like a hostname resolving to it.
137
+ const literalFamily = isIP(hostname);
138
+ let addresses;
139
+ if (literalFamily) {
140
+ addresses = [{ address: hostname, family: literalFamily }];
141
+ } else {
142
+ addresses = await lookup(hostname);
143
+ if (!Array.isArray(addresses) || addresses.length === 0) {
144
+ throw new SsrfBlockedError(`net-guard: '${hostname}' did not resolve to any address`, 'no-address');
145
+ }
146
+ }
147
+
148
+ // ALL resolved addresses must pass — a hostname that returns one public and
149
+ // one private address is rejected, never partially trusted.
150
+ for (const { address } of addresses) {
151
+ if (allowPrivate) continue;
152
+ const reason = classifyAddress(address);
153
+ if (reason) {
154
+ throw new SsrfBlockedError(`net-guard: '${hostname}' resolves to a blocked address (${address}: ${reason})`, reason);
155
+ }
156
+ }
157
+
158
+ const pinned = addresses[0];
159
+ const port = url.port ? Number(url.port) : (url.protocol === 'https:' ? 443 : 80);
160
+ return { url, pinnedAddress: pinned.address, family: pinned.family, port, servername: hostname };
161
+ }
162
+
163
+ function decodeBody(buffer, encoding) {
164
+ if (!buffer.length) return buffer;
165
+ try {
166
+ if (encoding === 'gzip') return gunzipSync(buffer);
167
+ if (encoding === 'deflate') return inflateSync(buffer);
168
+ if (encoding === 'br') return brotliDecompressSync(buffer);
169
+ } catch {
170
+ return buffer;
171
+ }
172
+ return buffer;
173
+ }
174
+
175
+ function makeResponse({ statusCode, headers, bodyBuffer, url }) {
176
+ const decoded = decodeBody(bodyBuffer, String(headers['content-encoding'] ?? '').toLowerCase());
177
+ const headerMap = new Map(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), Array.isArray(v) ? v.join(', ') : v]));
178
+ return {
179
+ status: statusCode,
180
+ ok: statusCode >= 200 && statusCode < 300,
181
+ url,
182
+ headers: { get: (name) => headerMap.get(String(name).toLowerCase()) ?? null },
183
+ async text() { return decoded.toString('utf8'); },
184
+ async json() { return JSON.parse(decoded.toString('utf8') || 'null'); },
185
+ };
186
+ }
187
+
188
+ /**
189
+ * fetch-compatible egress call that enforces the guard and connects to the
190
+ * pinned, validated address (rebinding-proof). Follows redirects manually,
191
+ * re-validating every hop through the same guard. Returns a minimal Response
192
+ * ({ status, ok, headers.get, text, json }) covering what the provider
193
+ * transports use.
194
+ *
195
+ * @param {string} rawUrl
196
+ * @param {{ method?: string, headers?: Record<string,string>, body?: string }} [init]
197
+ * @param {object} [opts] - forwarded to resolveGuardedTarget, plus maxRedirects
198
+ * @returns {Promise<object>}
199
+ */
200
+ export async function guardedFetch(rawUrl, init = {}, opts = {}) {
201
+ const maxRedirects = opts.maxRedirects ?? 5;
202
+ let currentUrl = rawUrl;
203
+
204
+ for (let hop = 0; hop <= maxRedirects; hop++) {
205
+ const target = await resolveGuardedTarget(currentUrl, opts);
206
+ const res = await requestPinned(target, init);
207
+
208
+ if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
209
+ currentUrl = new URL(res.headers.location, target.url).href;
210
+ if (res.statusCode === 303) { init = { ...init, method: 'GET', body: undefined }; }
211
+ continue;
212
+ }
213
+ return makeResponse({ statusCode: res.statusCode, headers: res.headers, bodyBuffer: res.bodyBuffer, url: target.url.href });
214
+ }
215
+ throw new SsrfBlockedError(`net-guard: exceeded ${maxRedirects} redirects for ${rawUrl}`, 'too-many-redirects');
216
+ }
217
+
218
+ function requestPinned(target, init) {
219
+ const { url, pinnedAddress, family, port, servername } = target;
220
+ const doRequest = url.protocol === 'https:' ? httpsRequest : httpRequest;
221
+
222
+ // The socket connects to the pinned IP; the hostname survives only as TLS
223
+ // servername + Host header. The custom lookup guarantees the pinned address
224
+ // is the only one the agent can dial — DNS is never consulted again here.
225
+ const pinnedLookup = (_hostname, _options, cb) => cb(null, pinnedAddress, family);
226
+
227
+ const options = {
228
+ method: init.method ?? 'GET',
229
+ host: servername,
230
+ port,
231
+ path: `${url.pathname}${url.search}`,
232
+ headers: { Host: url.host, ...(init.headers ?? {}) },
233
+ servername,
234
+ lookup: pinnedLookup,
235
+ };
236
+
237
+ return new Promise((resolve, reject) => {
238
+ const req = doRequest(options, (res) => {
239
+ const chunks = [];
240
+ res.on('data', (c) => chunks.push(c));
241
+ res.on('end', () => resolve({ statusCode: res.statusCode, headers: res.headers, bodyBuffer: Buffer.concat(chunks) }));
242
+ });
243
+ req.on('error', reject);
244
+ if (init.body) req.write(init.body);
245
+ req.end();
246
+ });
247
+ }
@@ -9,6 +9,9 @@
9
9
  * Storage layout:
10
10
  * .cx/observations/index.json — lightweight listing for fast filtering
11
11
  * .cx/observations/<id>.json — full observation record
12
+ * <stateRoot>/lancedb — vector index (ADR-0066: machine-scoped,
13
+ * not project-local); the JSON records
14
+ * above remain project-local for now
12
15
  */
13
16
  import crypto from 'node:crypto';
14
17
  import fs from 'node:fs';
@@ -16,6 +19,7 @@ import path from 'node:path';
16
19
  import { embedText as embedTextEngine } from './storage/embeddings-engine.mjs';
17
20
  import { VectorClient } from './storage/vector-client.mjs';
18
21
  import { ensureCxDir } from './project-init-shared.mjs';
22
+ import { resolveStateDir } from './state-root.mjs';
19
23
 
20
24
  const OBS_DIR = '.cx/observations';
21
25
  const INDEX_FILE = 'index.json';
@@ -31,7 +35,7 @@ const MAX_INDEX = 1000;
31
35
  function vectorClientFor(rootDir) {
32
36
  const env = process.env.CONSTRUCT_LANCEDB_PATH
33
37
  ? process.env
34
- : { ...process.env, CONSTRUCT_LANCEDB_PATH: path.join(rootDir, '.cx', 'lancedb') };
38
+ : { ...process.env, CONSTRUCT_LANCEDB_PATH: resolveStateDir(rootDir, 'lancedb', { ensureDir: false }) };
35
39
  return new VectorClient({ env });
36
40
  }
37
41
 
@@ -111,7 +115,7 @@ function logCapDrop(rootDir, kind, dropped, total) {
111
115
  dropped,
112
116
  total,
113
117
  cap: MAX_INDEX,
114
- remedy: 'run `construct memory consolidate` or raise --max-stored',
118
+ remedy: "Run 'construct memory consolidate' to compact stored observations.",
115
119
  });
116
120
  fs.appendFileSync(logPath, entry + '\n');
117
121
  } catch { /* cap warnings are best-effort */ }
@@ -18,6 +18,7 @@ import { spawnSync } from "node:child_process";
18
18
  import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
19
19
  import { tmpdir } from "node:os";
20
20
  import { join } from "node:path";
21
+ import { isMainModule } from '../roots.mjs';
21
22
 
22
23
  const DEFAULT_NUM_CTX = 32768;
23
24
  const CHATML_STOPS = ["<|im_start|>", "<|im_end|>", "<|endoftext|>"];
@@ -325,7 +326,7 @@ export async function probeStreamingToolCallLeak(model, { baseURL = "http://127.
325
326
  }
326
327
  }
327
328
 
328
- if (import.meta.url === `file://${process.argv[1]}`) {
329
+ if (isMainModule(import.meta.url)) {
329
330
  if (process.argv.includes("--probe")) {
330
331
  const model = process.argv.find((a) => a.startsWith("--model="))?.split("=")[1];
331
332
  const targets = model ? [model] : listModels();
@@ -175,10 +175,20 @@ export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
175
175
  if (sessions.includes('github-copilot')) {
176
176
  const envValues = parseEnvFile(getUserEnvPath());
177
177
  const port = envValues.COPILOT_BRIDGE_PORT || '5174';
178
- config.provider['github-copilot'] = config.provider['github-copilot'] || {
178
+ const provider = config.provider['github-copilot'] || {
179
179
  baseUrl: `http://localhost:${port}/v1`,
180
- apiKey: "noop" // The bridge handles auth via gh CLI
180
+ apiKey: "noop" // Copilot itself is authed via gh CLI inside the bridge; this field is unused
181
181
  };
182
+
183
+ // The bridge now requires a per-launch bearer token. Wire it as an {env:} reference
184
+ // so OpenCode resolves it from its own env at request time and the token is never
185
+ // written into opencode.json in the clear.
186
+ if (envValues.CONSTRUCT_COPILOT_BRIDGE_TOKEN) {
187
+ provider.options = provider.options || {};
188
+ provider.options.headers = provider.options.headers || {};
189
+ provider.options.headers.Authorization = '{env:CONSTRUCT_COPILOT_BRIDGE_TOKEN}';
190
+ }
191
+ config.provider['github-copilot'] = provider;
182
192
  }
183
193
  }
184
194
 
@@ -259,6 +269,15 @@ export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
259
269
  });
260
270
  }
261
271
 
262
- fs.writeFileSync(target, `${JSON.stringify(sanitizeOpenCodeConfig(config), null, 2)}\n`, "utf8");
272
+ fs.writeFileSync(target, `${JSON.stringify(sanitizeOpenCodeConfig(config), null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
273
+
274
+ // opencode.json can carry a resolved provider apiKey or Authorization header.
275
+ // writeFileSync keeps an existing inode's mode, so the mode option above does not
276
+ // tighten a file an earlier write left 0644; re-apply 0600 on every write, matching
277
+ // lib/env-config.mjs's writeEnvValues contract for config.env.
278
+
279
+ if (process.platform !== "win32") {
280
+ try { fs.chmodSync(target, 0o600); } catch { /* exotic filesystem */ }
281
+ }
263
282
  return target;
264
283
  }