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
@@ -13,24 +13,33 @@
13
13
  * judgment is made here.
14
14
  */
15
15
  import { createInterface } from "node:readline/promises";
16
+ import { execFileSync } from "node:child_process";
16
17
  import { homedir } from "node:os";
17
- import { writeFileSync } from "node:fs";
18
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
18
19
  import { join } from "node:path";
19
- import { canUseInteractivePicker, runMultiSelectPicker, runSingleSelectPicker } from "../lib/terminalPicker.js";
20
+ import { canUseInteractivePicker, runMultiSelectPicker, runSearchMultiSelectPicker, runSingleSelectPicker } from "../lib/terminalPicker.js";
20
21
  import * as claudeCodeProbe from "../probes/claude-code.js";
21
22
  import * as codexProbe from "../probes/codex.js";
22
23
  import * as kiroProbe from "../probes/kiro.js";
23
24
  import * as piProbe from "../probes/pi.js";
24
- import { ALL_AGENTS } from "../core/types.js";
25
- import { loadCanonicalSource, removeServerYaml, upsertServerYaml, writeMcpModeYaml } from "../core/canonical.js";
25
+ import * as kimiCodeProbe from "../probes/kimi-code.js";
26
+ import * as zcodeProbe from "../probes/zcode.js";
27
+ import { ALL_AGENTS, capabilityDeliveryForAgent } from "../core/types.js";
28
+ import { loadCanonicalSource, removeServerYaml, upsertServerYaml, writeMcpModeYaml, writeMcpRoutesYaml, writeMcpRuntimeDeliveryYaml } from "../core/canonical.js";
29
+ import { buildCapabilityInventory, parseCapabilitySelectionFile, resolveSelectedNames, selectionContains } from "../core/capabilitySelection.js";
26
30
  import { INSTALL_HINTS, collectInitReport } from "./init.js";
27
31
  import { applyMigratePlan, collectMigratePlan, printPlan as printMigratePlan } from "./migrate.js";
32
+ import { isGatewayAgent } from "../adapters/mcpPlan.js";
33
+ import { isInScope } from "../core/adapter.js";
34
+ import { TRELLIS_RUNTIME_SKILL_NAME } from "../lib/builtinSkills.js";
35
+ import { discoverNativeMemory } from "../lib/nativeMemory.js";
28
36
  import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
29
37
  import { collectMcpSyncReport, printReport as printMcpSyncReport } from "./mcp.js";
30
38
  import { collectSecretsAuditReport, printReport as printSecretsAuditReport } from "./secretsAudit.js";
31
39
  import { applyMemorySync, collectMemorySyncResult, printMemorySyncResult, DEFAULT_MEMORY_SERVER_DEF, MEMORY_SERVER_NAME } from "./memory.js";
32
40
  import { collectDoctorReport, printReport as printDoctorReport, resolveKnownHostInjected } from "./doctor.js";
33
41
  import { openBackupSession } from "../lib/backup.js";
42
+ import { applyRollbackPlan, collectRollbackPlan, loadManifest } from "./rollback.js";
34
43
  import { confirmAndInstall } from "../lib/installAgent.js";
35
44
  import { normalizeDoctorVerdict, normalizeMcpSyncVerdict, normalizeMemorySyncVerdict, normalizeMigrateVerdict, normalizeSecretsAuditVerdict, normalizeSelfVerificationVerdict, normalizeSyncVerdict, printVerdict, } from "./onboardVerdict.js";
36
45
  const PROBES = {
@@ -38,21 +47,79 @@ const PROBES = {
38
47
  codex: (homeDir) => codexProbe.probe(homeDir),
39
48
  kiro: (homeDir) => kiroProbe.probe(homeDir),
40
49
  pi: (homeDir) => piProbe.probe(homeDir),
50
+ "kimi-code": (homeDir) => kimiCodeProbe.probe(homeDir),
51
+ zcode: (homeDir) => zcodeProbe.probe(homeDir),
41
52
  };
42
- export async function collectOnboardSummary(homeDir = homedir()) {
53
+ /**
54
+ * Scores only real source-side content. The recommendation is a hint for a
55
+ * first-time user, never a filter: every candidate remains selectable.
56
+ */
57
+ export function migrationSourceScore(summary) {
58
+ return summary.skillCount + summary.mcpServerCount + (summary.hasRealInstructions ? 1 : 0);
59
+ }
60
+ export function recommendedMigrationSource(summaries) {
61
+ return summaries.reduce((best, current) => {
62
+ if (!best || migrationSourceScore(current) > migrationSourceScore(best))
63
+ return current;
64
+ return best;
65
+ }, undefined)?.agent;
66
+ }
67
+ export const MCP_ROUTE_OPTIONS = [
68
+ {
69
+ mode: "gateway",
70
+ label: "gateway(推荐:本机统一托管) — Trellis 在本机会话中运行 MCP 入口,不需要额外部署服务",
71
+ },
72
+ {
73
+ mode: "direct",
74
+ label: "direct(简单直连) — Agent 分别连接每个 MCP,适合不需要统一托管的情况",
75
+ },
76
+ {
77
+ mode: "hub",
78
+ label: "hub(已有外部 Hub 时使用) — Agent 连接一个外部 HTTP MCP 服务",
79
+ },
80
+ ];
81
+ export function interactiveMcpRouteOptions(hubConfigured) {
82
+ return MCP_ROUTE_OPTIONS.map((option) => option.mode === "hub" && !hubConfigured
83
+ ? { ...option, label: "hub(暂不可选:需要先配置外部 Hub URL) — 不会自动部署 Hub", disabled: true }
84
+ : { ...option });
85
+ }
86
+ export async function collectOnboardSummary(homeDir = homedir(), workspaceDir = process.cwd()) {
43
87
  return Promise.all(ALL_AGENTS.map(async (agent) => {
44
88
  const snapshot = await PROBES[agent](homeDir);
45
89
  if (!snapshot.present) {
46
- return { agent, present: false, skillCount: 0, skillNames: [], hasRealInstructions: false, mcpServerCount: 0 };
90
+ return { agent, present: false, skillCount: 0, skillNames: [], hasRealInstructions: false, mcpServerCount: 0, mcpServerNames: [], nativeMemoryStatus: "unsupported", nativeMemoryCount: 0, nativeMemoryNames: [], nativeMemoryDetail: "agent is not installed" };
47
91
  }
48
92
  const skillNames = snapshot.skillRoots.flatMap((root) => root.skills.map((s) => s.name));
49
93
  const hasRealInstructions = snapshot.instructionsFile !== undefined && !snapshot.instructionsFile.isSymlink;
50
94
  const mcpPlan = await collectMigratePlan(agent, homeDir, ["mcp"]);
51
- return { agent, present: true, skillCount: skillNames.length, skillNames, hasRealInstructions, mcpServerCount: mcpPlan.items.length };
95
+ const nativeMemory = discoverNativeMemory(agent, homeDir, workspaceDir);
96
+ return { agent, present: true, skillCount: skillNames.length, skillNames, hasRealInstructions, mcpServerCount: mcpPlan.items.length, mcpServerNames: mcpPlan.items.map((item) => item.name), nativeMemoryStatus: nativeMemory.status, nativeMemoryCount: nativeMemory.candidates.length, nativeMemoryNames: nativeMemory.candidates.map((candidate) => candidate.displayName), nativeMemoryDetail: nativeMemory.detail };
52
97
  }));
53
98
  }
54
99
  function hasContent(s) {
55
- return s.skillCount > 0 || s.hasRealInstructions || s.mcpServerCount > 0;
100
+ return s.skillCount > 0 || s.hasRealInstructions || s.mcpServerCount > 0 || s.nativeMemoryCount > 0;
101
+ }
102
+ function hasSelectedSourceContent(s, selection) {
103
+ if (!selection)
104
+ return hasContent(s);
105
+ const hasSkills = selection.skills !== "none" && s.skillNames.some((name) => selectionContains(selection.skills, name));
106
+ const hasMcp = selection.mcpServers !== "none" && s.mcpServerNames.some((name) => selectionContains(selection.mcpServers, name));
107
+ const hasMemory = selection.memoryMigration !== "none" && s.nativeMemoryCount > 0;
108
+ return hasSkills || hasMcp || hasMemory || s.hasRealInstructions;
109
+ }
110
+ function filterMigratePlan(plan, selection) {
111
+ if (!selection)
112
+ return plan;
113
+ const items = plan.items.filter((item) => {
114
+ if (item.kind === "skill")
115
+ return selectionContains(selection.skills, item.name);
116
+ if (item.kind === "mcp")
117
+ return selectionContains(selection.mcpServers, item.name);
118
+ if (item.kind === "memory")
119
+ return selection.memoryMigration !== "none";
120
+ return true;
121
+ });
122
+ return { ...plan, items };
56
123
  }
57
124
  const PROGRESS_STAGES = ["migrate", "sync", "mcp sync", "memory sync", "secrets audit", "doctor"];
58
125
  /** Transient status, not part of the report — printed to stderr so
@@ -74,18 +141,21 @@ function logProgress(opts, stage) {
74
141
  * (found via a real mirasim terminal session). The full names are still
75
142
  * available from `trellis doctor`/`--json`; a picker row's job is to
76
143
  * let you tell agents apart at a glance, not enumerate everything. */
77
- function agentSummaryLabel(s) {
78
- return `${s.agent} — ${s.skillCount} skill(s), instructions: ${s.hasRealInstructions ? "yes" : "no"}, mcp: ${s.mcpServerCount}`;
144
+ function agentSummaryLabel(s, recommendedAgent) {
145
+ const recommendation = s.agent === recommendedAgent ? "(推荐:可迁移内容最多)" : "";
146
+ const memory = s.nativeMemoryCount > 0 ? `Memory: ${s.nativeMemoryCount}` : `Memory: ${s.nativeMemoryStatus}`;
147
+ return `${s.agent} — Skills: ${s.skillCount}, global instructions: ${s.hasRealInstructions ? "yes" : "no"}, MCP: ${s.mcpServerCount}, ${memory} ${recommendation}`;
79
148
  }
80
149
  /** Numbered-typing fallback (trellis-onboard-interactive-picker design.md
81
150
  * D2) — used only when the terminal can't support the raw-mode picker
82
151
  * (`canUseInteractivePicker()` false). Unchanged from before that change. */
83
152
  async function promptForAgentNumbered(present) {
84
- const rl = createInterface({ input: process.stdin, output: process.stdout });
153
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
154
+ const recommendedAgent = recommendedMigrationSource(present);
85
155
  try {
86
- console.log("Multiple agents detected:");
156
+ console.error("检测到多个 Agent。推荐选择可迁移内容最多的 Agent 作为来源:");
87
157
  present.forEach((s, i) => {
88
- console.log(` ${i + 1}) ${agentSummaryLabel(s)}`);
158
+ console.error(` ${i + 1}) ${agentSummaryLabel(s, recommendedAgent)}`);
89
159
  });
90
160
  for (let attempt = 0; attempt < 2; attempt++) {
91
161
  const answer = (await rl.question(`Choose a migration source [1-${present.length}]: `)).trim();
@@ -97,7 +167,7 @@ async function promptForAgentNumbered(present) {
97
167
  return byIndex.agent;
98
168
  if (present.some((s) => s.agent === answer))
99
169
  return answer;
100
- console.log(`Not a valid choice: enter a number from 1-${present.length}, or one of ${present.map((s) => s.agent).join(", ")}`);
170
+ console.error(`Not a valid choice: enter a number from 1-${present.length}, or one of ${present.map((s) => s.agent).join(", ")}`);
101
171
  }
102
172
  throw new Error("no valid migration source chosen after 2 attempts");
103
173
  }
@@ -116,10 +186,11 @@ async function promptForAgentReal(present) {
116
186
  if (!canUseInteractivePicker()) {
117
187
  return promptForAgentNumbered(present);
118
188
  }
119
- console.log("Multiple agents detected — use Up/Down (or j/k) and Enter to choose a migration source:");
120
- const index = await runSingleSelectPicker(present.map(agentSummaryLabel));
189
+ const recommendedAgent = recommendedMigrationSource(present);
190
+ console.error("选择迁移来源。推荐选择可迁移内容最多的 Agent;这只决定从哪里读取,不会自动托管它:");
191
+ const index = await runSingleSelectPicker(present.map((summary) => agentSummaryLabel(summary, recommendedAgent)), undefined, "迁移来源");
121
192
  if (index === null) {
122
- console.log("cancelled, no changes made");
193
+ console.error("cancelled, no changes made");
123
194
  process.exit(1);
124
195
  }
125
196
  return present[index].agent;
@@ -127,13 +198,14 @@ async function promptForAgentReal(present) {
127
198
  /** Numbered-typing fallback — unchanged from before this change (see
128
199
  * `promptForAgentNumbered`'s doc comment). */
129
200
  async function promptForManagedAgentsNumbered(candidates, alreadyManaged) {
130
- const rl = createInterface({ input: process.stdin, output: process.stdout });
201
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
131
202
  try {
132
- console.log("Which agents should Trellis manage? (comma-separated numbers; enter for none new)");
203
+ console.error("选择 Trellis 要托管的 Agent(推荐选择已安装的 Agent;直接回车表示本次不新增):");
133
204
  candidates.forEach((s, i) => {
134
205
  const status = s.present ? `present, ${s.skillCount} skill(s)` : "not installed";
135
206
  const tag = alreadyManaged.includes(s.agent) ? " [already managed]" : "";
136
- console.log(` ${i + 1}) ${s.agent} — ${status}${tag}`);
207
+ const recommendation = s.present ? "(推荐:已安装)" : "";
208
+ console.error(` ${i + 1}) ${s.agent} — ${status}${recommendation}${tag}`);
137
209
  });
138
210
  const answer = (await rl.question("Select: ")).trim();
139
211
  return answer;
@@ -152,21 +224,23 @@ async function promptForManagedAgentsReal(candidates, alreadyManaged) {
152
224
  if (!canUseInteractivePicker()) {
153
225
  return promptForManagedAgentsNumbered(candidates, alreadyManaged);
154
226
  }
155
- console.log("Which agents should Trellis manage? Up/Down (or j/k) to move, Space to toggle, Enter to confirm:");
227
+ console.error("选择 Trellis 要托管的 Agent。推荐选择已安装的 Agent;选择未安装的 Agent 会先请求安装确认:");
156
228
  const labels = candidates.map((s) => {
157
229
  const status = s.present ? `present, ${s.skillCount} skill(s)` : "not installed";
158
- return `${s.agent} — ${status}`;
230
+ const recommendation = s.present ? "(推荐:已安装)" : "";
231
+ const managed = alreadyManaged.includes(s.agent) ? "(已托管)" : "";
232
+ return `${s.agent} — ${status}${recommendation}${managed}`;
159
233
  });
160
234
  const initiallyChecked = candidates.map((s) => alreadyManaged.includes(s.agent));
161
235
  const indices = await runMultiSelectPicker(labels, initiallyChecked);
162
236
  if (indices === null) {
163
- console.log("cancelled, no changes made");
237
+ console.error("cancelled, no changes made");
164
238
  process.exit(1);
165
239
  }
166
240
  return indices.map((i) => candidates[i].agent).join(",");
167
241
  }
168
242
  /**
169
- * Resolves which categories (skills, instructions) to migrate from a
243
+ * Resolves which categories (Skills, instructions, MCP, native Memory) to migrate from a
170
244
  * resolved source (trellis-migrate-category-selection, extended to a
171
245
  * third category by trellis-onboard-mcp-memory design.md D2). Unlike the
172
246
  * two pickers above, there is no numbered-text fallback to preserve
@@ -178,17 +252,32 @@ async function promptForManagedAgentsReal(candidates, alreadyManaged) {
178
252
  * An empty result is a valid answer: "skip migrate for this run"
179
253
  * (design.md D6), left for the caller to act on.
180
254
  */
181
- async function resolveMigrateCategories(source, opts) {
255
+ async function resolveMigrateCategories(source, opts, selection) {
182
256
  // Built dynamically, in skill/instructions/mcp display order — not a
183
- // fixed two- or three-slot structure, so a fourth category some day
257
+ // fixed category structure, so future categories
184
258
  // would only need an entry here, not a rewritten branch (design.md D2).
185
259
  const candidates = [];
186
- if (source.skillCount > 0)
187
- candidates.push({ kind: "skill", label: "skills" });
260
+ if (source.skillCount > 0 && (!selection || selection.skills !== "none") && (!selection || source.skillNames.some((name) => selectionContains(selection.skills, name)))) {
261
+ candidates.push({ kind: "skill", label: "Skills" });
262
+ }
188
263
  if (source.hasRealInstructions)
189
- candidates.push({ kind: "instructions", label: "instructions" });
190
- if (source.mcpServerCount > 0)
191
- candidates.push({ kind: "mcp", label: "mcp" });
264
+ candidates.push({ kind: "instructions", label: "global instructions" });
265
+ if (source.mcpServerCount > 0 && (!selection || selection.mcpServers !== "none") && (!selection || source.mcpServerNames.some((name) => selectionContains(selection.mcpServers, name)))) {
266
+ candidates.push({ kind: "mcp", label: "MCP servers" });
267
+ }
268
+ let migrateMemory = source.nativeMemoryCount > 0 && opts.memoryMigrate !== "off" && (!selection || selection.memoryMigration !== "none");
269
+ if (migrateMemory && opts.memoryMigrate === undefined && !opts.selectionFile && !opts.json && (opts.promptForMemoryMigration || canUseInteractivePicker())) {
270
+ const choice = opts.promptForMemoryMigration
271
+ ? await opts.promptForMemoryMigration(source.agent, source.nativeMemoryCount)
272
+ : await runSingleSelectPicker([
273
+ `迁移支持的记忆(推荐:${source.nativeMemoryCount} 项)`,
274
+ "暂不迁移记忆",
275
+ ], undefined, `选择是否从 ${source.agent} 迁移已有记忆`);
276
+ if (choice === null || choice === "off" || choice === 1)
277
+ migrateMemory = false;
278
+ }
279
+ if (migrateMemory)
280
+ candidates.push({ kind: "memory", label: `Memory(${source.nativeMemoryCount} 项)` });
192
281
  // The choice is only meaningful when two or more kinds are real — same
193
282
  // gate for the injected test seam as for the real picker, mirroring
194
283
  // how `promptForAgent`/`promptForManagedAgents` are only ever
@@ -198,10 +287,10 @@ async function resolveMigrateCategories(source, opts) {
198
287
  return opts.promptForMigrateCategories(source);
199
288
  }
200
289
  if (canUseInteractivePicker()) {
201
- console.log(`Which categories should be migrated from ${source.agent}? Space to toggle, Enter to confirm:`);
202
- const indices = await runMultiSelectPicker(candidates.map((c) => c.label), candidates.map(() => true));
290
+ console.error(`选择要从 ${source.agent} 迁移的内容(默认全选,推荐先完整迁移,之后再清理不需要的项):`);
291
+ const indices = await runMultiSelectPicker(candidates.map((c) => `${c.label}(推荐迁移)`), candidates.map(() => true), undefined, "迁移内容");
203
292
  if (indices === null) {
204
- console.log("cancelled, no changes made");
293
+ console.error("cancelled, no changes made");
205
294
  process.exit(1);
206
295
  }
207
296
  return indices.map((i) => candidates[i].kind);
@@ -209,6 +298,75 @@ async function resolveMigrateCategories(source, opts) {
209
298
  }
210
299
  return candidates.map((c) => c.kind);
211
300
  }
301
+ async function resolveInteractiveCapabilitySelection(source, opts) {
302
+ if (opts.json || opts.promptForMigrateCategories || !canUseInteractivePicker())
303
+ return undefined;
304
+ const skills = source.skillNames.length > 0
305
+ ? await runSearchMultiSelectPicker(source.skillNames, undefined, "选择要迁移的 Skills(默认全选,推荐先全部保留)")
306
+ : [];
307
+ if (skills === null) {
308
+ console.error("cancelled, no changes made");
309
+ process.exit(1);
310
+ }
311
+ const mcpServers = source.mcpServerNames.length > 0
312
+ ? await runSearchMultiSelectPicker(source.mcpServerNames, undefined, "选择要迁移的 MCP(默认全选,推荐先全部保留)")
313
+ : [];
314
+ if (mcpServers === null) {
315
+ console.error("cancelled, no changes made");
316
+ process.exit(1);
317
+ }
318
+ return { skills: skills ?? "none", mcpServers: mcpServers ?? "none", memories: "all", memoryMigration: "all", mcpRoutes: {}, runtimeDelivery: {} };
319
+ }
320
+ async function resolveInteractiveMemoryChoice(homeDir, opts) {
321
+ if (opts.memory !== undefined || opts.selectionFile || opts.json)
322
+ return undefined;
323
+ const current = loadCanonicalSource(homeDir).mcp.servers[MEMORY_SERVER_NAME] ? "on" : "off";
324
+ const canPrompt = Boolean(opts.promptForMemory) || (opts.isTTY ?? process.stdin.isTTY === true) && canUseInteractivePicker();
325
+ if (!canPrompt)
326
+ return undefined;
327
+ const choice = opts.promptForMemory
328
+ ? await opts.promptForMemory(current)
329
+ : (() => {
330
+ const options = current === "on"
331
+ ? ["保持开启(推荐:继续使用共享 Memory)", "关闭共享 Memory"]
332
+ : ["保持关闭(推荐:不改变现有环境)", "启用共享 Memory(推荐:Kimi/Pi 共享同一 graph)"];
333
+ return runSingleSelectPicker(options, undefined, "选择 Trellis Memory 托管方式").then((index) => {
334
+ if (index === null) {
335
+ console.error("cancelled, no changes made");
336
+ process.exit(1);
337
+ }
338
+ if (index === 0)
339
+ return "keep";
340
+ return current === "on" ? "off" : "on";
341
+ });
342
+ })();
343
+ if (choice === "on" || choice === "off")
344
+ return choice;
345
+ return undefined;
346
+ }
347
+ async function resolveInteractiveMcpRoutes(agentIds, serverNames, hubConfigured) {
348
+ const routes = {};
349
+ for (const agent of agentIds) {
350
+ const options = interactiveMcpRouteOptions(hubConfigured);
351
+ const modeIndex = await runSingleSelectPicker(options, undefined, `选择 ${agent} 的 MCP 模式(推荐 Gateway:Trellis 本机统一托管;Hub 只用于已有外部服务)`);
352
+ if (modeIndex === null) {
353
+ console.error("cancelled, no changes made");
354
+ process.exit(1);
355
+ }
356
+ const mode = options[modeIndex].mode;
357
+ if (mode === "hub") {
358
+ routes[agent] = { mode };
359
+ continue;
360
+ }
361
+ const selected = await runSearchMultiSelectPicker(serverNames, undefined, `选择 ${agent} 的 MCP 服务器(默认全选,推荐先全部启用)`);
362
+ if (selected === null) {
363
+ console.error("cancelled, no changes made");
364
+ process.exit(1);
365
+ }
366
+ routes[agent] = { mode, servers: selected ?? [] };
367
+ }
368
+ return routes;
369
+ }
212
370
  /** Shared by `--manage` and the interactive prompt's answer — same
213
371
  * grammar either way (design.md D4): a real agent id list, comma- or
214
372
  * whitespace-separated numbers referring to `candidates`' own order, or
@@ -300,6 +458,38 @@ function validateMemoryOption(memory) {
300
458
  return { error: `--memory must be "on" or "off" (got "${memory}")` };
301
459
  return { value: memory };
302
460
  }
461
+ function validateMemoryMigrationOption(memoryMigrate) {
462
+ if (memoryMigrate === undefined)
463
+ return {};
464
+ if (memoryMigrate !== "on" && memoryMigrate !== "off")
465
+ return { error: `--memory-migrate must be "on" or "off" (got "${memoryMigrate}")` };
466
+ return { value: memoryMigrate };
467
+ }
468
+ function preflightMcpMode(mode) {
469
+ if (!mode)
470
+ return undefined;
471
+ if (mode.kind === "hub") {
472
+ try {
473
+ const parsed = new URL(mode.url);
474
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
475
+ return `Hub URL must use http:// or https:// (got "${mode.url}")`;
476
+ }
477
+ }
478
+ catch {
479
+ return `Hub URL is not valid: "${mode.url}"`;
480
+ }
481
+ return undefined;
482
+ }
483
+ if (mode.kind === "gateway") {
484
+ try {
485
+ execFileSync("which", ["trellis"], { stdio: "ignore" });
486
+ }
487
+ catch {
488
+ return "gateway mode requires the `trellis` executable to be available on PATH before any Agent config is changed";
489
+ }
490
+ }
491
+ return undefined;
492
+ }
303
493
  function mcpModeValueOf(mcp) {
304
494
  if (mcp.gateway?.enabled)
305
495
  return "gateway";
@@ -348,6 +538,49 @@ function memoryStatusLine(memory) {
348
538
  return `memory: ${memory.current} (unchanged) — pass --memory ${memory.current === "on" ? "off" : "on"} to change`;
349
539
  return `memory: ${memory.current} (changed from ${memory.previous})`;
350
540
  }
541
+ /** Converges explicit per-Agent route lists with the shared Memory toggle.
542
+ * Legacy/top-level routes without a `servers` list already expose every
543
+ * eligible server and need no rewrite. Hub routes are intentionally opaque:
544
+ * the external hub owns its upstream set. */
545
+ export function mergeMemoryIntoExplicitRoutes(mcp, managedAgents, memoryEnabled) {
546
+ const routes = mcp.routes;
547
+ if (!routes)
548
+ return { mcp, changed: false };
549
+ const memoryServer = mcp.servers[MEMORY_SERVER_NAME];
550
+ const changedRoutes = { ...routes };
551
+ let changed = false;
552
+ for (const agent of managedAgents) {
553
+ const route = routes[agent];
554
+ if (!route || route.mode === "hub" || !route.servers)
555
+ continue;
556
+ const inScope = Boolean(memoryServer && memoryServer.enabled !== false && isInScope(agent, memoryServer.agents, managedAgents));
557
+ const nextServers = memoryEnabled && inScope
558
+ ? [...route.servers, ...(route.servers.includes(MEMORY_SERVER_NAME) ? [] : [MEMORY_SERVER_NAME])]
559
+ : route.servers.filter((name) => name !== MEMORY_SERVER_NAME);
560
+ if (nextServers.length === route.servers.length && nextServers.every((name, index) => name === route.servers[index]))
561
+ continue;
562
+ changedRoutes[agent] = { ...route, servers: nextServers };
563
+ changed = true;
564
+ }
565
+ return changed ? { mcp: { ...mcp, routes: changedRoutes }, changed } : { mcp, changed: false };
566
+ }
567
+ async function confirmMcpModeChange(previous, next, agents, opts) {
568
+ if (opts.promptForModeChange)
569
+ return opts.promptForModeChange(previous, next, agents);
570
+ if (!canUseInteractivePicker())
571
+ return true;
572
+ const affected = agents.length > 0 ? agents.join(", ") : "(none)";
573
+ console.error(`MCP 模式将从 ${previous} 切换到 ${next},影响托管 Agent:${affected}`);
574
+ if (next === "hub")
575
+ console.error("Hub 模式会让 Agent 连接外部 HTTP MCP 服务;Trellis 不会自动部署该服务。");
576
+ if (next === "gateway")
577
+ console.error("Gateway 模式会由 Trellis 在本机运行 MCP 入口并托管上游连接。");
578
+ const choice = await runSingleSelectPicker([
579
+ "取消切换(推荐:保持当前模式)",
580
+ "确认切换",
581
+ ], undefined, "确认 MCP 模式切换");
582
+ return choice === 1;
583
+ }
351
584
  async function resolveManagedAgents(opts, summary, alreadyManaged) {
352
585
  if (opts.manage !== undefined) {
353
586
  const parsed = parseManagedSelection(opts.manage, summary);
@@ -369,13 +602,35 @@ async function resolveManagedAgents(opts, summary, alreadyManaged) {
369
602
  function readManagedYaml(homeDir) {
370
603
  return loadCanonicalSource(homeDir).managedAgents;
371
604
  }
372
- function writeManagedYaml(homeDir, agents) {
373
- writeFileSync(join(homeDir, ".trellis", "managed.yaml"), `agents: [${agents.join(", ")}]\n`);
605
+ function writeManagedYaml(homeDir, agents, backup) {
606
+ const content = `agents: [${agents.join(", ")}]\n`;
607
+ const path = join(homeDir, ".trellis", "managed.yaml");
608
+ if (existsSync(path) && readFileSync(path, "utf-8") === content)
609
+ return;
610
+ if (backup)
611
+ backup.writeFile(path, content);
612
+ else
613
+ writeFileSync(path, content);
374
614
  }
375
615
  export async function collectOnboardPlan(opts = {}) {
376
616
  const homeDir = opts.homeDir ?? homedir();
377
- await collectInitReport(homeDir);
378
- const summary = await collectOnboardSummary(homeDir);
617
+ if (opts.dryRun && !existsSync(join(homeDir, ".trellis"))) {
618
+ return {
619
+ summary: [],
620
+ refusal: "dry-run does not bootstrap a missing ~/.trellis — run `trellis init` first, then re-run the preview",
621
+ verdict: [],
622
+ };
623
+ }
624
+ let capabilitySelection;
625
+ if (opts.selectionFile) {
626
+ const parsedSelection = parseCapabilitySelectionFile(opts.selectionFile);
627
+ if (!parsedSelection.ok)
628
+ return { summary: [], refusal: parsedSelection.error, verdict: [] };
629
+ capabilitySelection = parsedSelection.selection;
630
+ }
631
+ await collectInitReport(homeDir, { write: !opts.dryRun });
632
+ const workspaceDir = opts.workspaceDir ?? process.cwd();
633
+ const summary = await collectOnboardSummary(homeDir, workspaceDir);
379
634
  const present = summary.filter((s) => s.present);
380
635
  if (present.length === 0) {
381
636
  return { summary, installHints: { ...INSTALL_HINTS }, verdict: [] };
@@ -390,11 +645,19 @@ export async function collectOnboardPlan(opts = {}) {
390
645
  if ("error" in mcpModeValidation) {
391
646
  return { summary, refusal: mcpModeValidation.error, verdict: [] };
392
647
  }
393
- const memoryValidation = validateMemoryOption(opts.memory);
394
- if ("error" in memoryValidation) {
395
- return { summary, refusal: memoryValidation.error, verdict: [] };
648
+ const explicitMemoryValidation = validateMemoryOption(opts.memory);
649
+ if ("error" in explicitMemoryValidation) {
650
+ return { summary, refusal: explicitMemoryValidation.error, verdict: [] };
651
+ }
652
+ const explicitMemoryMigrationValidation = validateMemoryMigrationOption(opts.memoryMigrate);
653
+ if ("error" in explicitMemoryMigrationValidation) {
654
+ return { summary, refusal: explicitMemoryMigrationValidation.error, verdict: [] };
396
655
  }
397
- const sourceCandidates = present.filter(hasContent);
656
+ const modePreflightError = preflightMcpMode(mcpModeValidation.mode);
657
+ if (modePreflightError) {
658
+ return { summary, refusal: modePreflightError, verdict: [] };
659
+ }
660
+ const sourceCandidates = present.filter((candidate) => hasSelectedSourceContent(candidate, capabilitySelection));
398
661
  let source;
399
662
  let sourceReason;
400
663
  if (opts.agent) {
@@ -470,35 +733,65 @@ export async function collectOnboardPlan(opts = {}) {
470
733
  // managed set, not an abort of the rest of the flow.
471
734
  }
472
735
  const managedAgents = [...new Set([...alreadyManaged, ...resolvedNew])];
473
- if (!opts.dryRun) {
474
- writeManagedYaml(homeDir, managedAgents);
475
- }
476
- // MCP mode + memory-server resolution (design.md D5/D11) — grouped
477
- // with managed-set above as "how is this machine configured"
478
- // decisions resolved once per run, ahead of migrate/sync/mcp-sync/
479
- // memory-sync below, which act on the result. Both flags were already
480
- // validated before managed-set was even touched; omitting either
481
- // resolves to "no change requested" here too (design.md D1/D12) — no
482
- // writer call at all, on either a first run or a later one.
736
+ // Resolve all mode/memory decisions before opening the transaction. A
737
+ // refusal here must leave both canonical and managed state untouched.
483
738
  const canonicalForModeAndMemory = loadCanonicalSource(homeDir);
739
+ const interactiveMemory = await resolveInteractiveMemoryChoice(homeDir, opts);
740
+ const memoryValue = explicitMemoryValidation.value ?? interactiveMemory;
741
+ const memoryValidation = { value: memoryValue };
742
+ const sourceSummaryForInventory = source ? summary.find((item) => item.agent === source) : undefined;
743
+ const nativeMemory = sourceSummaryForInventory && sourceSummaryForInventory.nativeMemoryStatus !== "supported"
744
+ ? [{ name: `${source}:native`, detail: sourceSummaryForInventory.nativeMemoryDetail }]
745
+ : [];
746
+ const inventory = sourceSummaryForInventory
747
+ ? buildCapabilityInventory(sourceSummaryForInventory, canonicalForModeAndMemory, nativeMemory)
748
+ : undefined;
749
+ const capabilitySummary = {
750
+ awarenessSkill: canonicalForModeAndMemory.skills.some((skill) => skill.name === TRELLIS_RUNTIME_SKILL_NAME),
751
+ globalInstructions: { available: existsSync(canonicalForModeAndMemory.instructionsFile), native: true, runtimeReadable: true },
752
+ skills: canonicalForModeAndMemory.skills.length,
753
+ memories: canonicalForModeAndMemory.memories.length,
754
+ mcpServers: Object.keys(canonicalForModeAndMemory.mcp.servers).length,
755
+ };
756
+ const managedAgentRuntime = managedAgents.map((agent) => ({
757
+ agent,
758
+ delivery: capabilityDeliveryForAgent(agent, canonicalForModeAndMemory.mcp),
759
+ route: canonicalForModeAndMemory.mcp.routes?.[agent]?.mode
760
+ ?? (isGatewayAgent(agent, canonicalForModeAndMemory.mcp, managedAgents) ? "gateway" : canonicalForModeAndMemory.mcp.hub ? "hub" : "direct"),
761
+ }));
484
762
  const mcpModeResolution = resolveMcpModeChange(mcpModeValidation.mode, mcpModeValueOf(canonicalForModeAndMemory.mcp));
485
763
  const mcpMode = { current: mcpModeResolution.current, previous: mcpModeResolution.previous, changed: mcpModeResolution.changed };
486
764
  const serversYamlPath = join(homeDir, ".trellis", "mcp", "servers.yaml");
487
- if (mcpModeResolution.write && !opts.dryRun) {
488
- writeMcpModeYaml(serversYamlPath, mcpModeResolution.write);
489
- }
490
765
  const memoryConfiguredBefore = canonicalForModeAndMemory.mcp.servers[MEMORY_SERVER_NAME] !== undefined;
491
766
  const memoryResolution = resolveMemoryToggle(memoryValidation.value, memoryConfiguredBefore, canonicalForModeAndMemory.mcp.knownHostInjected);
492
767
  if ("refusal" in memoryResolution) {
493
768
  return { summary, source, sourceReason, managedAgents, mcpMode, refusal: memoryResolution.refusal, verdict: [] };
494
769
  }
495
770
  const memory = { current: memoryResolution.current, previous: memoryResolution.previous, changed: memoryResolution.changed };
771
+ if (!opts.dryRun
772
+ && !opts.json
773
+ && mcpMode.changed
774
+ && (opts.isTTY ?? process.stdin.isTTY === true)
775
+ && (opts.promptForModeChange || canUseInteractivePicker())) {
776
+ const accepted = await confirmMcpModeChange(mcpMode.previous, mcpMode.current, managedAgents, opts);
777
+ if (!accepted)
778
+ return { summary, source, sourceReason, managedAgents, mcpMode, memory, refusal: "MCP mode change cancelled — current mode was preserved", verdict: [] };
779
+ }
780
+ // One transaction for the entire onboarding run, including canonical
781
+ // state. A dry run opens no session and a no-op session creates no backup.
782
+ const backupSession = opts.dryRun ? undefined : openBackupSession(homeDir, "onboard");
783
+ if (!opts.dryRun) {
784
+ writeManagedYaml(homeDir, managedAgents, backupSession);
785
+ }
786
+ if (mcpModeResolution.write && !opts.dryRun) {
787
+ writeMcpModeYaml(serversYamlPath, mcpModeResolution.write, backupSession);
788
+ }
496
789
  if (memory.changed && !opts.dryRun) {
497
790
  if (memory.current === "on") {
498
- upsertServerYaml(serversYamlPath, MEMORY_SERVER_NAME, DEFAULT_MEMORY_SERVER_DEF);
791
+ upsertServerYaml(serversYamlPath, MEMORY_SERVER_NAME, DEFAULT_MEMORY_SERVER_DEF, backupSession);
499
792
  }
500
793
  else {
501
- removeServerYaml(serversYamlPath, MEMORY_SERVER_NAME);
794
+ removeServerYaml(serversYamlPath, MEMORY_SERVER_NAME, backupSession);
502
795
  }
503
796
  }
504
797
  let migratePlan;
@@ -506,22 +799,87 @@ export async function collectOnboardPlan(opts = {}) {
506
799
  logProgress(opts, "migrate");
507
800
  if (source) {
508
801
  const sourceSummary = summary.find((s) => s.agent === source);
509
- const categories = await resolveMigrateCategories(sourceSummary, opts);
802
+ const interactiveSelection = capabilitySelection ?? await resolveInteractiveCapabilitySelection(sourceSummary, opts);
803
+ const categories = await resolveMigrateCategories(sourceSummary, opts, interactiveSelection);
510
804
  if (categories.length > 0) {
511
- migratePlan = await collectMigratePlan(source, homeDir, categories);
805
+ migratePlan = filterMigratePlan(await collectMigratePlan(source, homeDir, categories, workspaceDir), interactiveSelection);
512
806
  if (!opts.dryRun) {
513
- applyMigratePlan(migratePlan, homeDir);
807
+ applyMigratePlan(migratePlan, homeDir, backupSession);
514
808
  }
515
809
  }
516
810
  else {
517
811
  migrateSkipped = "migrate skipped — no categories selected";
518
812
  }
519
813
  }
520
- // One session for the whole chained run (trellis-backup-rollback) —
521
- // `--dry-run` opens none, there's nothing either stage will write.
522
- // Neither collectSyncReport nor collectMcpSyncReport finalizes a
523
- // session they were handed; only this caller does, once, after both.
524
- const backupSession = opts.dryRun ? undefined : openBackupSession(homeDir, "onboard");
814
+ const currentMcpAfterMigration = loadCanonicalSource(homeDir).mcp;
815
+ const migratedMcpNames = migratePlan?.items.filter((item) => item.kind === "mcp").map((item) => item.name) ?? [];
816
+ const availableMcpNames = [...new Set([...Object.keys(currentMcpAfterMigration.servers), ...migratedMcpNames])];
817
+ let requestedRoutes = capabilitySelection?.mcpRoutes;
818
+ const shouldPromptForRoutes = !opts.selectionFile
819
+ && opts.mcpMode === undefined
820
+ && !currentMcpAfterMigration.routes
821
+ && !currentMcpAfterMigration.gateway
822
+ && !currentMcpAfterMigration.hub
823
+ && managedAgents.length > 0
824
+ && availableMcpNames.length > 0
825
+ && canUseInteractivePicker();
826
+ if (!requestedRoutes && shouldPromptForRoutes) {
827
+ requestedRoutes = await resolveInteractiveMcpRoutes(managedAgents, availableMcpNames, currentMcpAfterMigration.hub !== undefined);
828
+ }
829
+ // ZCode's generic ~/.agents/skills discovery bypasses Trellis scope. Its
830
+ // default is therefore Runtime-first unless a capability-selection file
831
+ // explicitly chooses native/both, or canonical already has a choice.
832
+ const zcodeRuntimeRecommendation = managedAgents.includes("zcode") && currentMcpAfterMigration.runtime?.delivery?.zcode === undefined
833
+ ? { zcode: "mcp" }
834
+ : {};
835
+ const requestedRuntimeDelivery = {
836
+ ...zcodeRuntimeRecommendation,
837
+ ...(capabilitySelection?.runtimeDelivery ?? {}),
838
+ };
839
+ let mcpSyncOverride;
840
+ if ((requestedRoutes && Object.keys(requestedRoutes).length > 0) || Object.keys(requestedRuntimeDelivery).length > 0) {
841
+ const currentMcp = currentMcpAfterMigration;
842
+ const routes = requestedRoutes && Object.keys(requestedRoutes).length > 0
843
+ ? { ...(currentMcp.routes ?? {}), ...requestedRoutes }
844
+ : currentMcp.routes;
845
+ const delivery = Object.keys(requestedRuntimeDelivery).length > 0
846
+ ? { ...(currentMcp.runtime?.delivery ?? {}), ...requestedRuntimeDelivery }
847
+ : currentMcp.runtime?.delivery;
848
+ mcpSyncOverride = {
849
+ ...currentMcp,
850
+ ...(routes ? { routes } : {}),
851
+ ...(delivery ? { runtime: { ...(currentMcp.runtime ?? {}), delivery } } : {}),
852
+ };
853
+ if (!opts.dryRun && requestedRoutes && Object.keys(requestedRoutes).length > 0) {
854
+ writeMcpRoutesYaml(join(homeDir, ".trellis", "mcp", "servers.yaml"), routes ?? {}, backupSession);
855
+ }
856
+ if (!opts.dryRun && Object.keys(requestedRuntimeDelivery).length > 0) {
857
+ writeMcpRuntimeDeliveryYaml(join(homeDir, ".trellis", "mcp", "servers.yaml"), delivery ?? {}, backupSession);
858
+ }
859
+ }
860
+ // Dry-run must plan the Memory server that this transaction would write;
861
+ // otherwise the following mcp-sync/memory-sync stages would inspect the
862
+ // pre-run canonical file and falsely report an unconfigured backend. The
863
+ // effective view is never persisted here — real writes above still use the
864
+ // normal backup transaction.
865
+ const effectiveMcpForRun = {
866
+ ...(mcpSyncOverride ?? currentMcpAfterMigration),
867
+ servers: { ...(mcpSyncOverride ?? currentMcpAfterMigration).servers },
868
+ };
869
+ if (memory.current === "on") {
870
+ effectiveMcpForRun.servers[MEMORY_SERVER_NAME] = effectiveMcpForRun.servers[MEMORY_SERVER_NAME] ?? DEFAULT_MEMORY_SERVER_DEF;
871
+ }
872
+ else {
873
+ delete effectiveMcpForRun.servers[MEMORY_SERVER_NAME];
874
+ }
875
+ const mergedMemoryRoutes = mergeMemoryIntoExplicitRoutes(effectiveMcpForRun, managedAgents, memory.current === "on");
876
+ const effectiveMcpWithRoutes = mergedMemoryRoutes.mcp;
877
+ if (mergedMemoryRoutes.changed && !opts.dryRun && effectiveMcpWithRoutes.routes) {
878
+ writeMcpRoutesYaml(serversYamlPath, effectiveMcpWithRoutes.routes, backupSession);
879
+ }
880
+ // The same session now covers canonical and native writes. Neither
881
+ // collectSyncReport nor collectMcpSyncReport finalizes it; this caller
882
+ // finalizes once after every verification stage has completed.
525
883
  logProgress(opts, "sync");
526
884
  const syncReport = await collectSyncReport({ homeDir, dryRun: opts.dryRun, managedAgents, backupSession });
527
885
  // The self-verification re-plan (design.md D2b): immediately after a
@@ -533,18 +891,18 @@ export async function collectOnboardPlan(opts = {}) {
533
891
  // written, so there is nothing to verify.
534
892
  const syncVerification = opts.dryRun ? undefined : await collectSyncReport({ homeDir, dryRun: true, managedAgents });
535
893
  logProgress(opts, "mcp sync");
536
- const mcpSyncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun, managedAgents, backupSession });
894
+ const mcpSyncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun, managedAgents, backupSession, mcp: effectiveMcpWithRoutes });
537
895
  const mcpSyncVerification = opts.dryRun ? undefined : await collectMcpSyncReport({ homeDir, dryRun: true, managedAgents });
538
- backupSession?.finalize();
539
896
  // Independent of `managedAgents` — the shared memory server is not
540
897
  // per-agent (design.md D4, trellis-onboard-mcp-memory), unlike
541
898
  // sync/mcp-sync above. Runs unconditionally once reached; an
542
899
  // unconfigured `memory` server is a legitimate "nothing to do yet"
543
900
  // state (design.md D5), not gated on any prior step here.
544
901
  logProgress(opts, "memory sync");
545
- const memorySyncResult = collectMemorySyncResult(homeDir);
902
+ const selectedMemoryNames = capabilitySelection ? resolveSelectedNames(capabilitySelection.memories, loadCanonicalSource(homeDir).memories.map((memory) => memory.name)) : undefined;
903
+ const memorySyncResult = collectMemorySyncResult(homeDir, selectedMemoryNames, effectiveMcpWithRoutes);
546
904
  if (!opts.dryRun) {
547
- applyMemorySync(memorySyncResult);
905
+ applyMemorySync(memorySyncResult, backupSession);
548
906
  }
549
907
  // Read-only, no dryRun concept — same report either way, run last since
550
908
  // it audits the config mcp sync just wrote (or, on --dry-run, whatever
@@ -572,8 +930,27 @@ export async function collectOnboardPlan(opts = {}) {
572
930
  ...normalizeSecretsAuditVerdict(secretsAuditReport),
573
931
  ...normalizeDoctorVerdict(doctorReport, managedAgents),
574
932
  ];
933
+ let rollback;
934
+ if (!opts.dryRun && backupSession?.hasOperations()) {
935
+ backupSession.finalize();
936
+ // Doctor intentionally scans the whole machine, including unmanaged
937
+ // Agents and pre-existing cross-agent drift. It is informative health
938
+ // state, not proof that this transaction failed. Roll back only stages
939
+ // whose writes/on-write verification belong to this onboard run.
940
+ const transactionBlocked = verdict.some((item) => item.severity === "blocked" && !item.stage.startsWith("doctor"));
941
+ if (transactionBlocked) {
942
+ rollback = await collectRollbackPlan(homeDir, backupSession.runId);
943
+ await applyRollbackPlan(homeDir, rollback.runId, loadManifest(homeDir, rollback.runId), rollback.items);
944
+ }
945
+ }
946
+ else {
947
+ backupSession?.finalize();
948
+ }
575
949
  return {
576
950
  summary,
951
+ inventory,
952
+ capabilitySummary,
953
+ managedAgentRuntime,
577
954
  source,
578
955
  sourceReason,
579
956
  managedAgents,
@@ -587,6 +964,7 @@ export async function collectOnboardPlan(opts = {}) {
587
964
  memorySyncResult,
588
965
  secretsAuditReport,
589
966
  doctorReport,
967
+ rollback,
590
968
  mcpMode,
591
969
  memory,
592
970
  verdict,
@@ -604,8 +982,11 @@ async function offerToApply(opts) {
604
982
  return opts.promptToApply();
605
983
  if (!canUseInteractivePicker())
606
984
  return false;
607
- console.log("");
608
- const choice = await runSingleSelectPicker(["No, don't apply", "Yes, apply now"]);
985
+ console.error("");
986
+ const choice = await runSingleSelectPicker([
987
+ "暂不应用(推荐:先检查上面的预览)",
988
+ "立即应用",
989
+ ], undefined, "预览已完成,是否写入配置?");
609
990
  return choice === 1;
610
991
  }
611
992
  export async function runOnboard(opts = {}) {
@@ -679,6 +1060,13 @@ function printResult(result, dryRun, showStatusLine) {
679
1060
  : `${install.agent} has no npm package to install (see \`trellis init\`'s install hint) — left out of this run's managed set.`);
680
1061
  }
681
1062
  console.log(`Managed agents: ${result.managedAgents && result.managedAgents.length > 0 ? result.managedAgents.join(", ") : "(none)"}`);
1063
+ if (showStatusLine && result.managedAgentRuntime?.length) {
1064
+ console.log(`Agent delivery: ${result.managedAgentRuntime.map((item) => `${item.agent}=${item.delivery}/${item.route}`).join(", ")}`);
1065
+ }
1066
+ if (showStatusLine && result.capabilitySummary) {
1067
+ const summary = result.capabilitySummary;
1068
+ console.log(`Trellis capabilities: awareness=${summary.awarenessSkill ? "ready" : "missing"}, global instructions=${summary.globalInstructions.native ? "native" : "unavailable"} + Runtime read, Skills=${summary.skills}, Memory=${summary.memories}, MCP=${summary.mcpServers}`);
1069
+ }
682
1070
  // Informational only, never a prompt (design.md D6/D12,
683
1071
  // trellis-onboard-mcp-mode) — this is what keeps `--mcp-mode`/
684
1072
  // `--memory` discoverable without costing every run a keystroke.
@@ -686,6 +1074,10 @@ function printResult(result, dryRun, showStatusLine) {
686
1074
  // already uses.
687
1075
  if (showStatusLine && result.mcpMode) {
688
1076
  console.log(mcpModeStatusLine(result.mcpMode));
1077
+ if (result.mcpMode.changed) {
1078
+ const affected = result.managedAgents && result.managedAgents.length > 0 ? result.managedAgents.join(", ") : "(none)";
1079
+ console.log(`mode transition impact: ${result.mcpMode.previous} → ${result.mcpMode.current}; managed agents: ${affected}`);
1080
+ }
689
1081
  }
690
1082
  if (showStatusLine && result.memory) {
691
1083
  console.log(memoryStatusLine(result.memory));
@@ -725,4 +1117,10 @@ function printResult(result, dryRun, showStatusLine) {
725
1117
  console.log("\nhealth scan (trellis doctor)");
726
1118
  printDoctorReport(result.doctorReport);
727
1119
  }
1120
+ if (result.rollback) {
1121
+ console.log("\nautomatic rollback");
1122
+ const restored = result.rollback.items.filter((item) => item.action === "restore").length;
1123
+ const conflicts = result.rollback.items.filter((item) => item.action === "conflict").length;
1124
+ console.log(`↩️ restored ${restored} path(s), ${conflicts} rollback conflict(s); backup: ${result.rollback.runId}`);
1125
+ }
728
1126
  }