@geraldmaron/construct 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (556) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1029 -65
  4. package/bin/construct-postinstall.mjs +27 -2
  5. package/config/tag-vocabulary.json +264 -0
  6. package/examples/distribution/sources/deck-one-pager.md +1 -1
  7. package/lib/acp/server.mjs +21 -7
  8. package/lib/adapters-sync.mjs +2 -1
  9. package/lib/audit-trail.mjs +185 -2
  10. package/lib/beads/auto-close.mjs +2 -1
  11. package/lib/beads/drift.mjs +2 -1
  12. package/lib/bridges/copilot-proxy.mjs +20 -5
  13. package/lib/certification/skill-inventory.mjs +10 -1
  14. package/lib/cli/approvals.mjs +147 -0
  15. package/lib/cli-commands.mjs +105 -14
  16. package/lib/comment-lint.mjs +127 -8
  17. package/lib/config/schema.mjs +7 -30
  18. package/lib/config/source-target-registry.mjs +70 -0
  19. package/lib/config/source-targets.mjs +179 -205
  20. package/lib/contracts/coverage.mjs +77 -0
  21. package/lib/contracts/validate.mjs +102 -13
  22. package/lib/contracts/violation-log.mjs +50 -4
  23. package/lib/db/migrate.mjs +69 -0
  24. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  25. package/lib/db/migrations/002_queue_provider.sql +50 -0
  26. package/lib/db/migrations/003_worker_registry.sql +17 -0
  27. package/lib/db/migrations/004_trace_events.sql +16 -0
  28. package/lib/db/migrations/005_shared_memory.sql +15 -0
  29. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  30. package/lib/decisions/enforced-baseline.json +0 -2
  31. package/lib/decisions/registry.mjs +3 -2
  32. package/lib/deployment/parity-contract.mjs +1 -1
  33. package/lib/deployment-mode.mjs +78 -2
  34. package/lib/diagram-export.mjs +10 -1
  35. package/lib/distill.mjs +5 -2
  36. package/lib/docs-verify.mjs +2 -1
  37. package/lib/doctor/cli.mjs +1 -0
  38. package/lib/doctor/command-on-path.mjs +24 -0
  39. package/lib/doctor/diagnosis.mjs +109 -0
  40. package/lib/doctor/embedding-health.mjs +50 -0
  41. package/lib/doctor/engine-health.mjs +93 -0
  42. package/lib/doctor/graph-validate.mjs +47 -0
  43. package/lib/doctor/index.mjs +23 -4
  44. package/lib/doctor/sidecar-providers.mjs +56 -0
  45. package/lib/doctor/watchers/consistency.mjs +93 -1
  46. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  47. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  48. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  49. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  50. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  51. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  52. package/lib/document-export.mjs +52 -27
  53. package/lib/document-extract/docling-client.mjs +9 -5
  54. package/lib/document-extract/docling-sidecar.py +30 -0
  55. package/lib/document-extract.mjs +1 -1
  56. package/lib/document-ingest.mjs +9 -0
  57. package/lib/embed/approval-queue.mjs +184 -88
  58. package/lib/embed/authority-guard.mjs +65 -2
  59. package/lib/embed/capability-jobs.mjs +365 -0
  60. package/lib/embed/capability-lifecycle.mjs +219 -0
  61. package/lib/embed/capability-loader.mjs +303 -0
  62. package/lib/embed/capability-runtime.mjs +58 -0
  63. package/lib/embed/cli.mjs +185 -8
  64. package/lib/embed/config.mjs +4 -0
  65. package/lib/embed/daemon.mjs +125 -6
  66. package/lib/embed/demand-fetch.mjs +190 -54
  67. package/lib/embed/inbox.mjs +12 -10
  68. package/lib/embed/presets/ops-triage.mjs +236 -0
  69. package/lib/embed/presets/pm-feedback.mjs +341 -0
  70. package/lib/embed/presets/tpm.mjs +401 -0
  71. package/lib/embed/providers/jira.mjs +72 -1
  72. package/lib/embed/providers/registry.mjs +140 -33
  73. package/lib/embed/worker.mjs +5 -0
  74. package/lib/embedded-contract/capability.mjs +4 -0
  75. package/lib/embedded-contract/execution.mjs +1 -1
  76. package/lib/embedded-contract/model-resolve.mjs +53 -3
  77. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  78. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  79. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  80. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  81. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  82. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  83. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  84. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  85. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  86. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  88. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  90. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  91. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  92. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  93. package/lib/env-config.mjs +60 -11
  94. package/lib/export-validate.mjs +34 -2
  95. package/lib/extensions/index.mjs +27 -0
  96. package/lib/extensions/loader.mjs +120 -0
  97. package/lib/extensions/manifest-schema.mjs +141 -0
  98. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  99. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  100. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  101. package/lib/extensions/manifests/directory.manifest.json +12 -0
  102. package/lib/extensions/manifests/docling.manifest.json +37 -0
  103. package/lib/extensions/manifests/echo.manifest.json +8 -0
  104. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  105. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  106. package/lib/extensions/manifests/github.manifest.json +52 -0
  107. package/lib/extensions/manifests/linear.manifest.json +50 -0
  108. package/lib/extensions/manifests/local.manifest.json +12 -0
  109. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  110. package/lib/extensions/manifests/openai.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  113. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  114. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  115. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  116. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  117. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  118. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  119. package/lib/extensions/manifests/slack.manifest.json +58 -0
  120. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  121. package/lib/extensions/validate.mjs +175 -0
  122. package/lib/features.mjs +13 -9
  123. package/lib/flows/checkpoint.mjs +184 -0
  124. package/lib/flows/constants.mjs +27 -0
  125. package/lib/flows/define.mjs +146 -0
  126. package/lib/flows/engine.mjs +249 -0
  127. package/lib/flows/errors.mjs +19 -0
  128. package/lib/flows/index.mjs +27 -0
  129. package/lib/flows/joins.mjs +18 -0
  130. package/lib/flows/schema.mjs +90 -0
  131. package/lib/flows/state.mjs +31 -0
  132. package/lib/frameworks/loader.mjs +99 -0
  133. package/lib/frameworks/schema.mjs +157 -0
  134. package/lib/graph/build-from-corpus.mjs +67 -0
  135. package/lib/graph/build-from-embed.mjs +82 -0
  136. package/lib/graph/build-from-registry.mjs +164 -3
  137. package/lib/graph/cli.mjs +456 -12
  138. package/lib/graph/gap-queries.mjs +156 -0
  139. package/lib/graph/gaps.mjs +41 -0
  140. package/lib/graph/impacted.mjs +129 -0
  141. package/lib/graph/runtime-evidence.mjs +177 -0
  142. package/lib/graph/security-coverage.mjs +113 -0
  143. package/lib/graph/staleness.mjs +241 -10
  144. package/lib/graph/store.mjs +24 -7
  145. package/lib/graph/validate.mjs +161 -0
  146. package/lib/headhunt.mjs +9 -8
  147. package/lib/health-check.mjs +15 -7
  148. package/lib/hook-health.mjs +1 -1
  149. package/lib/hooks/agent-tracker.mjs +10 -4
  150. package/lib/hooks/edit-guard.mjs +3 -3
  151. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  152. package/lib/hooks/guard-bash.mjs +41 -15
  153. package/lib/hooks/mcp-health-check.mjs +11 -4
  154. package/lib/hooks/model-fallback.mjs +50 -6
  155. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  156. package/lib/hooks/pre-compact.mjs +181 -173
  157. package/lib/hooks/rule-verifier.mjs +2 -1
  158. package/lib/hooks/session-reflect.mjs +2 -1
  159. package/lib/hooks/session-start.mjs +3 -3
  160. package/lib/host/readiness.mjs +109 -0
  161. package/lib/host-capabilities.mjs +13 -2
  162. package/lib/host-disposition.mjs +29 -8
  163. package/lib/identity.mjs +92 -0
  164. package/lib/ingest/degraded-extract.mjs +96 -0
  165. package/lib/ingest/docling-remote.mjs +2 -1
  166. package/lib/ingest/provider-extract.mjs +7 -19
  167. package/lib/ingest/sidecar-providers.mjs +196 -0
  168. package/lib/ingest-tooling.mjs +11 -5
  169. package/lib/init-unified.mjs +55 -38
  170. package/lib/init-update.mjs +2 -1
  171. package/lib/install/legacy-global-cleanup.mjs +25 -0
  172. package/lib/install/stage-project.mjs +41 -4
  173. package/lib/intake/daemon.mjs +99 -8
  174. package/lib/intake/git-queue.mjs +110 -38
  175. package/lib/intake/prepare.mjs +2 -0
  176. package/lib/intake/queue-registry.mjs +50 -0
  177. package/lib/intake/queue.mjs +133 -17
  178. package/lib/intake/session-prelude.mjs +57 -8
  179. package/lib/integrations/intake-integrations.mjs +61 -111
  180. package/lib/intent-classifier.mjs +43 -5
  181. package/lib/libreoffice-export.mjs +8 -8
  182. package/lib/logging/rotate.mjs +5 -4
  183. package/lib/mcp/broker.mjs +332 -17
  184. package/lib/mcp/denied-store.mjs +95 -0
  185. package/lib/mcp/destructive-approval.mjs +57 -0
  186. package/lib/mcp/destructive-gate.mjs +30 -0
  187. package/lib/mcp/dispatch-envelope.mjs +199 -0
  188. package/lib/mcp/memory-bridge.mjs +2 -1
  189. package/lib/mcp/server.mjs +185 -1268
  190. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  191. package/lib/mcp/tool-definitions-project.mjs +271 -0
  192. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  193. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  194. package/lib/mcp/tool-definitions.mjs +25 -0
  195. package/lib/mcp/tool-rate-limit.mjs +47 -0
  196. package/lib/mcp/tool-registry.mjs +107 -0
  197. package/lib/mcp/tool-safety.mjs +95 -0
  198. package/lib/mcp/tool-surface-parity.mjs +60 -0
  199. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  200. package/lib/mcp/tools/orchestration-run.mjs +197 -19
  201. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  202. package/lib/mcp/tools/project.mjs +25 -8
  203. package/lib/mcp/tools/provider-write.mjs +187 -0
  204. package/lib/mcp/tools/scope.mjs +0 -3
  205. package/lib/mcp/tools/skills.mjs +1 -1
  206. package/lib/mcp/tools/storage.mjs +2 -2
  207. package/lib/mcp/tools/web-search-governance.mjs +96 -0
  208. package/lib/mcp/tools/web-search.mjs +5 -76
  209. package/lib/mcp/transport/auth.mjs +238 -0
  210. package/lib/mcp/transport/http.mjs +136 -0
  211. package/lib/mcp/transport/mode.mjs +31 -0
  212. package/lib/mcp/transport/stdio.mjs +22 -0
  213. package/lib/mcp-catalog.json +4 -4
  214. package/lib/mcp-manager.mjs +34 -23
  215. package/lib/mcp-platform-config.mjs +53 -20
  216. package/lib/mode-capabilities.mjs +109 -0
  217. package/lib/model-router.mjs +42 -9
  218. package/lib/models/catalog.mjs +40 -1
  219. package/lib/net-guard.mjs +247 -0
  220. package/lib/observation-store.mjs +6 -2
  221. package/lib/ollama/provision-context.mjs +2 -1
  222. package/lib/opencode-config.mjs +22 -3
  223. package/lib/opencode-runtime-plugin.mjs +120 -2
  224. package/lib/oracle/actions.mjs +204 -10
  225. package/lib/oracle/cli.mjs +24 -3
  226. package/lib/oracle/daemon-entry.mjs +6 -0
  227. package/lib/oracle/dispatch.mjs +2 -1
  228. package/lib/oracle/execute.mjs +2 -2
  229. package/lib/oracle/gaps.mjs +3 -3
  230. package/lib/oracle/heartbeat.mjs +53 -0
  231. package/lib/oracle/read-model.mjs +69 -13
  232. package/lib/oracle/reconcile.mjs +3 -46
  233. package/lib/oracle/routing.mjs +37 -31
  234. package/lib/oracle/synthesize.mjs +1 -1
  235. package/lib/orchestration/classification.mjs +434 -0
  236. package/lib/orchestration/delegation-flow.mjs +132 -0
  237. package/lib/orchestration/flow-selection.mjs +418 -0
  238. package/lib/orchestration/gates.mjs +244 -0
  239. package/lib/orchestration/host-sampling.mjs +121 -0
  240. package/lib/orchestration/policy-constants.mjs +48 -0
  241. package/lib/orchestration/provider-outcome.mjs +197 -0
  242. package/lib/orchestration/readiness.mjs +114 -13
  243. package/lib/orchestration/run-store-postgres.mjs +32 -26
  244. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  245. package/lib/orchestration/run-store.mjs +25 -12
  246. package/lib/orchestration/runtime.mjs +508 -62
  247. package/lib/orchestration/store.mjs +87 -12
  248. package/lib/orchestration/trace-store.mjs +125 -0
  249. package/lib/orchestration/web-capability.mjs +60 -0
  250. package/lib/orchestration/worker-runtime.mjs +162 -0
  251. package/lib/orchestration/worker.mjs +763 -80
  252. package/lib/orchestration-policy.mjs +67 -1111
  253. package/lib/output-quality.mjs +61 -2
  254. package/lib/packs/cli.mjs +144 -0
  255. package/lib/packs/core-pack.mjs +112 -0
  256. package/lib/packs/enablement.mjs +187 -0
  257. package/lib/packs/index.mjs +23 -0
  258. package/lib/packs/loader.mjs +176 -0
  259. package/lib/packs/manifest-schema.mjs +58 -0
  260. package/lib/packs/prompts.mjs +120 -0
  261. package/lib/packs/validate.mjs +208 -0
  262. package/lib/path-policy.mjs +56 -0
  263. package/lib/plugin-registry.mjs +4 -5
  264. package/lib/policy/audit-gate.mjs +42 -0
  265. package/lib/policy/consumption-budget.mjs +149 -0
  266. package/lib/policy/engine.mjs +81 -6
  267. package/lib/policy/role-authority.mjs +89 -0
  268. package/lib/prompt-composer.js +19 -3
  269. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  270. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  271. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  272. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  273. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  274. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  275. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  276. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  277. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  278. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  279. package/lib/providers/contract.mjs +207 -0
  280. package/lib/providers/credential-bootstrap.mjs +7 -4
  281. package/lib/providers/credential-sources.mjs +14 -2
  282. package/lib/providers/directory/index.mjs +261 -0
  283. package/lib/providers/feedback/index.mjs +392 -0
  284. package/lib/providers/filter-audit.mjs +68 -0
  285. package/lib/providers/filter-schema.mjs +54 -0
  286. package/lib/providers/github/index.mjs +31 -0
  287. package/lib/providers/instance-config.mjs +215 -0
  288. package/lib/providers/op-locate.mjs +96 -0
  289. package/lib/providers/op-run.mjs +64 -9
  290. package/lib/providers/registry.mjs +26 -3
  291. package/lib/providers/secret-audit-wiring.mjs +41 -18
  292. package/lib/providers/secret-resolver.mjs +263 -31
  293. package/lib/publish-template.mjs +6 -3
  294. package/lib/publish-tooling.mjs +4 -0
  295. package/lib/publish.mjs +32 -4
  296. package/lib/queue/pg-queue.mjs +395 -0
  297. package/lib/reflect.mjs +2 -1
  298. package/lib/registry/assemble.mjs +28 -2
  299. package/lib/registry/catalog.mjs +6 -0
  300. package/lib/registry/consolidation.mjs +8 -1
  301. package/lib/registry/custom-scaffold.mjs +200 -0
  302. package/lib/registry/custom-schema.mjs +137 -0
  303. package/lib/registry/loader.mjs +15 -4
  304. package/lib/registry/manifests/format-engines.default.json +15 -0
  305. package/lib/registry/manifests/surface-map.default.json +46 -0
  306. package/lib/registry/retired-paths.mjs +1 -0
  307. package/lib/registry/surface-map.mjs +100 -50
  308. package/lib/registry/validate.mjs +3 -3
  309. package/lib/render-visual-check.mjs +240 -0
  310. package/lib/resources/budget.mjs +40 -17
  311. package/lib/roles/flavor-bindings.mjs +36 -14
  312. package/lib/roles/gateway.mjs +15 -8
  313. package/lib/roots.mjs +107 -0
  314. package/lib/runtime/uv-bootstrap.mjs +32 -6
  315. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  316. package/lib/runtime-env.mjs +5 -2
  317. package/lib/sandbox.mjs +1 -1
  318. package/lib/scheduler/index.mjs +8 -20
  319. package/lib/schema-infer.mjs +31 -2
  320. package/lib/scopes/lifecycle.mjs +29 -25
  321. package/lib/scopes/loader.mjs +49 -12
  322. package/lib/scopes/teams.mjs +1 -1
  323. package/lib/security/ingest-boundary.mjs +80 -0
  324. package/lib/security/recall-wrapper.mjs +99 -0
  325. package/lib/security/trust.mjs +132 -0
  326. package/lib/service-manager.mjs +93 -24
  327. package/lib/setup.mjs +41 -23
  328. package/lib/skills-apply.mjs +4 -2
  329. package/lib/specialists/postconditions.mjs +2 -2
  330. package/lib/state-root.mjs +147 -0
  331. package/lib/status.mjs +597 -6
  332. package/lib/storage/admin.mjs +84 -7
  333. package/lib/storage/backend-registry.mjs +73 -0
  334. package/lib/storage/backend.mjs +63 -9
  335. package/lib/storage/embeddings-openai.mjs +12 -2
  336. package/lib/storage/hybrid-query.mjs +11 -3
  337. package/lib/storage/retrieval-hardening.mjs +384 -0
  338. package/lib/storage/shared-memory.mjs +158 -0
  339. package/lib/storage/vector-client.mjs +69 -2
  340. package/lib/team/health.mjs +72 -0
  341. package/lib/telemetry/backends/local.mjs +9 -3
  342. package/lib/telemetry/client.mjs +3 -7
  343. package/lib/template-registry.mjs +10 -12
  344. package/lib/tenant/context.mjs +82 -0
  345. package/lib/tenant/isolation.mjs +129 -0
  346. package/lib/test-corpus-inventory.mjs +103 -2
  347. package/lib/uninstall/uninstall.mjs +78 -12
  348. package/lib/validator.mjs +4 -6
  349. package/lib/worker/entrypoint.mjs +2 -1
  350. package/lib/worker/run.mjs +2 -2
  351. package/lib/worker/trace.mjs +5 -11
  352. package/lib/workflow-state.mjs +52 -8
  353. package/lib/workflows/liveness.mjs +203 -0
  354. package/lib/workflows/loader.mjs +177 -0
  355. package/lib/workflows/manifest-schema.mjs +71 -0
  356. package/lib/workflows/surface-parity.mjs +118 -0
  357. package/lib/workflows/validate.mjs +127 -0
  358. package/lib/writes/control-plane.mjs +140 -0
  359. package/lib/writes/envelope.mjs +206 -0
  360. package/lib/writes/sent-log.mjs +86 -0
  361. package/lib/writes/write-intent.mjs +109 -0
  362. package/package.json +20 -7
  363. package/personas/construct.md +12 -3
  364. package/registry/agent-manifest.json +117 -0
  365. package/registry/capabilities.json +1987 -0
  366. package/rules/common/comments.md +1 -0
  367. package/schemas/brand-voice.schema.json +24 -0
  368. package/schemas/capability-registry.schema.json +72 -0
  369. package/schemas/certification-run.schema.json +130 -0
  370. package/schemas/demo-recording.schema.json +46 -0
  371. package/schemas/eval-dataset.schema.json +79 -0
  372. package/schemas/execution-capability-profile.schema.json +46 -0
  373. package/schemas/execution-policy.schema.json +114 -0
  374. package/schemas/improvement-proposal.schema.json +65 -0
  375. package/schemas/mcp-tool-output.schema.json +61 -0
  376. package/schemas/platform-capabilities.schema.json +83 -0
  377. package/schemas/project-config.schema.json +215 -0
  378. package/schemas/project-demo.schema.json +60 -0
  379. package/schemas/provider-behavior-matrix.schema.json +91 -0
  380. package/schemas/scope.schema.json +197 -0
  381. package/schemas/specialist-trace.schema.json +107 -0
  382. package/schemas/team.schema.json +99 -0
  383. package/schemas/unified-registry.schema.json +546 -0
  384. package/scripts/sync-specialists.mjs +336 -106
  385. package/skills/docs/adr-workflow.md +1 -1
  386. package/skills/docs/codebase-research-workflow.md +3 -3
  387. package/skills/docs/prd-workflow.md +5 -6
  388. package/skills/docs/runbook-workflow.md +2 -2
  389. package/skills/docs/user-research-workflow.md +3 -3
  390. package/skills/operating/fleet-health-routing.md +77 -0
  391. package/skills/roles/ai-engineer.md +1 -1
  392. package/skills/roles/architect.md +0 -1
  393. package/skills/roles/business-strategist.md +1 -1
  394. package/skills/roles/data-analyst.telemetry.md +1 -1
  395. package/skills/roles/data-engineer.md +1 -1
  396. package/skills/roles/data-engineer.pipeline.md +1 -1
  397. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  398. package/skills/roles/data-engineer.warehouse.md +1 -1
  399. package/skills/roles/designer.accessibility.md +1 -1
  400. package/skills/roles/designer.md +0 -1
  401. package/skills/roles/devil-advocate.md +1 -1
  402. package/skills/roles/docs-keeper.md +1 -1
  403. package/skills/roles/evaluator.md +1 -1
  404. package/skills/roles/explorer.md +1 -1
  405. package/skills/roles/platform-engineer.md +1 -1
  406. package/skills/roles/qa.ai-eval.md +1 -2
  407. package/skills/roles/qa.api-contract.md +0 -1
  408. package/skills/roles/qa.data-pipeline.md +0 -1
  409. package/skills/roles/qa.md +0 -1
  410. package/skills/roles/qa.web-ui.md +0 -1
  411. package/skills/roles/release-manager.md +1 -1
  412. package/skills/roles/researcher.md +0 -2
  413. package/skills/roles/reviewer.md +0 -3
  414. package/skills/roles/security.ai.md +1 -1
  415. package/skills/roles/security.legal-compliance.md +1 -1
  416. package/skills/roles/security.md +0 -1
  417. package/skills/roles/security.privacy.md +0 -1
  418. package/skills/roles/security.supply-chain.md +1 -1
  419. package/skills/roles/sre.md +1 -1
  420. package/skills/roles/test-automation.md +1 -1
  421. package/skills/roles/trace-reviewer.md +1 -1
  422. package/skills/roles/ux-researcher.md +1 -1
  423. package/specialists/artifact-manifest.json +36 -36
  424. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  425. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  426. package/specialists/org/contracts/any-to-debugger.json +7 -1
  427. package/specialists/org/contracts/any-to-designer.json +7 -1
  428. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  429. package/specialists/org/contracts/any-to-explorer.json +13 -4
  430. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  431. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  432. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  433. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  434. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  435. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  436. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  437. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  438. package/specialists/org/contracts/architect-to-operations.json +17 -4
  439. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  440. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  441. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  442. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  443. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  444. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  445. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  446. package/specialists/org/contracts/operations-to-user.json +53 -0
  447. package/specialists/org/contracts/operations-triage-output.json +53 -0
  448. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  449. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  450. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  451. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  452. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  453. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  454. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  455. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  456. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  457. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  458. package/specialists/org/contracts/user-to-construct.json +11 -4
  459. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  460. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  461. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  462. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  463. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  464. package/specialists/org/groups/engineering-group.json +2 -6
  465. package/specialists/org/groups/governance-group.json +2 -3
  466. package/specialists/org/groups/operations-group.json +5 -8
  467. package/specialists/org/groups/product-group.json +4 -8
  468. package/specialists/org/groups/quality-group.json +3 -7
  469. package/specialists/org/groups/strategy-group.json +6 -9
  470. package/specialists/org/models.json.example +8 -0
  471. package/specialists/org/scopes/creative.json +6 -1
  472. package/specialists/org/scopes/operations.json +7 -1
  473. package/specialists/org/scopes/research.json +6 -1
  474. package/specialists/org/scopes/rnd.json +7 -1
  475. package/specialists/org/specialists/cx-architect.json +27 -8
  476. package/specialists/org/specialists/cx-designer.json +22 -8
  477. package/specialists/org/specialists/cx-engineer.json +71 -7
  478. package/specialists/org/specialists/cx-operations.json +89 -15
  479. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  480. package/specialists/org/specialists/cx-product-manager.json +28 -9
  481. package/specialists/org/specialists/cx-qa.json +16 -6
  482. package/specialists/org/specialists/cx-researcher.json +41 -7
  483. package/specialists/org/specialists/cx-reviewer.json +61 -11
  484. package/specialists/org/specialists/cx-security.json +30 -10
  485. package/specialists/org/teams/design-team.json +3 -4
  486. package/specialists/org/teams/engineering-team.json +3 -10
  487. package/specialists/org/teams/governance-team.json +3 -5
  488. package/specialists/org/teams/operations-team.json +6 -12
  489. package/specialists/org/teams/product-management-team.json +2 -3
  490. package/specialists/org/teams/quality-team.json +4 -12
  491. package/specialists/org/teams/research-team.json +3 -3
  492. package/specialists/org/teams/strategy-team.json +7 -14
  493. package/specialists/prompts/cx-architect.md +20 -1
  494. package/specialists/prompts/cx-data-analyst.md +1 -1
  495. package/specialists/prompts/cx-designer.md +12 -4
  496. package/specialists/prompts/cx-engineer.md +15 -1
  497. package/specialists/prompts/cx-operations.md +14 -2
  498. package/specialists/prompts/cx-orchestrator.md +9 -5
  499. package/specialists/prompts/cx-product-manager.md +5 -1
  500. package/specialists/prompts/cx-qa.md +6 -2
  501. package/specialists/prompts/cx-researcher.md +27 -31
  502. package/specialists/prompts/cx-reviewer.md +19 -1
  503. package/specialists/prompts/cx-security.md +5 -1
  504. package/templates/distribution/construct-brand.typ +123 -59
  505. package/templates/distribution/construct-decision.typ +6 -3
  506. package/templates/distribution/construct-pdf.typ +11 -4
  507. package/templates/distribution/construct-prd.typ +6 -3
  508. package/templates/distribution/construct-research.typ +7 -4
  509. package/vendor/pandoc-ext/README.md +3 -0
  510. package/vendor/pandoc-ext/diagram.lua +687 -0
  511. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  512. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  513. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  514. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  515. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  516. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  517. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  518. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  519. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  520. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  521. package/specialists/org/specialists/cx-accessibility.json +0 -69
  522. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  523. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  524. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  525. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  526. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  527. package/specialists/org/specialists/cx-evaluator.json +0 -69
  528. package/specialists/org/specialists/cx-explorer.json +0 -69
  529. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  530. package/specialists/org/specialists/cx-oracle.json +0 -46
  531. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  532. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  533. package/specialists/org/specialists/cx-release-manager.json +0 -77
  534. package/specialists/org/specialists/cx-sre.json +0 -94
  535. package/specialists/org/specialists/cx-test-automation.json +0 -69
  536. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  537. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  538. package/specialists/org/teams/accessibility-team.json +0 -39
  539. package/specialists/org/teams/ux-research-team.json +0 -39
  540. package/specialists/prompts/cx-accessibility.md +0 -55
  541. package/specialists/prompts/cx-ai-engineer.md +0 -124
  542. package/specialists/prompts/cx-business-strategist.md +0 -58
  543. package/specialists/prompts/cx-data-engineer.md +0 -55
  544. package/specialists/prompts/cx-devil-advocate.md +0 -59
  545. package/specialists/prompts/cx-docs-keeper.md +0 -164
  546. package/specialists/prompts/cx-evaluator.md +0 -49
  547. package/specialists/prompts/cx-explorer.md +0 -71
  548. package/specialists/prompts/cx-legal-compliance.md +0 -58
  549. package/specialists/prompts/cx-oracle.md +0 -98
  550. package/specialists/prompts/cx-platform-engineer.md +0 -97
  551. package/specialists/prompts/cx-rd-lead.md +0 -59
  552. package/specialists/prompts/cx-release-manager.md +0 -54
  553. package/specialists/prompts/cx-sre.md +0 -111
  554. package/specialists/prompts/cx-test-automation.md +0 -55
  555. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  556. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -10,11 +10,16 @@
10
10
  * `orchestrator` entry). Specialists, slash commands, and skills do NOT
11
11
  * land at global scope — they are project content.
12
12
  * - Plus the hook installer in `~/.claude/settings.json`, which has to be
13
- * global so hooks fire in every Claude Code session.
13
+ * global so hooks fire in every Claude Code session. MCP server
14
+ * definitions are a separate file: Claude Code reads user-scope MCP
15
+ * servers from the top-level `mcpServers` object in `~/.claude.json`,
16
+ * not from settings.json (construct-ranh).
14
17
  *
15
18
  * Project scope (`<project>/.claude/`, `<project>/.codex/`, `<project>/.github/`, …)
16
19
  * - `construct` front door only (Single Front Door), slash commands, skills, MCP
17
20
  * wiring. Specialists dispatch internally via orchestration MCP tools.
21
+ * MCP server definitions land in `<project>/.mcp.json` (Claude Code's
22
+ * documented project scope), not `.claude/settings.json`.
18
23
  *
19
24
  * Flags:
20
25
  * --dry-run Print a diff of what would change without writing anything.
@@ -47,6 +52,7 @@ import {
47
52
  import { findOpenCodeConfigPath, readOpenCodeConfig, writeOpenCodeConfig, ensureOpenRouterProviderAuth } from "../lib/opencode-config.mjs";
48
53
  import { HEAVY_EXTERNAL_MCP_IDS, LOCAL_SURFACE_MODES, decideTrim, isLocalModel } from "../lib/mcp/tool-budget.mjs";
49
54
  import { emitCursorRules } from "../lib/rules-delivery.mjs";
55
+ import { memoryPort } from "../lib/home-namespace.mjs";
50
56
  import { resolvePromptContract, readPromptBody } from "../lib/prompt-composer.js";
51
57
  import { renderPersonaForTier } from "../lib/persona-sections.mjs";
52
58
  import { getModelVerdict } from "../lib/ollama/capability-store.mjs";
@@ -117,7 +123,6 @@ function unifiedToLegacyRegistry(unified) {
117
123
  };
118
124
  }
119
125
 
120
- clearCache();
121
126
  clearCache();
122
127
  const unified = loadRegistry({ rootDir: root });
123
128
  const registry = unifiedToLegacyRegistry(unified);
@@ -454,7 +459,7 @@ const standardConstructTools = [
454
459
  // agent's tools list or the call is blocked, leaving the orchestrator unable to route.
455
460
  // Single source of truth so every host's orchestrator grant stays in parity.
456
461
 
457
- const ORCHESTRATOR_DISPATCH_TOOLS = ["orchestration_policy", "orchestration_run"];
462
+ const ORCHESTRATOR_DISPATCH_TOOLS = ["orchestration_policy", "orchestration_run", "orchestration_delegation_next"];
458
463
  const managedStart = `# BEGIN ${systemName.toUpperCase()} AGENTS`;
459
464
  const managedEnd = `# END ${systemName.toUpperCase()} AGENTS`;
460
465
  const mdManagedStart = `<!-- BEGIN ${systemName.toUpperCase()} AGENTS -->`;
@@ -486,10 +491,17 @@ function resolveArgs(args) {
486
491
  : a));
487
492
  }
488
493
 
489
- function resolveTemplateStrings(value) {
494
+ // A secret-suffixed placeholder (TOKEN/SECRET/API_KEY/PUBLIC_KEY/PRIVATE_KEY) always
495
+ // resolves to OpenCode's `{env:NAME}` reference form, even when the named var is set
496
+ // in process.env, so a live credential never lands in a generated config file. This
497
+ // guard runs before the general env lookup below, mirroring the buildLocalEnvironment
498
+ // value->ref flip in lib/mcp-platform-config.mjs. GITHUB_TOKEN is an intentional alias
499
+ // that still materializes GITHUB_PERSONAL_ACCESS_TOKEN's value: that branch stays ahead
500
+ // of the suffix guard since it targets a different source var, not GITHUB_TOKEN itself.
501
+
502
+ export function resolveTemplateStrings(value) {
490
503
  if (typeof value === "string") {
491
504
  return value.replace(/__([A-Z0-9_]+)__/g, (_, name) => {
492
- if (process.env[name] !== undefined && process.env[name] !== "") return process.env[name];
493
505
  if (name === "CX_TOOLKIT_DIR") return root;
494
506
  if (name === "MEMORY_PORT") return process.env.MEMORY_PORT || "8765";
495
507
  if (name === "CONSTRUCT_MEMORY_BRIDGE_URL") {
@@ -501,6 +513,7 @@ function resolveTemplateStrings(value) {
501
513
  if (/(?:TOKEN|SECRET|API_KEY|PUBLIC_KEY|PRIVATE_KEY)$/.test(name)) {
502
514
  return `{env:${name}}`;
503
515
  }
516
+ if (process.env[name] !== undefined && process.env[name] !== "") return process.env[name];
504
517
  return `__${name}__`;
505
518
  });
506
519
  }
@@ -773,13 +786,13 @@ function orchestrationMicroPrompt(platform) {
773
786
  `Example — the user says "add rate limiting to the API". Your first action is a tool call, not prose:\n` +
774
787
  ` call ${policyTool} { "request": "add rate limiting to the API" }\n` +
775
788
  `If the route is focused/orchestrated specialist work, call \`${runTool}\` with the same request. If the route suggests a workflow such as \`research-synthesis\`, pass it as \`workflow_type\`. Do not narrate completed research unless \`${runTool}\` or evidence tools actually ran.\n\n` +
776
- `If a request needs a capability you lack — live web/network access, external data, code execution — do not refuse or tell the user to run it themselves. Route it via \`${runTool}\` to the specialist that holds the capability (web access lives with the researcher), or ask one clarifying question when the target is ambiguous.`
789
+ `If a request needs a capability you lack — live web/network access, external data, code execution — route it via \`${runTool}\` to the specialist that holds it (the researcher performs live web retrieval when a web path is available). Do not tell the user to run it themselves. But if \`${runTool}\` reports the capability was unavailable (degraded with \`capability-unavailable\`, or a prepare-only result), say plainly it could not be reached and return an insufficient-evidence result — never fabricate URLs, dates, quotes, or citations. Ask one clarifying question when the target is ambiguous.`
777
790
  );
778
791
  }
779
792
 
780
793
  // Directive for the local editor agent (construct-local). It executes bounded work on
781
794
  // the cheap local model and hands planning/reasoning back to the construct architect —
782
- // the aider architect/editor split. Kept short: a small model must actually obey it.
795
+ // a cheap-editor/capable-architect split. Kept short: a small model must actually obey it.
783
796
 
784
797
  const LOCAL_EDITOR_DIRECTIVE =
785
798
  `You are a focused execution agent running on a local model, dispatched by construct to do one bounded job. Do well-scoped edits for the current task and verify them; make the smallest correct change, never a broad rewrite.\n` +
@@ -1017,7 +1030,7 @@ ${buildPrompt(entry, allEntries, "claude")}
1017
1030
  * Rewrite the home-mode hook command pattern
1018
1031
  * node "$HOME/.config/construct/lib/hooks/<name>.mjs"
1019
1032
  * into the project-portable form
1020
- * node "${CLAUDE_PROJECT_DIR:-.}/.construct/run.mjs" hook <name>
1033
+ * node "${CLAUDE_PROJECT_DIR:-<absRoot>}/.construct/run.mjs" hook <name>
1021
1034
  * so the resulting settings.json works on any clone where the project ships
1022
1035
  * the .construct/ launcher (committed by `npm install`'s postinstall or by
1023
1036
  * `construct init`). The launcher resolves Construct via node_modules → npx
@@ -1025,21 +1038,29 @@ ${buildPrompt(entry, allEntries, "claude")}
1025
1038
  * works for non-Node ecosystems too. Other commands (inline node -e
1026
1039
  * snippets, npx block-no-verify@…) are left untouched.
1027
1040
  *
1028
- * The `${CLAUDE_PROJECT_DIR:-.}` anchor matters: Claude Code invokes hooks with
1041
+ * The `${CLAUDE_PROJECT_DIR:-<absRoot>}` anchor matters: hosts invoke hooks with
1029
1042
  * a working directory that is not guaranteed to be the project root (observed:
1030
- * $HOME), and a bare relative `.construct/run.mjs` then fails with MODULE_NOT_FOUND
1031
- * at node:internal/modules/cjs/loader. CLAUDE_PROJECT_DIR is the project root Claude
1032
- * Code exports to every hook (same var lib/hooks/*.mjs already read); the `:-.`
1033
- * fallback preserves the prior relative behavior wherever the var is unset.
1043
+ * $HOME, a `cd`-ed scratch dir), and a bare relative `.construct/run.mjs` then
1044
+ * fails with MODULE_NOT_FOUND at node:internal/modules/cjs/loader before any
1045
+ * code in run.mjs can execute, so the shim cannot self-correct. CLAUDE_PROJECT_DIR
1046
+ * is the project root Claude Code exports to every hook (same var lib/hooks/*.mjs
1047
+ * already read) and stays correct if the checkout moves; the fallback is the
1048
+ * absolute project root baked at sync time, so tools that do not export
1049
+ * CLAUDE_PROJECT_DIR (or any host with a drifted cwd) still resolve the launcher.
1050
+ * `.claude/settings.json` is gitignored and regenerated per machine by
1051
+ * `construct sync`, so an absolute fallback is machine-correct without leaking a
1052
+ * shared path. The prior `:-.` fallback was cwd-relative and broke on any drift.
1034
1053
  */
1035
- function makeHooksPortable(hooksJson) {
1054
+ function makeHooksPortable(hooksJson, projectRoot) {
1055
+ const anchor = `\${CLAUDE_PROJECT_DIR:-${path.resolve(projectRoot)}}`;
1056
+
1036
1057
  // Operate on the in-memory object so we don't fight JSON string escaping.
1037
1058
  const replaceCommand = (cmd) => {
1038
1059
  if (typeof cmd !== 'string') return cmd;
1039
1060
  const m = cmd.match(/^node\s+"?\$HOME\/\.config\/construct\/lib\/hooks\/([a-z0-9-]+)\.mjs"?\s*(.*)$/);
1040
1061
  if (!m) return cmd;
1041
1062
  const [, name, rest] = m;
1042
- return `node "\${CLAUDE_PROJECT_DIR:-.}/.construct/run.mjs" hook ${name}${rest ? ' ' + rest.trim() : ''}`;
1063
+ return `node "${anchor}/.construct/run.mjs" hook ${name}${rest ? ' ' + rest.trim() : ''}`;
1043
1064
  };
1044
1065
 
1045
1066
  const walk = (node) => {
@@ -1092,14 +1113,9 @@ function syncGlobalClaudeMcpServers(settings, registryMcp) {
1092
1113
  for (const [id, mcpDef] of Object.entries(registryMcp)) {
1093
1114
  if (!GLOBAL_CLAUDE_MCP_IDS.has(id)) continue;
1094
1115
  const existingEntry = settings.mcpServers[id];
1095
- const existing = JSON.stringify(existingEntry ?? "");
1096
- const hasPlaceholder = existing.includes("__");
1097
- const hasFloatingVersion = existing.includes("@latest");
1098
- const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
1099
- const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1100
- const transportMismatch = registryWantsCommand && existingIsRemote;
1101
- if (existingEntry && !hasPlaceholder && !hasFloatingVersion && !transportMismatch) continue;
1102
- settings.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1116
+ const desiredEntry = buildClaudeMcpEntry(id, mcpDef, process.env);
1117
+ if (!needsRefresh(existingEntry, desiredEntry, { root })) continue;
1118
+ settings.mcpServers[id] = desiredEntry;
1103
1119
  }
1104
1120
  }
1105
1121
 
@@ -1108,6 +1124,12 @@ function syncGlobalClaudeMcpServers(settings, registryMcp) {
1108
1124
  * with hook commands rewritten to be path-relative to whatever Construct
1109
1125
  * install the project carries. Merges into an existing settings.json
1110
1126
  * if one is already in the project; otherwise creates a fresh one.
1127
+ *
1128
+ * Carries hooks and permissions only — Claude Code does not read MCP server
1129
+ * definitions from settings.json at any scope (confirmed against
1130
+ * code.claude.com/docs/en/mcp's installation-scopes table and live `claude
1131
+ * mcp list` repro, construct-ranh). Project-scope MCP wiring is
1132
+ * writeProjectMcpJson's job, targeting `.mcp.json`.
1111
1133
  */
1112
1134
  function writeProjectClaudeSettings(targetDir) {
1113
1135
  const settingsPath = path.join(targetDir, ".claude", "settings.json");
@@ -1121,17 +1143,42 @@ function writeProjectClaudeSettings(targetDir) {
1121
1143
  : {};
1122
1144
 
1123
1145
  if (template.hooks) {
1124
- existing.hooks = JSON.parse(makeHooksPortable(template.hooks));
1146
+ existing.hooks = JSON.parse(makeHooksPortable(template.hooks, targetDir));
1125
1147
  }
1126
1148
  if (template.permissions) {
1127
1149
  existing.permissions ??= template.permissions;
1128
1150
  }
1129
- if (template.mcpServers) {
1130
- existing.mcpServers ??= {};
1131
- for (const [id, mcpDef] of Object.entries(template.mcpServers)) {
1132
- if (existing.mcpServers[id]) continue;
1133
- if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
1134
- existing.mcpServers[id] = mcpDef;
1151
+
1152
+ if (DRY_RUN) return;
1153
+ mkdirp(path.dirname(settingsPath));
1154
+ writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n");
1155
+ }
1156
+
1157
+ /**
1158
+ * Materialise project-scope MCP wiring at `.mcp.json` in the project root —
1159
+ * the scope Claude Code documents for team-shared, version-controlled MCP
1160
+ * server definitions (settings.json only carries policy keys and hooks).
1161
+ * Merges into an existing `.mcp.json` if the project already has one;
1162
+ * existing entries win so manual edits or a prior `claude mcp add --scope
1163
+ * project` stick.
1164
+ */
1165
+ function writeProjectMcpJson(targetDir) {
1166
+ const mcpJsonPath = path.join(targetDir, ".mcp.json");
1167
+ const templatePath = path.join(root, "platforms", "claude", "settings.template.json");
1168
+
1169
+ const existing = fs.existsSync(mcpJsonPath)
1170
+ ? JSON.parse(fs.readFileSync(mcpJsonPath, "utf8"))
1171
+ : {};
1172
+ existing.mcpServers ??= {};
1173
+
1174
+ if (fs.existsSync(templatePath)) {
1175
+ const template = JSON.parse(fs.readFileSync(templatePath, "utf8"));
1176
+ if (template.mcpServers) {
1177
+ for (const [id, mcpDef] of Object.entries(template.mcpServers)) {
1178
+ if (existing.mcpServers[id]) continue;
1179
+ if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
1180
+ existing.mcpServers[id] = mcpDef;
1181
+ }
1135
1182
  }
1136
1183
  }
1137
1184
 
@@ -1140,19 +1187,23 @@ function writeProjectClaudeSettings(targetDir) {
1140
1187
  // the server exposing project_context/get_skill/get_template/orchestration_policy
1141
1188
  // that the specialist loop depends on. The curated template omits it, which left
1142
1189
  // Claude Code as the only selected tool without the construct config. Merge the
1143
- // registry on top of the template seed; existing entries win so user edits stick,
1144
- // and buildClaudeMcpEntry resolves each server's path/env.
1190
+ // registry on top of the template seed via needsRefresh(): an existing entry
1191
+ // wins as long as it still matches the registry (env keys, pinned version,
1192
+ // transport, toolkit path), so a manual opt-in sticks but drift — a missing
1193
+ // registry env key, a stale pinned version — gets corrected.
1145
1194
  const registryMcp = scopedManagedMcpDefs({ projectScope: true });
1146
- existing.mcpServers ??= {};
1147
1195
  for (const [id, mcpDef] of Object.entries(registryMcp)) {
1148
- if (existing.mcpServers[id]) continue;
1149
1196
  if (!PROJECT_DEFAULT_MCP_IDS.has(id)) continue;
1150
- existing.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1197
+ const existingEntry = existing.mcpServers[id];
1198
+ const desiredEntry = buildClaudeMcpEntry(id, mcpDef, process.env);
1199
+ if (!needsRefresh(existingEntry, desiredEntry, { root })) continue;
1200
+ existing.mcpServers[id] = desiredEntry;
1151
1201
  }
1202
+ reconcileStaleManagedEntries(existing.mcpServers, { registryMcp, rebuildEntry: (id, def) => buildClaudeMcpEntry(id, def, process.env) });
1152
1203
 
1153
1204
  if (DRY_RUN) return;
1154
- mkdirp(path.dirname(settingsPath));
1155
- writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n");
1205
+ mkdirp(path.dirname(mcpJsonPath));
1206
+ writeFile(mcpJsonPath, JSON.stringify(existing, null, 2) + "\n");
1156
1207
  }
1157
1208
 
1158
1209
  function syncClaude(entries, targetDir = null, wants = true) {
@@ -1183,6 +1234,7 @@ function syncClaude(entries, targetDir = null, wants = true) {
1183
1234
 
1184
1235
  if (targetDir) {
1185
1236
  writeProjectClaudeSettings(targetDir);
1237
+ writeProjectMcpJson(targetDir);
1186
1238
  return;
1187
1239
  }
1188
1240
 
@@ -1200,7 +1252,7 @@ ${personaList}
1200
1252
  // User-managed file with our managed-block carved out — never doc-stamp.
1201
1253
  writeFile(claudeMdPath, replaceManagedBlock(existing, note, mdManagedStart, mdManagedEnd), { stamp: false });
1202
1254
 
1203
- // Sync MCP servers into ~/.claude/settings.json if it exists
1255
+ // Sync hooks into ~/.claude/settings.json if it exists
1204
1256
  const claudeSettingsPath = path.join(home, ".claude", "settings.json");
1205
1257
  if (fs.existsSync(claudeSettingsPath)) {
1206
1258
  const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, "utf8"));
@@ -1220,10 +1272,22 @@ ${personaList}
1220
1272
  settings.hooks = JSON.parse(hookStr);
1221
1273
  }
1222
1274
  }
1223
- const registryMcp = scopedManagedMcpDefs({ projectScope: false });
1224
- syncGlobalClaudeMcpServers(settings, registryMcp);
1225
1275
  if (!DRY_RUN) fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + "\n");
1226
1276
  }
1277
+
1278
+ // Sync user-scope MCP servers into ~/.claude.json's top-level `mcpServers`
1279
+ // — Claude Code does not read MCP server definitions from settings.json at
1280
+ // any scope (confirmed against code.claude.com/docs/en/mcp and live `claude
1281
+ // mcp list` repro, construct-ranh). Gated on the file already existing, same
1282
+ // as the hooks sync above: only write into Claude Code's own state file for
1283
+ // a user who has actually run the CLI at least once.
1284
+ const claudeUserConfigPath = path.join(home, ".claude.json");
1285
+ if (fs.existsSync(claudeUserConfigPath)) {
1286
+ const claudeUserConfig = JSON.parse(fs.readFileSync(claudeUserConfigPath, "utf8"));
1287
+ const registryMcp = scopedManagedMcpDefs({ projectScope: false });
1288
+ syncGlobalClaudeMcpServers(claudeUserConfig, registryMcp);
1289
+ if (!DRY_RUN) fs.writeFileSync(claudeUserConfigPath, JSON.stringify(claudeUserConfig, null, 2) + "\n");
1290
+ }
1227
1291
  }
1228
1292
  }
1229
1293
 
@@ -1368,15 +1432,14 @@ When using this prompt, stay within the role above and adapt to the current repo
1368
1432
  // recognized here, so the orchestrator needs its own VS Code agent or it lists
1369
1433
  // in the picker with no usable tools.
1370
1434
 
1371
- const COPILOT_AGENT_TOOLS = [
1372
- "construct-mcp/*",
1373
- "web/fetch",
1374
- "web/githubRepo",
1435
+ export const COPILOT_AGENT_TOOLS = [
1436
+ "construct-mcp/orchestration_policy",
1437
+ "construct-mcp/orchestration_run",
1438
+ "construct-mcp/orchestration_readiness",
1375
1439
  "search/codebase",
1376
1440
  "search/usages",
1377
1441
  "search/fileSearch",
1378
1442
  "read/problems",
1379
- "edit/editFiles",
1380
1443
  ];
1381
1444
 
1382
1445
  function copilotAgentFile(entry, allEntries) {
@@ -1486,6 +1549,86 @@ function getVSCodeUserMcpPaths() {
1486
1549
  .filter((file) => fs.existsSync(file));
1487
1550
  }
1488
1551
 
1552
+ // One comparator every host dialect (global Claude, project Claude, VS Code,
1553
+ // Cursor, OpenCode) calls instead of a bespoke preserve/refresh predicate per
1554
+ // surface — so the construct-mcp telemetry env passthrough and a pinned package
1555
+ // version converge identically everywhere rather than drifting per host.
1556
+ // needsRefresh() is the one question every dialect asks: does this host's
1557
+ // existing entry still match what the registry wants, across placeholder
1558
+ // resolution, transport, registry-declared env keys, pinned package versions,
1559
+ // and toolkit path. Callers
1560
+ // build the desired entry with their own host-specific builder (buildClaudeMcpEntry
1561
+ // / buildOpenCodeMcpEntry) first — both a Claude-shape entry (`args`/`env`) and an
1562
+ // OpenCode-shape entry (`command` array with the binary as element 0 / `environment`)
1563
+ // are normalized here so one comparator serves both shapes.
1564
+
1565
+ function entryArgs(entry) {
1566
+ if (Array.isArray(entry?.args)) return entry.args;
1567
+ if (Array.isArray(entry?.command)) return entry.command;
1568
+ return [];
1569
+ }
1570
+
1571
+ function entryEnv(entry) {
1572
+ return entry?.env ?? entry?.environment ?? {};
1573
+ }
1574
+
1575
+ function entryIsRemote(entry) {
1576
+ return entry?.type === "http" || entry?.type === "remote";
1577
+ }
1578
+
1579
+ // A registry env block that declares a key (e.g. the construct-mcp telemetry
1580
+ // passthrough) must be present on the host entry by name; the host-resolved
1581
+ // value differs per host (a literal, a `${VAR}`, a `{env:VAR}`) so only key
1582
+ // presence is compared. Absence — including "no env block at all" — means stale.
1583
+
1584
+ function envKeysMissing(existingEnv, desiredEnv) {
1585
+ const desiredKeys = Object.keys(desiredEnv ?? {});
1586
+ if (desiredKeys.length === 0) return false;
1587
+ const existingKeys = new Set(Object.keys(existingEnv ?? {}));
1588
+ return desiredKeys.some((key) => !existingKeys.has(key));
1589
+ }
1590
+
1591
+ // A version-pinned package arg (`@scope/name@version`, including the `@latest`
1592
+ // anti-pin) is compared as an exact string: if the registry's desired pin string
1593
+ // is not present verbatim anywhere in the existing args/command, the host is
1594
+ // carrying a different (or floating) version and must be refreshed. Unversioned
1595
+ // packages are not compared — pinning is opt-in per package via the catalog.
1596
+
1597
+ function argsVersionDiffers(existingArgs, desiredArgs) {
1598
+ const isPinned = (arg) => typeof arg === "string" && /^@[^/]+\/[^@]+@[\w.-]+$/.test(arg);
1599
+ const desiredPins = (desiredArgs ?? []).filter(isPinned);
1600
+ if (desiredPins.length === 0) return false;
1601
+ const existingPins = new Set((existingArgs ?? []).filter(isPinned));
1602
+ return desiredPins.some((pin) => !existingPins.has(pin));
1603
+ }
1604
+
1605
+ export function needsRefresh(existingEntry, desiredEntry, { root } = {}) {
1606
+ if (!existingEntry) return true;
1607
+ if (JSON.stringify(existingEntry).includes("__")) return true;
1608
+ if (!entryIsRemote(desiredEntry) && entryIsRemote(existingEntry)) return true;
1609
+ if (envKeysMissing(entryEnv(existingEntry), entryEnv(desiredEntry))) return true;
1610
+ if (argsVersionDiffers(entryArgs(existingEntry), entryArgs(desiredEntry))) return true;
1611
+ if (root && mcpEntryPointsOutsideToolkit({ args: entryArgs(existingEntry) }, root)) return true;
1612
+ if (desiredEntry?.cwd !== undefined && existingEntry.cwd !== desiredEntry.cwd) return true;
1613
+ return false;
1614
+ }
1615
+
1616
+ // VS Code's orchestration tools default their working directory to the SERVER
1617
+ // process's cwd, which is host-launch-dependent (construct-6y6w.9) — the same
1618
+ // tool call can read/write a different project depending on which host
1619
+ // launched the server and from where. VS Code resolves `${workspaceFolder}`
1620
+ // against the workspace that owns the `.vscode/mcp.json` the entry lives in,
1621
+ // so pinning it there removes the ambiguity for exactly the host that has a
1622
+ // single fixed workspace folder per config file. A remote/http entry has no
1623
+ // process cwd to pin and is left untouched.
1624
+
1625
+ const VSCODE_WORKSPACE_CWD = "${workspaceFolder}";
1626
+
1627
+ function withVscodeWorkspaceCwd(entry) {
1628
+ if (!entry || entry.type === "http" || entry.type === "remote") return entry;
1629
+ return { ...entry, cwd: VSCODE_WORKSPACE_CWD };
1630
+ }
1631
+
1489
1632
  // A merged mcp.json preserves existing entries so user customizations survive a
1490
1633
  // re-sync. A construct-owned server path is a fully-resolved, non-placeholder
1491
1634
  // path, so the preserve rule keeps it even when it points at a different toolkit
@@ -1493,34 +1636,119 @@ function getVSCodeUserMcpPaths() {
1493
1636
  // exist. Treat a construct toolkit path outside the current root as stale so the
1494
1637
  // sync refreshes it; user-owned servers carry no lib/mcp toolkit path and stay.
1495
1638
 
1639
+ // Claude/VS Code/Cursor entries carry the script path in `args`; OpenCode's local
1640
+ // entry shape (buildOpenCodeMcpEntry) instead carries `command: [bin, ...args]`.
1641
+ // Falling back to `command` when `args` is absent lets this one predicate serve
1642
+ // both shapes without OpenCode's caller having to reshape the entry first.
1643
+
1496
1644
  export function mcpEntryPointsOutsideToolkit(entry, root) {
1497
- const args = Array.isArray(entry?.args) ? entry.args : [];
1498
- return args.some(
1499
- (arg) => typeof arg === "string"
1500
- && /\/lib\/mcp\/[a-z0-9-]+\.mjs$/.test(arg)
1501
- && !arg.startsWith(`${root}/`),
1502
- );
1645
+ const args = Array.isArray(entry?.args) ? entry.args : (Array.isArray(entry?.command) ? entry.command : []);
1646
+ return args.some((arg) => {
1647
+ if (typeof arg !== "string") return false;
1648
+ const normalArg = arg.replace(/\\/g, "/");
1649
+ const normalRoot = root.replace(/\\/g, "/");
1650
+ return /\/lib\/mcp\/[a-z0-9-]+\.mjs$/i.test(normalArg)
1651
+ && !normalArg.startsWith(`${normalRoot}/`);
1652
+ });
1503
1653
  }
1504
1654
 
1505
- // VS Code scans both `.github/agents` and `.claude/agents`, so the orchestrator
1506
- // would list twice once with VS Code tools (.github/agents) and once with
1507
- // Claude tool names VS Code ignores (.claude/agents). `chat.agentFilesLocations`
1508
- // is the documented lever to pin the scan; its power over the built-in
1509
- // `.claude/agents` compatibility scan is version-dependent, so this is a
1510
- // best-effort hint, not a guarantee. Merge only into a strictly-parseable file
1511
- // and never overwrite an existing choice, so a commented (JSONC) or
1512
- // user-customized settings.json is left untouched.
1655
+ // The desired-set loop only visits registryMcp ids, so a construct-managed entry
1656
+ // present in the host config but OUTSIDE the current sync set (an optional server
1657
+ // like `memory` the user opted into) is never revisited — a stale toolkit path in it
1658
+ // becomes immortal. This second pass rewrites those in place: the double guard (id is
1659
+ // construct-managed AND its path points outside the toolkit) never touches an
1660
+ // unmanaged/user entry, and rewriting to a path under root makes it idempotent (a
1661
+ // second run finds nothing stale). Rewrite, never delete, never seed — opt-in sticks.
1662
+
1663
+ // A memory entry can be stale without pointing outside the toolkit: pinned to the dead
1664
+ // legacy port, to a port other than the one currently allocated, or to a bridge script
1665
+ // path that does not exist (an old checkout). Any of these means the entry cannot reach
1666
+ // the running memory server — the split-brain — so it must be rewritten to the current
1667
+ // port and path.
1668
+
1669
+ function memoryEntryIsStale(entry) {
1670
+ if (!entry) return false;
1671
+ const want = String(memoryPort(process.env));
1672
+ const portOf = (s) => String(s || "").match(/:(\d+)\/?$/)?.[1];
1673
+ const bridgeUrlEnv = entry.env?.CONSTRUCT_MEMORY_BRIDGE_URL ?? entry.environment?.CONSTRUCT_MEMORY_BRIDGE_URL;
1674
+ const ports = [portOf(entry.url), portOf(bridgeUrlEnv)];
1675
+ const portStale = ports.some((p) => p && p !== want);
1676
+ const commandArgs = Array.isArray(entry.args) ? entry.args : (Array.isArray(entry.command) ? entry.command : []);
1677
+ const scriptArg = commandArgs.find((a) => typeof a === "string" && a.endsWith("memory-bridge.mjs"));
1678
+ const pathMissing = scriptArg ? !fs.existsSync(scriptArg) : false;
1679
+ return portStale || pathMissing;
1680
+ }
1681
+
1682
+ // OpenCode keys config.mcp by getOpenCodeMcpId(id), not the catalog id (identity
1683
+ // today, but the indirection exists for a future per-host rename) — so the host
1684
+ // config key this loop sees is not always the id `registryMcp`/`managedMcpDefs()`
1685
+ // know. `mapId` translates the host key back to the catalog id for the ownership
1686
+ // and sync-set checks; the write-back stays keyed on the original host key so no
1687
+ // entry is renamed, only rewritten in place.
1688
+
1689
+ export function reconcileStaleManagedEntries(configMap, { registryMcp, rebuildEntry, mapId = (key) => key }) {
1690
+ if (!configMap) return false;
1691
+ const managed = managedMcpDefs();
1692
+ let changed = false;
1693
+ for (const [key, entry] of Object.entries(configMap)) {
1694
+ const catalogId = mapId(key);
1695
+ if (catalogId in registryMcp) continue;
1696
+ const mcpDef = managed[catalogId];
1697
+ if (!mcpDef) continue;
1698
+ const stale = mcpEntryPointsOutsideToolkit(entry, root) || (catalogId === "memory" && memoryEntryIsStale(entry));
1699
+ if (!stale) continue;
1700
+ configMap[key] = rebuildEntry(catalogId, mcpDef);
1701
+ changed = true;
1702
+ }
1703
+ return changed;
1704
+ }
1705
+
1706
+ // Workspace defaults Construct manages for VS Code chat. `chat.agentFilesLocations`
1707
+ // pins the agent scan to `.github/agents` so the orchestrator does not list twice
1708
+ // (VS Code also scans `.claude/agents`, whose Claude tool names it ignores); its
1709
+ // power over that built-in compatibility scan is version-dependent, so it is a
1710
+ // best-effort hint. `chat.mcp.autostart` (VS Code ≥1.105, string enum) set to
1711
+ // `always` eager-starts MCP servers so `construct-mcp` is live without a manual
1712
+ // Start each session — the orchestrator's first move is an MCP call, so a dormant
1713
+ // server otherwise reads as "enable the MCP server". Neither removes the one-time
1714
+ // per-developer MCP trust grant, which VS Code stores locally, not in committed
1715
+ // config. Each key is applied only when unset, and a settings.json that is not
1716
+ // strict JSON (commented/JSONC or user-customized) is left untouched.
1717
+
1718
+ const VSCODE_MANAGED_SETTINGS = {
1719
+ "chat.agentFilesLocations": { ".github/agents": true, ".claude/agents": false },
1720
+ "chat.mcp.autoStart": "always",
1721
+ };
1722
+
1723
+ // Strip full-line JSONC comments (`// …`) and trailing commas before JSON.parse.
1724
+ // Handles the common VS Code settings.json patterns (line comments, trailing commas);
1725
+ // does not attempt to handle inline comments after values.
1726
+
1727
+ function parseJsoncContent(text) {
1728
+ const stripped = text
1729
+ .split('\n')
1730
+ .map((line) => {
1731
+ const t = line.trimStart();
1732
+ return t.startsWith('//') ? '' : line;
1733
+ })
1734
+ .join('\n')
1735
+ .replace(/,(\s*[}\]])/g, '$1');
1736
+ return JSON.parse(stripped);
1737
+ }
1513
1738
 
1514
- export function pinVscodeAgentLocations(targetDir) {
1739
+ export function pinVscodeChatSettings(targetDir) {
1515
1740
  if (DRY_RUN) return;
1516
1741
  const settingsPath = path.join(targetDir, ".vscode", "settings.json");
1517
1742
  let settings = {};
1518
1743
  if (fs.existsSync(settingsPath)) {
1519
- try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")) || {}; }
1744
+ try { settings = parseJsoncContent(fs.readFileSync(settingsPath, "utf8")) || {}; }
1520
1745
  catch { return; }
1521
1746
  }
1522
- if (settings["chat.agentFilesLocations"]) return;
1523
- settings["chat.agentFilesLocations"] = { ".github/agents": true, ".claude/agents": false };
1747
+ let changed = false;
1748
+ for (const [key, value] of Object.entries(VSCODE_MANAGED_SETTINGS)) {
1749
+ if (settings[key] === undefined) { settings[key] = value; changed = true; }
1750
+ }
1751
+ if (!changed) return;
1524
1752
  mkdirp(path.dirname(settingsPath));
1525
1753
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1526
1754
  }
@@ -1568,20 +1796,16 @@ function syncVSCode(targetDir = null, wants = true) {
1568
1796
  if (!config.servers) config.servers = {};
1569
1797
  for (const [id, mcpDef] of Object.entries(registryMcp)) {
1570
1798
  const existingEntry = config.servers[id];
1571
- const existing = JSON.stringify(existingEntry ?? "");
1572
- const hasPlaceholder = existing.includes("__");
1573
- const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
1574
- const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1575
- const transportMismatch = registryWantsCommand && existingIsRemote;
1576
- const staleToolkitPath = mcpEntryPointsOutsideToolkit(existingEntry, root);
1577
- if (existingEntry && !hasPlaceholder && !transportMismatch && !staleToolkitPath) continue;
1578
- config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1799
+ const desiredEntry = withVscodeWorkspaceCwd(buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" }));
1800
+ if (!needsRefresh(existingEntry, desiredEntry, { root })) continue;
1801
+ config.servers[id] = desiredEntry;
1579
1802
  }
1803
+ reconcileStaleManagedEntries(config.servers, { registryMcp, rebuildEntry: (id, def) => withVscodeWorkspaceCwd(buildClaudeMcpEntry(id, def, process.env, { host: "vscode" })) });
1580
1804
  if (!DRY_RUN) {
1581
1805
  mkdirp(path.dirname(mcpPath));
1582
1806
  fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
1583
1807
  }
1584
- pinVscodeAgentLocations(targetDir);
1808
+ pinVscodeChatSettings(targetDir);
1585
1809
  return true;
1586
1810
  }
1587
1811
 
@@ -1594,14 +1818,9 @@ function syncVSCode(targetDir = null, wants = true) {
1594
1818
  if (!config.servers) config.servers = {};
1595
1819
  for (const [id, mcpDef] of Object.entries(registryMcp)) {
1596
1820
  const existingEntry = config.servers[id];
1597
- const existing = JSON.stringify(existingEntry ?? "");
1598
- const hasPlaceholder = existing.includes("__");
1599
- const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
1600
- const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1601
- const transportMismatch = registryWantsCommand && existingIsRemote;
1602
- const staleToolkitPath = mcpEntryPointsOutsideToolkit(existingEntry, root);
1603
- if (existingEntry && !hasPlaceholder && !transportMismatch && !staleToolkitPath) continue;
1604
- config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1821
+ const desiredEntry = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1822
+ if (!needsRefresh(existingEntry, desiredEntry, { root })) continue;
1823
+ config.servers[id] = desiredEntry;
1605
1824
  }
1606
1825
  if (!DRY_RUN) fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
1607
1826
  synced = true;
@@ -1665,14 +1884,11 @@ function syncCursor(targetDir = null, wants = true) {
1665
1884
  if (!config.mcpServers) config.mcpServers = {};
1666
1885
  for (const [id, mcpDef] of Object.entries(registryMcp)) {
1667
1886
  const existingEntry = config.mcpServers[id];
1668
- const existing = JSON.stringify(existingEntry ?? "");
1669
- const hasPlaceholder = existing.includes("__");
1670
- const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
1671
- const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1672
- const transportMismatch = registryWantsCommand && existingIsRemote;
1673
- if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
1674
- config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1887
+ const desiredEntry = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1888
+ if (!needsRefresh(existingEntry, desiredEntry, { root })) continue;
1889
+ config.mcpServers[id] = desiredEntry;
1675
1890
  }
1891
+ reconcileStaleManagedEntries(config.mcpServers, { registryMcp, rebuildEntry: (id, def) => buildClaudeMcpEntry(id, def, process.env, { host: "vscode" }) });
1676
1892
  if (!DRY_RUN) {
1677
1893
  mkdirp(path.dirname(cursorMcpPath));
1678
1894
  fs.writeFileSync(cursorMcpPath, JSON.stringify(config, null, 2) + "\n");
@@ -1944,18 +2160,32 @@ function syncOpencode(entries, targetDir = null, wants = true) {
1944
2160
  // any remote/http memory entry must be rewritten when the registry
1945
2161
  // defines a command-based bridge.
1946
2162
  const existingEntry = config.mcp[openCodeId];
1947
- const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
1948
- const existingIsRemote = existingEntry && (existingEntry.type === 'remote' || existingEntry.type === 'http');
1949
- const transportMismatch = registryWantsCommand && existingIsRemote;
2163
+ const desiredEntry = buildOpenCodeMcpEntry(id, mcpDef, process.env).entry;
1950
2164
 
1951
- const existing = JSON.stringify(existingEntry ?? "");
1952
- const hasPlaceholder = existing.includes("__");
2165
+ // The registry's own args can carry an unresolved `__NAME__` template (a
2166
+ // secret var not yet set in this environment); needsRefresh only inspects
2167
+ // built entries, so a still-templated registry def is checked separately
2168
+ // and always wins a refresh once the var resolves.
1953
2169
  const argsHaveTemplates = (mcpDef.args ?? []).some((a) => typeof a === 'string' && a.includes('__'));
1954
- if (!existingEntry || hasPlaceholder || argsHaveTemplates || transportMismatch) {
1955
- config.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcpDef, process.env).entry;
2170
+ if (argsHaveTemplates || needsRefresh(existingEntry, desiredEntry, { root })) {
2171
+ config.mcp[openCodeId] = desiredEntry;
1956
2172
  }
1957
2173
  }
1958
2174
 
2175
+ // The sync-set loop above only visits `registryMcp` ids, so a managed-but-optional
2176
+ // entry (e.g. `memory` in project scope) with a stale toolkit path is never
2177
+ // revisited here either — the same immortal-entry gap fixed for VS Code/Cursor/
2178
+ // Claude project (construct-6y6w.1). `getOpenCodeMcpId` keys config.mcp by the
2179
+ // host id rather than the catalog id, so map it back before checking ownership.
2180
+ const catalogIdForOpenCodeKey = new Map(
2181
+ Object.keys(managedMcpDefs()).map((catalogId) => [getOpenCodeMcpId(catalogId), catalogId]),
2182
+ );
2183
+ reconcileStaleManagedEntries(config.mcp, {
2184
+ registryMcp,
2185
+ mapId: (key) => catalogIdForOpenCodeKey.get(key) ?? key,
2186
+ rebuildEntry: (catalogId, def) => buildOpenCodeMcpEntry(catalogId, def, process.env).entry,
2187
+ });
2188
+
1959
2189
  // Heavy external MCP servers serialize a measured ~37k tokens of tool schema
1960
2190
  // into EVERY agent's request — github ~30k alone (fixtures 2026-06-22) —
1961
2191
  // agent's request — including the built-in Build/Plan agents the per-agent
@@ -2033,7 +2263,7 @@ function syncOpencode(entries, targetDir = null, wants = true) {
2033
2263
  };
2034
2264
  }
2035
2265
 
2036
- // Hybrid split (aider architect/editor). When the fast tier is a LOCAL model, emit a
2266
+ // Hybrid split (cheap local editor, capable architect). When the fast tier is a LOCAL model, emit a
2037
2267
  // narrow `construct-local` editor: it does bounded edits on a cheap local model and hands
2038
2268
  // planning/reasoning back to `construct` (the architect, which stays on the user's chosen
2039
2269
  // model — we never pin it). The editor's model is NOT the generic fast-tier default
@@ -2096,17 +2326,17 @@ function syncOpencode(entries, targetDir = null, wants = true) {
2096
2326
  delete config.model;
2097
2327
  }
2098
2328
 
2099
- // OpenCode's built-in helper agents drive session naming, summaries, and
2100
- // compaction. Keep them on a stronger auxiliary model so those surfaces do not
2101
- // inherit the main chat model's selection history or a low-end coder default.
2102
- const auxiliaryModel = resolvedModels.reasoning || resolvedModels.standard || resolvedModels.fast;
2103
- if (auxiliaryModel) {
2104
- for (const key of ["title", "summary", "compaction"]) {
2105
- config.agent[key] = {
2106
- ...(config.agent[key] && typeof config.agent[key] === "object" ? config.agent[key] : {}),
2107
- model: auxiliaryModel,
2108
- };
2109
- }
2329
+ // OpenCode's built-in helper agents (session naming, summaries, compaction)
2330
+ // must follow the host's live routing an absolute model pin here made
2331
+ // compaction call a provider the user had no key/credits for, failing hard
2332
+ // even though a working model was selected for the session. Keep the entries
2333
+ // present (hosts and tests rely on them existing) but strip any model pin a
2334
+ // previous sync wrote, whether the old weak coder default or a strong tier
2335
+ // pin, so OpenCode falls back to its own session-model/small_model routing.
2336
+ for (const key of ["title", "summary", "compaction"]) {
2337
+ const existing = config.agent[key] && typeof config.agent[key] === "object" ? config.agent[key] : {};
2338
+ delete existing.model;
2339
+ config.agent[key] = existing;
2110
2340
  }
2111
2341
 
2112
2342
  writeOpenCodeConfig(config, configPath);