agent-trellis 0.7.0 → 0.8.2

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 (302) hide show
  1. package/README.md +48 -153
  2. package/dist/adapters/claude-code.js +3 -2
  3. package/dist/adapters/codex.js +3 -2
  4. package/dist/adapters/jsonMcp.d.ts +3 -1
  5. package/dist/adapters/jsonMcp.js +18 -4
  6. package/dist/adapters/kimi-code.d.ts +21 -0
  7. package/dist/adapters/kimi-code.js +107 -0
  8. package/dist/adapters/kiro.js +3 -2
  9. package/dist/adapters/mcpPlan.d.ts +6 -1
  10. package/dist/adapters/mcpPlan.js +106 -8
  11. package/dist/adapters/pi.js +22 -3
  12. package/dist/adapters/symlinkPlan.d.ts +7 -0
  13. package/dist/adapters/symlinkPlan.js +16 -7
  14. package/dist/adapters/zcode.d.ts +21 -0
  15. package/dist/adapters/zcode.js +234 -0
  16. package/dist/cli.js +145 -29
  17. package/dist/commands/doctor.d.ts +7 -2
  18. package/dist/commands/doctor.js +40 -2
  19. package/dist/commands/init.d.ts +4 -2
  20. package/dist/commands/init.js +29 -13
  21. package/dist/commands/kimi.d.ts +16 -0
  22. package/dist/commands/kimi.js +64 -0
  23. package/dist/commands/manage.d.ts +39 -0
  24. package/dist/commands/manage.js +126 -0
  25. package/dist/commands/mcp.d.ts +77 -14
  26. package/dist/commands/mcp.js +169 -29
  27. package/dist/commands/mcpGateway.d.ts +6 -0
  28. package/dist/commands/mcpGateway.js +35 -14
  29. package/dist/commands/mcpImport.d.ts +46 -0
  30. package/dist/commands/mcpImport.js +359 -0
  31. package/dist/commands/memory.d.ts +17 -3
  32. package/dist/commands/memory.js +27 -8
  33. package/dist/commands/migrate.d.ts +16 -5
  34. package/dist/commands/migrate.js +93 -17
  35. package/dist/commands/onboard.d.ts +74 -2
  36. package/dist/commands/onboard.js +469 -71
  37. package/dist/commands/rollback.js +28 -2
  38. package/dist/commands/secretsAudit.js +4 -1
  39. package/dist/commands/skill.d.ts +52 -6
  40. package/dist/commands/skill.js +114 -15
  41. package/dist/commands/sync.js +5 -1
  42. package/dist/core/adapter.d.ts +11 -1
  43. package/dist/core/canonical.d.ts +15 -7
  44. package/dist/core/canonical.js +101 -13
  45. package/dist/core/capabilitySelection.d.ts +45 -0
  46. package/dist/core/capabilitySelection.js +124 -0
  47. package/dist/core/types.d.ts +25 -2
  48. package/dist/core/types.js +8 -0
  49. package/dist/lib/agentBridge.d.ts +169 -0
  50. package/dist/lib/agentBridge.js +367 -0
  51. package/dist/lib/agentBridgeProvider.d.ts +14 -0
  52. package/dist/lib/agentBridgeProvider.js +95 -0
  53. package/dist/lib/backup.d.ts +32 -0
  54. package/dist/lib/backup.js +20 -1
  55. package/dist/lib/builtinSkills.d.ts +2 -0
  56. package/dist/lib/builtinSkills.js +4 -0
  57. package/dist/lib/chatAgents.d.ts +42 -0
  58. package/dist/lib/chatAgents.js +59 -0
  59. package/dist/lib/cliMetadata.d.ts +1 -0
  60. package/dist/lib/cliMetadata.js +14 -0
  61. package/dist/lib/envVarNames.d.ts +3 -0
  62. package/dist/lib/envVarNames.js +19 -0
  63. package/dist/lib/gatewayBackend.d.ts +28 -0
  64. package/dist/lib/gatewayBackend.js +72 -1
  65. package/dist/lib/installAgent.d.ts +7 -5
  66. package/dist/lib/installAgent.js +4 -3
  67. package/dist/lib/mcpConnect.js +5 -1
  68. package/dist/lib/mcpMigrateRead.d.ts +2 -0
  69. package/dist/lib/mcpMigrateRead.js +22 -1
  70. package/dist/lib/mcpRuntime.d.ts +46 -0
  71. package/dist/lib/mcpRuntime.js +185 -0
  72. package/dist/lib/mcpStatusProvider.d.ts +10 -0
  73. package/dist/lib/mcpStatusProvider.js +25 -0
  74. package/dist/lib/mcpToolRegistry.d.ts +18 -0
  75. package/dist/lib/mcpToolRegistry.js +107 -14
  76. package/dist/lib/memoryGraph.d.ts +1 -1
  77. package/dist/lib/memoryGraph.js +9 -3
  78. package/dist/lib/memoryProvider.d.ts +58 -0
  79. package/dist/lib/memoryProvider.js +175 -0
  80. package/dist/lib/memoryStatus.d.ts +17 -0
  81. package/dist/lib/memoryStatus.js +88 -0
  82. package/dist/lib/nativeMemory.d.ts +19 -0
  83. package/dist/lib/nativeMemory.js +129 -0
  84. package/dist/lib/realHomeSnapshot.js +5 -0
  85. package/dist/lib/runtimeControlProvider.d.ts +16 -0
  86. package/dist/lib/runtimeControlProvider.js +170 -0
  87. package/dist/lib/secretEnv.d.ts +2 -1
  88. package/dist/lib/secretEnv.js +6 -2
  89. package/dist/lib/skillProvider.d.ts +13 -0
  90. package/dist/lib/skillProvider.js +210 -0
  91. package/dist/lib/taskProvider.d.ts +7 -0
  92. package/dist/lib/taskProvider.js +43 -0
  93. package/dist/lib/taskStore.d.ts +46 -0
  94. package/dist/lib/taskStore.js +156 -0
  95. package/dist/lib/terminalPicker.d.ts +23 -49
  96. package/dist/lib/terminalPicker.js +80 -218
  97. package/dist/lib/zcodeOwnership.d.ts +17 -0
  98. package/dist/lib/zcodeOwnership.js +37 -0
  99. package/dist/pi-bridge/bundle.js +476 -37
  100. package/dist/pi-bridge/index.js +42 -21
  101. package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
  102. package/dist/pi-bridge/schemaTranslate.js +4 -8
  103. package/dist/probes/codex.d.ts +7 -0
  104. package/dist/probes/codex.js +37 -5
  105. package/dist/probes/kimi-code.d.ts +12 -0
  106. package/dist/probes/kimi-code.js +55 -0
  107. package/dist/probes/zcode.d.ts +32 -0
  108. package/dist/probes/zcode.js +140 -0
  109. package/dist/sdk.d.ts +1 -1
  110. package/docs/architecture.md +230 -28
  111. package/docs/assets/brand-v3/trellis-mark-v3-a.png +0 -0
  112. package/docs/assets/brand-v3/trellis-mark-v3-a.svg +10 -0
  113. package/docs/assets/poster-series-v3/README.md +60 -0
  114. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.png +0 -0
  115. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.svg +51 -0
  116. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.png +0 -0
  117. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.svg +51 -0
  118. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.png +0 -0
  119. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.svg +48 -0
  120. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.png +0 -0
  121. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.svg +48 -0
  122. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.png +0 -0
  123. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.svg +56 -0
  124. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.png +0 -0
  125. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.svg +56 -0
  126. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.png +0 -0
  127. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.svg +47 -0
  128. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.png +0 -0
  129. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.svg +47 -0
  130. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.png +0 -0
  131. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.svg +51 -0
  132. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.png +0 -0
  133. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.svg +51 -0
  134. package/docs/getting-started.md +496 -42
  135. package/docs/overview.md +134 -0
  136. package/docs/overview.zh-CN.md +102 -0
  137. package/docs/research.md +59 -0
  138. package/docs/roadmap.md +11 -6
  139. package/docs/video/README.md +76 -0
  140. package/docs/video/build/audio/01-opening.aiff +0 -0
  141. package/docs/video/build/audio/01-opening.mp3 +0 -0
  142. package/docs/video/build/audio/02-pain.aiff +0 -0
  143. package/docs/video/build/audio/02-pain.mp3 +0 -0
  144. package/docs/video/build/audio/03-solution.aiff +0 -0
  145. package/docs/video/build/audio/03-solution.mp3 +0 -0
  146. package/docs/video/build/audio/04-install.aiff +0 -0
  147. package/docs/video/build/audio/04-install.mp3 +0 -0
  148. package/docs/video/build/audio/04-setup.mp3 +0 -0
  149. package/docs/video/build/audio/05-dryrun.mp3 +0 -0
  150. package/docs/video/build/audio/05-init.aiff +0 -0
  151. package/docs/video/build/audio/05-init.mp3 +0 -0
  152. package/docs/video/build/audio/05-interactive.mp3 +0 -0
  153. package/docs/video/build/audio/06-dryrun.aiff +0 -0
  154. package/docs/video/build/audio/06-dryrun.mp3 +0 -0
  155. package/docs/video/build/audio/06-onboard.mp3 +0 -0
  156. package/docs/video/build/audio/06-run.mp3 +0 -0
  157. package/docs/video/build/audio/07-onboard.aiff +0 -0
  158. package/docs/video/build/audio/07-onboard.mp3 +0 -0
  159. package/docs/video/build/audio/07-verify.mp3 +0 -0
  160. package/docs/video/build/audio/08-closing.mp3 +0 -0
  161. package/docs/video/build/audio/08-doctor.aiff +0 -0
  162. package/docs/video/build/audio/08-doctor.mp3 +0 -0
  163. package/docs/video/build/audio/09-rollback.aiff +0 -0
  164. package/docs/video/build/audio/09-rollback.mp3 +0 -0
  165. package/docs/video/build/audio/10-principles.aiff +0 -0
  166. package/docs/video/build/audio/10-principles.mp3 +0 -0
  167. package/docs/video/build/audio/11-closing.aiff +0 -0
  168. package/docs/video/build/audio/11-closing.mp3 +0 -0
  169. package/docs/video/build/frames/01-opening-0.png +0 -0
  170. package/docs/video/build/frames/02-pain-1.png +0 -0
  171. package/docs/video/build/frames/02-pain-2.png +0 -0
  172. package/docs/video/build/frames/03-solution-3.png +0 -0
  173. package/docs/video/build/frames/04-install-4.png +0 -0
  174. package/docs/video/build/frames/04-setup-4.png +0 -0
  175. package/docs/video/build/frames/04-setup-5.png +0 -0
  176. package/docs/video/build/frames/05-dryrun-6.png +0 -0
  177. package/docs/video/build/frames/05-init-5.png +0 -0
  178. package/docs/video/build/frames/05-interactive-5.png +0 -0
  179. package/docs/video/build/frames/05-interactive-6.png +0 -0
  180. package/docs/video/build/frames/06-dryrun-6.png +0 -0
  181. package/docs/video/build/frames/06-dryrun-7.png +0 -0
  182. package/docs/video/build/frames/06-onboard-7.png +0 -0
  183. package/docs/video/build/frames/06-onboard-8.png +0 -0
  184. package/docs/video/build/frames/06-run-7.png +0 -0
  185. package/docs/video/build/frames/06-run-8.png +0 -0
  186. package/docs/video/build/frames/07-onboard-10.png +0 -0
  187. package/docs/video/build/frames/07-onboard-7.png +0 -0
  188. package/docs/video/build/frames/07-onboard-8.png +0 -0
  189. package/docs/video/build/frames/07-onboard-9.png +0 -0
  190. package/docs/video/build/frames/07-verify-10.png +0 -0
  191. package/docs/video/build/frames/07-verify-8.png +0 -0
  192. package/docs/video/build/frames/07-verify-9.png +0 -0
  193. package/docs/video/build/frames/08-closing-10.png +0 -0
  194. package/docs/video/build/frames/08-closing-11.png +0 -0
  195. package/docs/video/build/frames/08-doctor-11.png +0 -0
  196. package/docs/video/build/frames/08-doctor-9.png +0 -0
  197. package/docs/video/build/frames/09-rollback-10.png +0 -0
  198. package/docs/video/build/frames/09-rollback-12.png +0 -0
  199. package/docs/video/build/frames/10-principles-11.png +0 -0
  200. package/docs/video/build/frames/10-principles-13.png +0 -0
  201. package/docs/video/build/frames/11-closing-12.png +0 -0
  202. package/docs/video/build/frames/11-closing-14.png +0 -0
  203. package/docs/video/build/segments/01-opening.mp4 +0 -0
  204. package/docs/video/build/segments/01-opening.txt +3 -0
  205. package/docs/video/build/segments/02-pain.mp4 +0 -0
  206. package/docs/video/build/segments/02-pain.txt +5 -0
  207. package/docs/video/build/segments/03-solution.mp4 +0 -0
  208. package/docs/video/build/segments/03-solution.txt +3 -0
  209. package/docs/video/build/segments/04-install.mp4 +0 -0
  210. package/docs/video/build/segments/04-install.txt +3 -0
  211. package/docs/video/build/segments/04-setup.mp4 +0 -0
  212. package/docs/video/build/segments/04-setup.txt +3 -0
  213. package/docs/video/build/segments/05-dryrun.mp4 +0 -0
  214. package/docs/video/build/segments/05-dryrun.txt +3 -0
  215. package/docs/video/build/segments/05-init.mp4 +0 -0
  216. package/docs/video/build/segments/05-init.txt +3 -0
  217. package/docs/video/build/segments/05-interactive.mp4 +0 -0
  218. package/docs/video/build/segments/05-interactive.txt +5 -0
  219. package/docs/video/build/segments/06-dryrun.mp4 +0 -0
  220. package/docs/video/build/segments/06-dryrun.txt +3 -0
  221. package/docs/video/build/segments/06-onboard.mp4 +0 -0
  222. package/docs/video/build/segments/06-onboard.txt +5 -0
  223. package/docs/video/build/segments/06-run.mp4 +0 -0
  224. package/docs/video/build/segments/06-run.txt +3 -0
  225. package/docs/video/build/segments/07-onboard.mp4 +0 -0
  226. package/docs/video/build/segments/07-onboard.txt +5 -0
  227. package/docs/video/build/segments/07-verify.mp4 +0 -0
  228. package/docs/video/build/segments/07-verify.txt +5 -0
  229. package/docs/video/build/segments/08-closing.mp4 +0 -0
  230. package/docs/video/build/segments/08-closing.txt +3 -0
  231. package/docs/video/build/segments/08-doctor.mp4 +0 -0
  232. package/docs/video/build/segments/08-doctor.txt +3 -0
  233. package/docs/video/build/segments/09-rollback.mp4 +0 -0
  234. package/docs/video/build/segments/09-rollback.txt +3 -0
  235. package/docs/video/build/segments/10-principles.mp4 +0 -0
  236. package/docs/video/build/segments/10-principles.txt +3 -0
  237. package/docs/video/build/segments/11-closing.mp4 +0 -0
  238. package/docs/video/build/segments/11-closing.txt +3 -0
  239. package/docs/video/build/segments/final.txt +8 -0
  240. package/docs/video/build-ai/audio/01-hook.mp3 +0 -0
  241. package/docs/video/build-ai/audio/02-pain.mp3 +0 -0
  242. package/docs/video/build-ai/audio/03-solution.mp3 +0 -0
  243. package/docs/video/build-ai/audio/04-how.mp3 +0 -0
  244. package/docs/video/build-ai/audio/05-safe.mp3 +0 -0
  245. package/docs/video/build-ai/audio/06-closing.mp3 +0 -0
  246. package/docs/video/build-ai/clips/01-hook.mp4 +0 -0
  247. package/docs/video/build-ai/clips/02-pain.mp4 +0 -0
  248. package/docs/video/build-ai/clips/03-solution.mp4 +0 -0
  249. package/docs/video/build-ai/clips/04-how.mp4 +0 -0
  250. package/docs/video/build-ai/clips/05-safe.mp4 +0 -0
  251. package/docs/video/build-ai/clips/06-closing.mp4 +0 -0
  252. package/docs/video/build-ai/first-frame-02-pain.png +0 -0
  253. package/docs/video/build-ai/first-frame-03-solution.png +0 -0
  254. package/docs/video/build-ai/first-frame-04-how.png +0 -0
  255. package/docs/video/build-ai/first-frame-05-safe.png +0 -0
  256. package/docs/video/build-ai/segments/01-hook.caption.txt +2 -0
  257. package/docs/video/build-ai/segments/01-hook.mp4 +0 -0
  258. package/docs/video/build-ai/segments/02-pain.mp4 +0 -0
  259. package/docs/video/build-ai/segments/03-solution.mp4 +0 -0
  260. package/docs/video/build-ai/segments/04-how.mp4 +0 -0
  261. package/docs/video/build-ai/segments/05-safe.mp4 +0 -0
  262. package/docs/video/build-ai/segments/06-closing.mp4 +0 -0
  263. package/docs/video/build-ai/segments/cap-01-hook.png +0 -0
  264. package/docs/video/build-ai/segments/cap-02-pain.png +0 -0
  265. package/docs/video/build-ai/segments/cap-03-solution.png +0 -0
  266. package/docs/video/build-ai/segments/cap-04-how.png +0 -0
  267. package/docs/video/build-ai/segments/cap-05-safe.png +0 -0
  268. package/docs/video/build-ai/segments/cap-06-closing.png +0 -0
  269. package/docs/video/build-ai/segments/final.txt +6 -0
  270. package/docs/video/build-posters/audio/01-hook.mp3 +0 -0
  271. package/docs/video/build-posters/audio/02-pain.mp3 +0 -0
  272. package/docs/video/build-posters/audio/03-solution.mp3 +0 -0
  273. package/docs/video/build-posters/audio/04-how.mp3 +0 -0
  274. package/docs/video/build-posters/audio/05-safe.mp3 +0 -0
  275. package/docs/video/build-posters/audio/06-closing.mp3 +0 -0
  276. package/docs/video/build-posters/cap-01-hook.png +0 -0
  277. package/docs/video/build-posters/cap-02-pain.png +0 -0
  278. package/docs/video/build-posters/cap-03-solution.png +0 -0
  279. package/docs/video/build-posters/cap-04-how.png +0 -0
  280. package/docs/video/build-posters/cap-05-safe.png +0 -0
  281. package/docs/video/build-posters/cap-06-closing.png +0 -0
  282. package/docs/video/build-posters/card-01-hook.png +0 -0
  283. package/docs/video/build-posters/card-06-closing.png +0 -0
  284. package/docs/video/build-posters/segments/01-hook.mp4 +0 -0
  285. package/docs/video/build-posters/segments/02-pain.mp4 +0 -0
  286. package/docs/video/build-posters/segments/03-solution.mp4 +0 -0
  287. package/docs/video/build-posters/segments/04-how.mp4 +0 -0
  288. package/docs/video/build-posters/segments/05-safe.mp4 +0 -0
  289. package/docs/video/build-posters/segments/06-closing.mp4 +0 -0
  290. package/docs/video/build-posters/segments/final.txt +6 -0
  291. package/docs/video/generate-video-ai.mjs +334 -0
  292. package/docs/video/generate-video-posters.mjs +204 -0
  293. package/docs/video/generate-video.mjs +426 -0
  294. package/docs/video/outline.zh-CN.md +102 -0
  295. package/docs/video/trellis-demo-video-ai.mp4 +0 -0
  296. package/docs/video/trellis-demo-video.mp4 +0 -0
  297. package/docs/video/voiceover.zh-CN.md +49 -0
  298. package/package.json +6 -2
  299. package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
  300. package/schema/capability-selection.example.yaml +23 -0
  301. package/schema/scope.example.yaml +1 -1
  302. package/schema/servers.example.yaml +15 -1
@@ -17,21 +17,32 @@
17
17
  * ever returns create/repair + refuse.
18
18
  */
19
19
  import { isInScope } from "../core/adapter.js";
20
+ import { capabilityDeliveryForAgent } from "../core/types.js";
20
21
  import { codexBearerTokenEnvVar } from "../lib/tomlSection.js";
21
22
  import { resolveSecretEnv } from "../lib/secretEnv.js";
22
23
  export const HUB_ENTRY_NAME = "trellis-hub";
23
- export const GATEWAY_ENTRY_NAME = "trellis-gateway";
24
+ export const GATEWAY_ENTRY_NAME = "trellis";
25
+ export const LEGACY_GATEWAY_ENTRY_NAME = "trellis-gateway";
26
+ export const RUNTIME_ENTRY_NAME = "trellis";
27
+ export const LEGACY_RUNTIME_ENTRY_NAME = "trellis-runtime";
24
28
  /** The command an agent spawns in gateway mode. Bare `trellis` rather than
25
29
  * an absolute path: the entry has to keep working across reinstalls and
26
30
  * version bumps, and every agent resolves it through the same PATH the
27
31
  * user installed the CLI onto. */
28
32
  export const GATEWAY_COMMAND = "trellis";
33
+ export const RUNTIME_COMMAND = "trellis";
34
+ export function isOAuthMcpServer(def) {
35
+ return def.auth === "oauth";
36
+ }
29
37
  /**
30
38
  * Gateway mode applies to every managed agent when `agents` is omitted —
31
39
  * the expected normal case (design.md D14). An explicit list narrows it,
32
40
  * letting some agents stay in direct or hub mode.
33
41
  */
34
42
  export function isGatewayAgent(agentId, mcp, managedAgents) {
43
+ const route = mcp.routes?.[agentId];
44
+ if (route)
45
+ return route.mode === "gateway";
35
46
  const gateway = mcp.gateway;
36
47
  if (!gateway?.enabled)
37
48
  return false;
@@ -39,6 +50,16 @@ export function isGatewayAgent(agentId, mcp, managedAgents) {
39
50
  return gateway.agents.includes(agentId);
40
51
  return managedAgents.includes(agentId);
41
52
  }
53
+ function routeForAgent(agentId, mcp, managedAgents) {
54
+ const explicit = mcp.routes?.[agentId];
55
+ if (explicit)
56
+ return explicit;
57
+ if (isGatewayAgent(agentId, mcp, managedAgents))
58
+ return { mode: "gateway" };
59
+ if (mcp.hub)
60
+ return { mode: "hub" };
61
+ return { mode: "direct" };
62
+ }
42
63
  const CODEX_STDIO_URL_CRASH_NOTE = ' On Codex specifically, this crashes the entire process at startup ("url is not supported for stdio"), not just this one server — see docs/research.md.';
43
64
  function collisionMessage(name, agentId) {
44
65
  return `refusing to write MCP server "${name}": also appears in known_host_injected.${agentId === "codex" ? CODEX_STDIO_URL_CRASH_NOTE : ""}`;
@@ -116,45 +137,122 @@ function findUnresolvedEnvName(def, policy) {
116
137
  return names.find((name) => !resolved[name]);
117
138
  }
118
139
  export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
140
+ const route = routeForAgent(agentId, mcp, managedAgents);
141
+ const runtimeEnabled = capabilityDeliveryForAgent(agentId, mcp) !== "native";
119
142
  // Checked before `hub`: only one entry can be "the" MCP entry for an
120
143
  // agent, and gateway is the newer, purpose-built path (design.md D6).
121
144
  // `hub` stays available, unchanged, for anyone pointing an agent at an
122
145
  // externally-operated endpoint — including a different agent in the
123
146
  // same canonical source.
124
- if (isGatewayAgent(agentId, mcp, managedAgents)) {
125
- if (mcp.knownHostInjected.includes(GATEWAY_ENTRY_NAME)) {
147
+ if (route.mode === "gateway") {
148
+ const entryName = runtimeEnabled ? RUNTIME_ENTRY_NAME : GATEWAY_ENTRY_NAME;
149
+ if (mcp.knownHostInjected.includes(entryName)) {
126
150
  return {
127
151
  desired: [],
128
- conflicts: [{ name: GATEWAY_ENTRY_NAME, message: collisionMessage(GATEWAY_ENTRY_NAME, agentId), remediation: collisionRemediation(GATEWAY_ENTRY_NAME) }],
152
+ conflicts: [{ name: entryName, message: collisionMessage(entryName, agentId), remediation: collisionRemediation(entryName) }],
129
153
  };
130
154
  }
155
+ const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isOAuthMcpServer);
156
+ // Every ordinary per-server check (enabled, scope, literal secrets,
157
+ // unresolved env names, Codex's header shape) still runs at gateway
158
+ // startup. OAuth servers are the deliberate exception: they are written
159
+ // as direct entries so the gateway never owns their client connection.
131
160
  // Every per-server check (enabled, scope, literal secrets, unresolved
132
161
  // env names, Codex's header shape) still runs — but at gateway
133
162
  // *startup*, inside the subcommand, since no adapter sees an
134
163
  // individual server in this mode (design.md D6).
135
164
  return {
136
- desired: [{ name: GATEWAY_ENTRY_NAME, def: { transport: "stdio", command: GATEWAY_COMMAND, args: ["mcp-gateway", "--agent", agentId] } }],
137
- conflicts: [],
165
+ desired: [{
166
+ name: entryName,
167
+ def: {
168
+ transport: "stdio",
169
+ command: runtimeEnabled ? RUNTIME_COMMAND : GATEWAY_COMMAND,
170
+ args: [runtimeEnabled ? "mcp-runtime" : "mcp-gateway", "--agent", agentId],
171
+ },
172
+ }, ...oauth.desired],
173
+ conflicts: oauth.conflicts,
138
174
  };
139
175
  }
140
- if (mcp.hub) {
176
+ if (route.mode === "hub") {
177
+ if (!mcp.hub) {
178
+ return {
179
+ desired: [],
180
+ conflicts: [{
181
+ name: HUB_ENTRY_NAME,
182
+ message: `cannot resolve hub route "${agentId}": canonical mcp.hub.url is not configured`,
183
+ remediation: `configure mcp.hub.url or choose direct/gateway mode for ${agentId}`,
184
+ }],
185
+ };
186
+ }
141
187
  if (mcp.knownHostInjected.includes(HUB_ENTRY_NAME)) {
142
188
  return {
143
189
  desired: [],
144
190
  conflicts: [{ name: HUB_ENTRY_NAME, message: collisionMessage(HUB_ENTRY_NAME, agentId), remediation: collisionRemediation(HUB_ENTRY_NAME) }],
145
191
  };
146
192
  }
147
- return { desired: [{ name: HUB_ENTRY_NAME, def: { transport: "http", url: mcp.hub.url } }], conflicts: [] };
193
+ if (route.servers) {
194
+ return {
195
+ desired: [],
196
+ conflicts: [{
197
+ name: HUB_ENTRY_NAME,
198
+ message: `cannot restrict external hub route "${agentId}" to individual MCP servers: the hub owns its upstream set`,
199
+ remediation: `remove the servers list from the ${agentId} hub route, or use direct/gateway mode for per-server selection`,
200
+ }],
201
+ };
202
+ }
203
+ const desired = [{ name: HUB_ENTRY_NAME, def: { transport: "http", url: mcp.hub.url } }];
204
+ if (runtimeEnabled) {
205
+ if (mcp.knownHostInjected.includes(RUNTIME_ENTRY_NAME)) {
206
+ return {
207
+ desired: [],
208
+ conflicts: [{ name: RUNTIME_ENTRY_NAME, message: collisionMessage(RUNTIME_ENTRY_NAME, agentId), remediation: collisionRemediation(RUNTIME_ENTRY_NAME) }],
209
+ };
210
+ }
211
+ // The runtime provides Trellis-owned capabilities here. The external
212
+ // hub remains the upstream MCP edge; the runtime does not connect to
213
+ // canonical upstreams a second time for this route.
214
+ desired.push({
215
+ name: RUNTIME_ENTRY_NAME,
216
+ def: { transport: "stdio", command: RUNTIME_COMMAND, args: ["mcp-runtime", "--agent", agentId] },
217
+ });
218
+ }
219
+ return { desired, conflicts: [] };
220
+ }
221
+ if (runtimeEnabled) {
222
+ if (mcp.knownHostInjected.includes(RUNTIME_ENTRY_NAME)) {
223
+ return {
224
+ desired: [],
225
+ conflicts: [{ name: RUNTIME_ENTRY_NAME, message: collisionMessage(RUNTIME_ENTRY_NAME, agentId), remediation: collisionRemediation(RUNTIME_ENTRY_NAME) }],
226
+ };
227
+ }
228
+ // Runtime mode owns the agent-facing MCP edge. It resolves the same
229
+ // direct/gateway-compatible upstream route at startup, so native
230
+ // per-server entries are not duplicated here.
231
+ return {
232
+ desired: [{
233
+ name: RUNTIME_ENTRY_NAME,
234
+ def: { transport: "stdio", command: RUNTIME_COMMAND, args: ["mcp-runtime", "--agent", agentId] },
235
+ }],
236
+ conflicts: [],
237
+ };
148
238
  }
239
+ return planDirectServers(agentId, mcp, managedAgents, policy, route);
240
+ }
241
+ function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
149
242
  const desired = [];
150
243
  const conflicts = [];
151
244
  for (const [name, def] of Object.entries(mcp.servers)) {
152
245
  if (def.enabled === false) {
153
246
  continue;
154
247
  }
248
+ if (filter && !filter(def))
249
+ continue;
155
250
  if (!isInScope(agentId, def.agents, managedAgents)) {
156
251
  continue;
157
252
  }
253
+ if (route.servers && !route.servers.includes(name)) {
254
+ continue;
255
+ }
158
256
  if (mcp.knownHostInjected.includes(name)) {
159
257
  conflicts.push({ name, message: collisionMessage(name, agentId), remediation: collisionRemediation(name) });
160
258
  continue;
@@ -9,13 +9,31 @@
9
9
  * mechanism from every other agent's config-generation adapter, but the
10
10
  * same symlink create/repair/remove machinery as skills/instructions.
11
11
  */
12
- import { dirname, join } from "node:path";
12
+ import { dirname, join, sep } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { homedir } from "node:os";
15
+ import { readFileSync } from "node:fs";
15
16
  import { isInScope } from "../core/adapter.js";
17
+ import { usesNativeCapabilityDelivery } from "../core/types.js";
16
18
  import * as piProbe from "../probes/pi.js";
17
19
  import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
18
20
  const BRIDGE_SYMLINK_NAME = "trellis-mcp-bridge.js";
21
+ /**
22
+ * A source-checkout bridge can survive a switch to the packaged CLI. Adopt it
23
+ * only when it has the Trellis bridge artifact shape and is byte-identical
24
+ * to the bundle this installation would write. This is intentionally not a
25
+ * general symlink ownership rule.
26
+ */
27
+ function isEquivalentTrellisBridge(existingTarget, desiredTarget) {
28
+ if (!existingTarget.endsWith(`${sep}${join("dist", "pi-bridge", "bundle.js")}`))
29
+ return false;
30
+ try {
31
+ return readFileSync(existingTarget).equals(readFileSync(desiredTarget));
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
19
37
  /**
20
38
  * The bundled bridge file's location — always `<repo-root>/dist/pi-bridge/
21
39
  * bundle.js`, never `~/.trellis/` (it's Trellis's own packaged code, not a
@@ -56,7 +74,7 @@ export class PiAdapter {
56
74
  const canonicalRoot = dirname(canonical.instructionsFile);
57
75
  const agentDir = join(this.homeDir, ".pi", "agent");
58
76
  const desiredSkills = canonical.skills
59
- .filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents))
77
+ .filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
60
78
  .map((skill) => ({ name: skill.name, target: skill.dir }));
61
79
  const skillItems = planSymlinks({
62
80
  rootDir: join(agentDir, "skills"),
@@ -79,6 +97,7 @@ export class PiAdapter {
79
97
  // (design.md D2), same role `canonicalRoot` plays for skills.
80
98
  canonicalRoot: dirname(bridgeFile),
81
99
  kind: "extension",
100
+ adoptExistingSymlink: isEquivalentTrellisBridge,
82
101
  });
83
102
  return [...skillItems, ...instructionsItems, ...extensionItems];
84
103
  }
@@ -91,7 +110,7 @@ export class PiAdapter {
91
110
  return { ok: false, mismatches: ["pi is not present on this machine"] };
92
111
  }
93
112
  const mismatches = [];
94
- const desiredNames = new Set(canonical.skills.filter((s) => isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
113
+ const desiredNames = new Set(canonical.skills.filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
95
114
  const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
96
115
  for (const name of desiredNames) {
97
116
  if (!actualSkills.has(name)) {
@@ -12,6 +12,12 @@ export interface DesiredSymlink {
12
12
  /** Absolute path the symlink should point at (inside `canonicalRoot`). */
13
13
  target: string;
14
14
  }
15
+ /**
16
+ * A narrowly-scoped escape hatch for artifacts that Trellis can identify
17
+ * across installation roots. The default ownership rule remains path-based;
18
+ * callers must opt in explicitly for a particular artifact kind.
19
+ */
20
+ export type ExistingSymlinkAdoption = (existingTarget: string, desiredTarget: string) => boolean;
15
21
  /**
16
22
  * `rootDir` is the directory entries are named directly under (e.g.
17
23
  * `~/.claude/skills`, or the instructions file's own parent directory when
@@ -28,6 +34,7 @@ export declare function planSymlinks(opts: {
28
34
  desired: DesiredSymlink[];
29
35
  canonicalRoot: string;
30
36
  kind: "skill" | "instructions" | "extension";
37
+ adoptExistingSymlink?: ExistingSymlinkAdoption;
31
38
  }): AdapterPlanItem[];
32
39
  /** Executes a `planSymlinks` result through the run's backup session
33
40
  * (trellis-backup-rollback) — every create/repair/remove is recorded
@@ -4,9 +4,9 @@
4
4
  * trellis-sync-p1/specs/skill-instructions-sync/spec.md) — built once,
5
5
  * reused by every adapter rather than reimplemented per agent.
6
6
  */
7
- import { existsSync, lstatSync, readdirSync, readlinkSync } from "node:fs";
7
+ import { lstatSync, readdirSync, readlinkSync } from "node:fs";
8
8
  import { mkdir } from "node:fs/promises";
9
- import { join, resolve, sep } from "node:path";
9
+ import { dirname, join, resolve, sep } from "node:path";
10
10
  import { isSymlinkTo } from "../lib/fsIdentity.js";
11
11
  import { currentLinkTarget } from "../lib/backup.js";
12
12
  function isUnderRoot(path, root) {
@@ -29,7 +29,7 @@ function isUnderRoot(path, root) {
29
29
  * case this removal path exists to catch.
30
30
  */
31
31
  export function planSymlinks(opts) {
32
- const { rootDir, desired, kind } = opts;
32
+ const { rootDir, desired, kind, adoptExistingSymlink } = opts;
33
33
  const items = [];
34
34
  const desiredNames = new Set(desired.map((d) => d.name));
35
35
  const canonicalRootResolved = resolve(opts.canonicalRoot);
@@ -38,8 +38,15 @@ export function planSymlinks(opts) {
38
38
  if (isSymlinkTo(path, target)) {
39
39
  continue; // already correct — no-op
40
40
  }
41
- if (existsSync(path)) {
42
- const isSymlink = lstatSync(path).isSymbolicLink();
41
+ // lstat sees dangling links too; existsSync would treat them as absent
42
+ // and bypass the foreign-link protection below.
43
+ const existing = lstatSync(path, { throwIfNoEntry: false });
44
+ if (existing) {
45
+ const isSymlink = existing.isSymbolicLink();
46
+ const rawExistingTarget = isSymlink ? readlinkSync(path) : undefined;
47
+ const resolvedExistingTarget = rawExistingTarget === undefined
48
+ ? undefined
49
+ : resolve(dirname(path), rawExistingTarget);
43
50
  // A symlink whose stored target resolves outside canonicalRoot is
44
51
  // owned by something else (e.g. a user's own dotfile-management
45
52
  // setup) — repairing it as if it were a stale Trellis entry would
@@ -47,14 +54,16 @@ export function planSymlinks(opts) {
47
54
  // symlink Trellis itself left pointing at a since-removed canonical
48
55
  // entry is broken by construction and must still be treated as
49
56
  // ours to repair, not thrown out to a conflict by a realpath error.
50
- const isForeign = isSymlink && !isUnderRoot(readlinkSync(path), canonicalRootResolved);
57
+ const isForeign = resolvedExistingTarget !== undefined
58
+ && !isUnderRoot(resolvedExistingTarget, canonicalRootResolved)
59
+ && adoptExistingSymlink?.(resolvedExistingTarget, target) !== true;
51
60
  if (!isSymlink || isForeign) {
52
61
  items.push({
53
62
  action: "conflict",
54
63
  kind,
55
64
  target: path,
56
65
  description: isForeign
57
- ? `${path} exists as a symlink to ${readlinkSync(path)}, not owned by Trellis — left untouched`
66
+ ? `${path} exists as a symlink to ${rawExistingTarget}, not owned by Trellis — left untouched`
58
67
  : `${path} exists and is not a Trellis-managed symlink — left untouched`,
59
68
  remediation: isForeign
60
69
  ? `remove the existing symlink at ${path} if you want Trellis to manage it, then re-run sync — otherwise leave it, Trellis will not touch it`
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Runtime-first ZCode adapter.
3
+ *
4
+ * ZCode's native `~/.agents/skills` scan prevents scope-safe native delivery,
5
+ * so Runtime-only delivery uses one MCP Runtime edge and turns off the two
6
+ * native Skill switches through a ledger-backed, reversible config change.
7
+ */
8
+ import type { AdapterPlanItem, AdapterProbeResult, AdapterVerifyResult, TrellisAdapter } from "../core/adapter.js";
9
+ import type { CanonicalSource } from "../core/types.js";
10
+ import type { BackupSession } from "../lib/backup.js";
11
+ export declare class ZcodeAdapter implements TrellisAdapter {
12
+ private readonly homeDir;
13
+ private readonly env;
14
+ readonly name = "ZCode";
15
+ readonly id: "zcode";
16
+ constructor(homeDir?: string, env?: NodeJS.ProcessEnv);
17
+ probe(): Promise<AdapterProbeResult>;
18
+ plan(canonical: CanonicalSource): Promise<AdapterPlanItem[]>;
19
+ apply(plan: AdapterPlanItem[], backup: BackupSession): Promise<void>;
20
+ verify(canonical: CanonicalSource): Promise<AdapterVerifyResult>;
21
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Runtime-first ZCode adapter.
3
+ *
4
+ * ZCode's native `~/.agents/skills` scan prevents scope-safe native delivery,
5
+ * so Runtime-only delivery uses one MCP Runtime edge and turns off the two
6
+ * native Skill switches through a ledger-backed, reversible config change.
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ import { isInScope } from "../core/adapter.js";
12
+ import { capabilityDeliveryForAgent, usesNativeCapabilityDelivery } from "../core/types.js";
13
+ import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
14
+ import { renderJsonServerEntry } from "./jsonMcp.js";
15
+ import { LEGACY_GATEWAY_ENTRY_NAME, LEGACY_RUNTIME_ENTRY_NAME, resolveMcpPlan } from "./mcpPlan.js";
16
+ import { deepEqual } from "../lib/deepEqual.js";
17
+ import { forgetOwned, loadMcpOwnership, ownedByAgent, recordOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
18
+ import { forgetZcodeSettingsOwnership, loadZcodeSettingsOwnership, saveZcodeSettingsOwnership } from "../lib/zcodeOwnership.js";
19
+ import * as zcodeProbe from "../probes/zcode.js";
20
+ function readConfig(path) {
21
+ if (!existsSync(path))
22
+ return undefined;
23
+ return JSON.parse(readFileSync(path, "utf8"));
24
+ }
25
+ function record(value) {
26
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
27
+ }
28
+ function zcodeServers(parsed) {
29
+ return record(record(parsed?.mcp).servers);
30
+ }
31
+ function planZcodeMcp(opts) {
32
+ const { desired, conflicts } = resolveMcpPlan("zcode", opts.mcp, opts.managedAgents, opts.policy);
33
+ const existing = zcodeServers(opts.parsed);
34
+ const items = [];
35
+ for (const { name, def } of desired) {
36
+ const rendered = renderJsonServerEntry(def);
37
+ if (deepEqual(existing[name], rendered))
38
+ continue;
39
+ items.push({
40
+ action: "create",
41
+ kind: "mcp",
42
+ target: opts.configPath,
43
+ mcpWrite: { name, def },
44
+ description: `MCP server "${name}" ${existing[name] === undefined ? "created" : "updated"} in ${opts.configPath}`,
45
+ });
46
+ }
47
+ for (const conflict of conflicts) {
48
+ items.push({ action: "conflict", kind: "mcp", target: opts.configPath, description: conflict.message, ...(conflict.remediation ? { remediation: conflict.remediation } : {}) });
49
+ }
50
+ const desiredNames = new Set(desired.map((entry) => entry.name));
51
+ for (const [name, expected] of Object.entries(opts.ownership)) {
52
+ if (desiredNames.has(name))
53
+ continue;
54
+ if ((name === LEGACY_GATEWAY_ENTRY_NAME || name === LEGACY_RUNTIME_ENTRY_NAME) && desiredNames.has("trellis"))
55
+ continue;
56
+ if (existing[name] === undefined || !deepEqual(existing[name], expected))
57
+ continue;
58
+ items.push({
59
+ action: "remove",
60
+ kind: "mcp",
61
+ target: opts.configPath,
62
+ mcpRemove: { name },
63
+ description: `MCP server "${name}" removed from ${opts.configPath} — no longer in canonical`,
64
+ });
65
+ }
66
+ return items;
67
+ }
68
+ function applyZcodeMcp(parsed, items) {
69
+ const base = parsed ?? {};
70
+ const mcp = record(base.mcp);
71
+ const servers = { ...record(mcp.servers) };
72
+ for (const item of items) {
73
+ if (item.kind !== "mcp")
74
+ continue;
75
+ if (item.action === "create" && item.mcpWrite)
76
+ servers[item.mcpWrite.name] = renderJsonServerEntry(item.mcpWrite.def);
77
+ if (item.action === "remove" && item.mcpRemove)
78
+ delete servers[item.mcpRemove.name];
79
+ }
80
+ return { ...base, mcp: { ...mcp, servers } };
81
+ }
82
+ function currentSkillControls(parsed) {
83
+ const features = record(parsed?.features);
84
+ const skills = record(parsed?.skills);
85
+ return {
86
+ featuresSkill: typeof features.skill === "boolean" ? features.skill : null,
87
+ skillsEnabled: typeof skills.enabled === "boolean" ? skills.enabled : null,
88
+ };
89
+ }
90
+ function applySkillControls(parsed, controls) {
91
+ const features = { ...record(parsed.features) };
92
+ const skills = { ...record(parsed.skills) };
93
+ if (controls.featuresSkill === null)
94
+ delete features.skill;
95
+ else
96
+ features.skill = controls.featuresSkill;
97
+ if (controls.skillsEnabled === null)
98
+ delete skills.enabled;
99
+ else
100
+ skills.enabled = controls.skillsEnabled;
101
+ return { ...parsed, features, skills };
102
+ }
103
+ export class ZcodeAdapter {
104
+ homeDir;
105
+ env;
106
+ name = "ZCode";
107
+ id = "zcode";
108
+ constructor(homeDir = homedir(), env = process.env) {
109
+ this.homeDir = homeDir;
110
+ this.env = env;
111
+ }
112
+ async probe() {
113
+ const snapshot = await zcodeProbe.probe(this.homeDir, { env: this.env });
114
+ return { present: snapshot.present, version: snapshot.version, ...(snapshot.diagnostics.length ? { detail: snapshot.diagnostics.join("; ") } : {}) };
115
+ }
116
+ async plan(canonical) {
117
+ const profile = zcodeProbe.resolveZcodeProfile(this.homeDir, this.env);
118
+ if (!profile)
119
+ return [];
120
+ const parsed = readConfig(profile.configPath);
121
+ const canonicalRoot = dirname(canonical.instructionsFile);
122
+ const runtimeOnly = capabilityDeliveryForAgent(this.id, canonical.mcp) === "mcp";
123
+ const items = [];
124
+ items.push(...planSymlinks({
125
+ rootDir: join(this.homeDir, ".zcode", "skills"),
126
+ desired: usesNativeCapabilityDelivery(this.id, canonical.mcp)
127
+ ? canonical.skills.filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents)).map((skill) => ({ name: skill.name, target: skill.dir }))
128
+ : [],
129
+ canonicalRoot: join(canonicalRoot, "skills"),
130
+ kind: "skill",
131
+ }));
132
+ items.push(...planSymlinks({
133
+ rootDir: join(this.homeDir, ".zcode"),
134
+ desired: [{ name: "AGENTS.md", target: canonical.instructionsFile }],
135
+ canonicalRoot,
136
+ kind: "instructions",
137
+ }));
138
+ items.push(...planZcodeMcp({
139
+ configPath: profile.configPath,
140
+ parsed,
141
+ mcp: canonical.mcp,
142
+ managedAgents: canonical.managedAgents,
143
+ policy: canonical.secretsPolicy,
144
+ ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
145
+ }));
146
+ const current = currentSkillControls(parsed);
147
+ const ownership = loadZcodeSettingsOwnership(this.homeDir);
148
+ if (runtimeOnly && (current.featuresSkill !== false || current.skillsEnabled !== false)) {
149
+ items.push({
150
+ action: "create",
151
+ kind: "zcode-skill-control",
152
+ target: profile.configPath,
153
+ zcodeSkillControls: { featuresSkill: false, skillsEnabled: false, previous: current },
154
+ description: `native ZCode Skill discovery disabled in ${profile.configPath} for Runtime-only delivery`,
155
+ });
156
+ }
157
+ else if (!runtimeOnly && ownership?.configPath === profile.configPath && current.featuresSkill === false && current.skillsEnabled === false) {
158
+ items.push({
159
+ action: "remove",
160
+ kind: "zcode-skill-control",
161
+ target: profile.configPath,
162
+ zcodeSkillControls: { ...ownership.previous },
163
+ description: `previous native ZCode Skill settings restored in ${profile.configPath}`,
164
+ });
165
+ }
166
+ return items;
167
+ }
168
+ async apply(plan, backup) {
169
+ await applySymlinkPlan(plan.filter((item) => item.kind === "skill" || item.kind === "instructions"), backup);
170
+ const configItems = plan.filter((item) => (item.kind === "mcp" || item.kind === "zcode-skill-control") && (item.action === "create" || item.action === "remove"));
171
+ if (configItems.length === 0)
172
+ return;
173
+ const configPath = configItems[0].target;
174
+ let merged = applyZcodeMcp(readConfig(configPath), configItems);
175
+ const control = configItems.find((item) => item.kind === "zcode-skill-control")?.zcodeSkillControls;
176
+ if (control)
177
+ merged = applySkillControls(merged, control);
178
+ backup.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`);
179
+ let ownership = loadMcpOwnership(this.homeDir);
180
+ for (const item of configItems) {
181
+ if (item.kind !== "mcp")
182
+ continue;
183
+ if (item.action === "create" && item.mcpWrite)
184
+ ownership = recordOwned(ownership, this.id, item.mcpWrite.name, renderJsonServerEntry(item.mcpWrite.def));
185
+ if (item.action === "remove" && item.mcpRemove)
186
+ ownership = forgetOwned(ownership, this.id, item.mcpRemove.name);
187
+ }
188
+ saveMcpOwnership(this.homeDir, ownership);
189
+ const controlItem = configItems.find((item) => item.kind === "zcode-skill-control");
190
+ if (controlItem?.action === "create" && controlItem.zcodeSkillControls?.previous) {
191
+ saveZcodeSettingsOwnership(this.homeDir, {
192
+ configPath,
193
+ managed: { featuresSkill: false, skillsEnabled: false },
194
+ previous: controlItem.zcodeSkillControls.previous,
195
+ });
196
+ }
197
+ else if (controlItem?.action === "remove") {
198
+ forgetZcodeSettingsOwnership(this.homeDir);
199
+ }
200
+ }
201
+ async verify(canonical) {
202
+ const snapshot = await zcodeProbe.probe(this.homeDir, { env: this.env });
203
+ if (!snapshot.present)
204
+ return { ok: false, mismatches: ["zcode is not present on this machine"] };
205
+ const profile = zcodeProbe.resolveZcodeProfile(this.homeDir, this.env);
206
+ if (!profile)
207
+ return { ok: false, mismatches: ["zcode profile cannot be selected"] };
208
+ const mismatches = [];
209
+ const desiredNativeSkills = canonical.skills
210
+ .filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
211
+ .map((skill) => skill.name);
212
+ const actualNativeSkills = new Set(snapshot.skillRoots.filter((root) => root.path === join(this.homeDir, ".zcode", "skills")).flatMap((root) => root.skills.map((skill) => skill.name)));
213
+ for (const name of desiredNativeSkills)
214
+ if (!actualNativeSkills.has(name))
215
+ mismatches.push(`skill "${name}" is missing on ZCode`);
216
+ const expectedMcp = planZcodeMcp({
217
+ configPath: profile.configPath,
218
+ parsed: readConfig(profile.configPath),
219
+ mcp: canonical.mcp,
220
+ managedAgents: canonical.managedAgents,
221
+ policy: canonical.secretsPolicy,
222
+ ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
223
+ }).filter((item) => item.action === "create" || item.action === "conflict");
224
+ mismatches.push(...expectedMcp.map((item) => item.description));
225
+ if (!snapshot.instructionsFile?.isSymlink || snapshot.instructionsFile.target !== canonical.instructionsFile)
226
+ mismatches.push("shared instructions are not linked into ~/.zcode/AGENTS.md");
227
+ if (capabilityDeliveryForAgent(this.id, canonical.mcp) === "mcp") {
228
+ const controls = currentSkillControls(readConfig(profile.configPath));
229
+ if (controls.featuresSkill !== false || controls.skillsEnabled !== false)
230
+ mismatches.push("native ZCode Skill discovery is still enabled during Runtime-only delivery");
231
+ }
232
+ return mismatches.length === 0 ? { ok: true } : { ok: false, mismatches };
233
+ }
234
+ }