agent-trellis 0.8.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 (241) hide show
  1. package/README.md +43 -207
  2. package/dist/adapters/zcode.d.ts +21 -0
  3. package/dist/adapters/zcode.js +234 -0
  4. package/dist/cli.js +25 -9
  5. package/dist/commands/doctor.js +2 -0
  6. package/dist/commands/init.d.ts +1 -1
  7. package/dist/commands/init.js +4 -1
  8. package/dist/commands/mcp.d.ts +56 -14
  9. package/dist/commands/mcp.js +105 -28
  10. package/dist/commands/mcpGateway.js +2 -1
  11. package/dist/commands/mcpImport.d.ts +21 -2
  12. package/dist/commands/mcpImport.js +30 -7
  13. package/dist/commands/memory.d.ts +13 -1
  14. package/dist/commands/memory.js +15 -0
  15. package/dist/commands/migrate.js +4 -1
  16. package/dist/commands/onboard.js +15 -4
  17. package/dist/commands/rollback.js +24 -1
  18. package/dist/commands/secretsAudit.js +4 -1
  19. package/dist/commands/skill.d.ts +35 -6
  20. package/dist/commands/skill.js +67 -13
  21. package/dist/commands/sync.js +3 -1
  22. package/dist/core/adapter.d.ts +11 -1
  23. package/dist/core/types.d.ts +1 -1
  24. package/dist/core/types.js +1 -0
  25. package/dist/lib/agentBridge.d.ts +169 -0
  26. package/dist/lib/agentBridge.js +367 -0
  27. package/dist/lib/agentBridgeProvider.d.ts +14 -0
  28. package/dist/lib/agentBridgeProvider.js +95 -0
  29. package/dist/lib/backup.d.ts +30 -0
  30. package/dist/lib/backup.js +16 -1
  31. package/dist/lib/chatAgents.d.ts +42 -0
  32. package/dist/lib/chatAgents.js +59 -0
  33. package/dist/lib/envVarNames.d.ts +3 -0
  34. package/dist/lib/envVarNames.js +19 -0
  35. package/dist/lib/installAgent.d.ts +1 -1
  36. package/dist/lib/installAgent.js +1 -1
  37. package/dist/lib/mcpMigrateRead.d.ts +1 -0
  38. package/dist/lib/mcpMigrateRead.js +19 -1
  39. package/dist/lib/realHomeSnapshot.js +5 -0
  40. package/dist/lib/runtimeControlProvider.js +2 -0
  41. package/dist/lib/taskStore.js +1 -1
  42. package/dist/lib/terminalPicker.js +33 -6
  43. package/dist/lib/zcodeOwnership.d.ts +17 -0
  44. package/dist/lib/zcodeOwnership.js +37 -0
  45. package/dist/pi-bridge/bundle.js +2 -1
  46. package/dist/probes/codex.d.ts +7 -0
  47. package/dist/probes/codex.js +37 -5
  48. package/dist/probes/zcode.d.ts +32 -0
  49. package/dist/probes/zcode.js +140 -0
  50. package/docs/architecture.md +9 -5
  51. package/docs/assets/brand-v3/trellis-mark-v3-a.png +0 -0
  52. package/docs/assets/brand-v3/trellis-mark-v3-a.svg +10 -0
  53. package/docs/assets/poster-series-v3/README.md +60 -0
  54. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.png +0 -0
  55. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.svg +51 -0
  56. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.png +0 -0
  57. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.svg +51 -0
  58. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.png +0 -0
  59. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.svg +48 -0
  60. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.png +0 -0
  61. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.svg +48 -0
  62. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.png +0 -0
  63. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.svg +56 -0
  64. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.png +0 -0
  65. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.svg +56 -0
  66. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.png +0 -0
  67. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.svg +47 -0
  68. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.png +0 -0
  69. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.svg +47 -0
  70. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.png +0 -0
  71. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.svg +51 -0
  72. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.png +0 -0
  73. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.svg +51 -0
  74. package/docs/getting-started.md +173 -12
  75. package/docs/overview.md +6 -3
  76. package/docs/overview.zh-CN.md +5 -3
  77. package/docs/roadmap.md +2 -1
  78. package/docs/video/README.md +76 -0
  79. package/docs/video/build/audio/01-opening.aiff +0 -0
  80. package/docs/video/build/audio/01-opening.mp3 +0 -0
  81. package/docs/video/build/audio/02-pain.aiff +0 -0
  82. package/docs/video/build/audio/02-pain.mp3 +0 -0
  83. package/docs/video/build/audio/03-solution.aiff +0 -0
  84. package/docs/video/build/audio/03-solution.mp3 +0 -0
  85. package/docs/video/build/audio/04-install.aiff +0 -0
  86. package/docs/video/build/audio/04-install.mp3 +0 -0
  87. package/docs/video/build/audio/04-setup.mp3 +0 -0
  88. package/docs/video/build/audio/05-dryrun.mp3 +0 -0
  89. package/docs/video/build/audio/05-init.aiff +0 -0
  90. package/docs/video/build/audio/05-init.mp3 +0 -0
  91. package/docs/video/build/audio/05-interactive.mp3 +0 -0
  92. package/docs/video/build/audio/06-dryrun.aiff +0 -0
  93. package/docs/video/build/audio/06-dryrun.mp3 +0 -0
  94. package/docs/video/build/audio/06-onboard.mp3 +0 -0
  95. package/docs/video/build/audio/06-run.mp3 +0 -0
  96. package/docs/video/build/audio/07-onboard.aiff +0 -0
  97. package/docs/video/build/audio/07-onboard.mp3 +0 -0
  98. package/docs/video/build/audio/07-verify.mp3 +0 -0
  99. package/docs/video/build/audio/08-closing.mp3 +0 -0
  100. package/docs/video/build/audio/08-doctor.aiff +0 -0
  101. package/docs/video/build/audio/08-doctor.mp3 +0 -0
  102. package/docs/video/build/audio/09-rollback.aiff +0 -0
  103. package/docs/video/build/audio/09-rollback.mp3 +0 -0
  104. package/docs/video/build/audio/10-principles.aiff +0 -0
  105. package/docs/video/build/audio/10-principles.mp3 +0 -0
  106. package/docs/video/build/audio/11-closing.aiff +0 -0
  107. package/docs/video/build/audio/11-closing.mp3 +0 -0
  108. package/docs/video/build/frames/01-opening-0.png +0 -0
  109. package/docs/video/build/frames/02-pain-1.png +0 -0
  110. package/docs/video/build/frames/02-pain-2.png +0 -0
  111. package/docs/video/build/frames/03-solution-3.png +0 -0
  112. package/docs/video/build/frames/04-install-4.png +0 -0
  113. package/docs/video/build/frames/04-setup-4.png +0 -0
  114. package/docs/video/build/frames/04-setup-5.png +0 -0
  115. package/docs/video/build/frames/05-dryrun-6.png +0 -0
  116. package/docs/video/build/frames/05-init-5.png +0 -0
  117. package/docs/video/build/frames/05-interactive-5.png +0 -0
  118. package/docs/video/build/frames/05-interactive-6.png +0 -0
  119. package/docs/video/build/frames/06-dryrun-6.png +0 -0
  120. package/docs/video/build/frames/06-dryrun-7.png +0 -0
  121. package/docs/video/build/frames/06-onboard-7.png +0 -0
  122. package/docs/video/build/frames/06-onboard-8.png +0 -0
  123. package/docs/video/build/frames/06-run-7.png +0 -0
  124. package/docs/video/build/frames/06-run-8.png +0 -0
  125. package/docs/video/build/frames/07-onboard-10.png +0 -0
  126. package/docs/video/build/frames/07-onboard-7.png +0 -0
  127. package/docs/video/build/frames/07-onboard-8.png +0 -0
  128. package/docs/video/build/frames/07-onboard-9.png +0 -0
  129. package/docs/video/build/frames/07-verify-10.png +0 -0
  130. package/docs/video/build/frames/07-verify-8.png +0 -0
  131. package/docs/video/build/frames/07-verify-9.png +0 -0
  132. package/docs/video/build/frames/08-closing-10.png +0 -0
  133. package/docs/video/build/frames/08-closing-11.png +0 -0
  134. package/docs/video/build/frames/08-doctor-11.png +0 -0
  135. package/docs/video/build/frames/08-doctor-9.png +0 -0
  136. package/docs/video/build/frames/09-rollback-10.png +0 -0
  137. package/docs/video/build/frames/09-rollback-12.png +0 -0
  138. package/docs/video/build/frames/10-principles-11.png +0 -0
  139. package/docs/video/build/frames/10-principles-13.png +0 -0
  140. package/docs/video/build/frames/11-closing-12.png +0 -0
  141. package/docs/video/build/frames/11-closing-14.png +0 -0
  142. package/docs/video/build/segments/01-opening.mp4 +0 -0
  143. package/docs/video/build/segments/01-opening.txt +3 -0
  144. package/docs/video/build/segments/02-pain.mp4 +0 -0
  145. package/docs/video/build/segments/02-pain.txt +5 -0
  146. package/docs/video/build/segments/03-solution.mp4 +0 -0
  147. package/docs/video/build/segments/03-solution.txt +3 -0
  148. package/docs/video/build/segments/04-install.mp4 +0 -0
  149. package/docs/video/build/segments/04-install.txt +3 -0
  150. package/docs/video/build/segments/04-setup.mp4 +0 -0
  151. package/docs/video/build/segments/04-setup.txt +3 -0
  152. package/docs/video/build/segments/05-dryrun.mp4 +0 -0
  153. package/docs/video/build/segments/05-dryrun.txt +3 -0
  154. package/docs/video/build/segments/05-init.mp4 +0 -0
  155. package/docs/video/build/segments/05-init.txt +3 -0
  156. package/docs/video/build/segments/05-interactive.mp4 +0 -0
  157. package/docs/video/build/segments/05-interactive.txt +5 -0
  158. package/docs/video/build/segments/06-dryrun.mp4 +0 -0
  159. package/docs/video/build/segments/06-dryrun.txt +3 -0
  160. package/docs/video/build/segments/06-onboard.mp4 +0 -0
  161. package/docs/video/build/segments/06-onboard.txt +5 -0
  162. package/docs/video/build/segments/06-run.mp4 +0 -0
  163. package/docs/video/build/segments/06-run.txt +3 -0
  164. package/docs/video/build/segments/07-onboard.mp4 +0 -0
  165. package/docs/video/build/segments/07-onboard.txt +5 -0
  166. package/docs/video/build/segments/07-verify.mp4 +0 -0
  167. package/docs/video/build/segments/07-verify.txt +5 -0
  168. package/docs/video/build/segments/08-closing.mp4 +0 -0
  169. package/docs/video/build/segments/08-closing.txt +3 -0
  170. package/docs/video/build/segments/08-doctor.mp4 +0 -0
  171. package/docs/video/build/segments/08-doctor.txt +3 -0
  172. package/docs/video/build/segments/09-rollback.mp4 +0 -0
  173. package/docs/video/build/segments/09-rollback.txt +3 -0
  174. package/docs/video/build/segments/10-principles.mp4 +0 -0
  175. package/docs/video/build/segments/10-principles.txt +3 -0
  176. package/docs/video/build/segments/11-closing.mp4 +0 -0
  177. package/docs/video/build/segments/11-closing.txt +3 -0
  178. package/docs/video/build/segments/final.txt +8 -0
  179. package/docs/video/build-ai/audio/01-hook.mp3 +0 -0
  180. package/docs/video/build-ai/audio/02-pain.mp3 +0 -0
  181. package/docs/video/build-ai/audio/03-solution.mp3 +0 -0
  182. package/docs/video/build-ai/audio/04-how.mp3 +0 -0
  183. package/docs/video/build-ai/audio/05-safe.mp3 +0 -0
  184. package/docs/video/build-ai/audio/06-closing.mp3 +0 -0
  185. package/docs/video/build-ai/clips/01-hook.mp4 +0 -0
  186. package/docs/video/build-ai/clips/02-pain.mp4 +0 -0
  187. package/docs/video/build-ai/clips/03-solution.mp4 +0 -0
  188. package/docs/video/build-ai/clips/04-how.mp4 +0 -0
  189. package/docs/video/build-ai/clips/05-safe.mp4 +0 -0
  190. package/docs/video/build-ai/clips/06-closing.mp4 +0 -0
  191. package/docs/video/build-ai/first-frame-02-pain.png +0 -0
  192. package/docs/video/build-ai/first-frame-03-solution.png +0 -0
  193. package/docs/video/build-ai/first-frame-04-how.png +0 -0
  194. package/docs/video/build-ai/first-frame-05-safe.png +0 -0
  195. package/docs/video/build-ai/segments/01-hook.caption.txt +2 -0
  196. package/docs/video/build-ai/segments/01-hook.mp4 +0 -0
  197. package/docs/video/build-ai/segments/02-pain.mp4 +0 -0
  198. package/docs/video/build-ai/segments/03-solution.mp4 +0 -0
  199. package/docs/video/build-ai/segments/04-how.mp4 +0 -0
  200. package/docs/video/build-ai/segments/05-safe.mp4 +0 -0
  201. package/docs/video/build-ai/segments/06-closing.mp4 +0 -0
  202. package/docs/video/build-ai/segments/cap-01-hook.png +0 -0
  203. package/docs/video/build-ai/segments/cap-02-pain.png +0 -0
  204. package/docs/video/build-ai/segments/cap-03-solution.png +0 -0
  205. package/docs/video/build-ai/segments/cap-04-how.png +0 -0
  206. package/docs/video/build-ai/segments/cap-05-safe.png +0 -0
  207. package/docs/video/build-ai/segments/cap-06-closing.png +0 -0
  208. package/docs/video/build-ai/segments/final.txt +6 -0
  209. package/docs/video/build-posters/audio/01-hook.mp3 +0 -0
  210. package/docs/video/build-posters/audio/02-pain.mp3 +0 -0
  211. package/docs/video/build-posters/audio/03-solution.mp3 +0 -0
  212. package/docs/video/build-posters/audio/04-how.mp3 +0 -0
  213. package/docs/video/build-posters/audio/05-safe.mp3 +0 -0
  214. package/docs/video/build-posters/audio/06-closing.mp3 +0 -0
  215. package/docs/video/build-posters/cap-01-hook.png +0 -0
  216. package/docs/video/build-posters/cap-02-pain.png +0 -0
  217. package/docs/video/build-posters/cap-03-solution.png +0 -0
  218. package/docs/video/build-posters/cap-04-how.png +0 -0
  219. package/docs/video/build-posters/cap-05-safe.png +0 -0
  220. package/docs/video/build-posters/cap-06-closing.png +0 -0
  221. package/docs/video/build-posters/card-01-hook.png +0 -0
  222. package/docs/video/build-posters/card-06-closing.png +0 -0
  223. package/docs/video/build-posters/segments/01-hook.mp4 +0 -0
  224. package/docs/video/build-posters/segments/02-pain.mp4 +0 -0
  225. package/docs/video/build-posters/segments/03-solution.mp4 +0 -0
  226. package/docs/video/build-posters/segments/04-how.mp4 +0 -0
  227. package/docs/video/build-posters/segments/05-safe.mp4 +0 -0
  228. package/docs/video/build-posters/segments/06-closing.mp4 +0 -0
  229. package/docs/video/build-posters/segments/final.txt +6 -0
  230. package/docs/video/generate-video-ai.mjs +334 -0
  231. package/docs/video/generate-video-posters.mjs +204 -0
  232. package/docs/video/generate-video.mjs +426 -0
  233. package/docs/video/outline.zh-CN.md +102 -0
  234. package/docs/video/trellis-demo-video-ai.mp4 +0 -0
  235. package/docs/video/trellis-demo-video.mp4 +0 -0
  236. package/docs/video/voiceover.zh-CN.md +49 -0
  237. package/package.json +5 -2
  238. package/schema/servers.example.yaml +1 -1
  239. package/docs/assets/agent-trellis-architecture-horizontal.svg +0 -80
  240. package/docs/assets/agent-trellis-architecture-vertical.svg +0 -82
  241. package/docs/assets/trellis-mark.svg +0 -12
@@ -16,6 +16,7 @@ import * as codexProbe from "../probes/codex.js";
16
16
  import * as kiroProbe from "../probes/kiro.js";
17
17
  import * as piProbe from "../probes/pi.js";
18
18
  import * as kimiCodeProbe from "../probes/kimi-code.js";
19
+ import * as zcodeProbe from "../probes/zcode.js";
19
20
  import { ALL_AGENTS } from "../core/types.js";
20
21
  /** Exported so `migrate` can byte-compare canonical `agents.md` against
21
22
  * this exact string (design.md D3, trellis-cli-migrate) rather than
@@ -25,7 +26,7 @@ export const AGENTS_MD_TEMPLATE = `# Shared instructions
25
26
  Write what every agent should know here — communication preferences,
26
27
  project conventions, anything you'd otherwise repeat per agent.
27
28
 
28
- Already using Claude Code, Codex, Kiro, pi, or Kimi Code? \`trellis migrate --from
29
+ Already using Claude Code, Codex, Kiro, pi, Kimi Code, or ZCode? \`trellis migrate --from
29
30
  <agent>\` imports its real instructions and skills instead of starting
30
31
  from this placeholder.
31
32
  `;
@@ -95,6 +96,7 @@ export const INSTALL_HINTS = {
95
96
  kiro: "https://kiro.dev/downloads/",
96
97
  pi: "npm install -g @earendil-works/pi-coding-agent",
97
98
  "kimi-code": "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash",
99
+ zcode: "https://zcode.z.ai/en/docs/install",
98
100
  };
99
101
  function ensureFile(path, template, write = true) {
100
102
  if (existsSync(path)) {
@@ -132,6 +134,7 @@ export async function collectInitReport(homeDir = homedir(), options = {}) {
132
134
  { agent: "kiro", run: () => kiroProbe.probe(homeDir) },
133
135
  { agent: "pi", run: () => piProbe.probe(homeDir) },
134
136
  { agent: "kimi-code", run: () => kimiCodeProbe.probe(homeDir) },
137
+ { agent: "zcode", run: () => zcodeProbe.probe(homeDir) },
135
138
  ];
136
139
  const settled = await Promise.allSettled(probes.map((p) => p.run()));
137
140
  const agents = settled.map((result, index) => {
@@ -5,10 +5,12 @@
5
5
  * gated — remove), and reports what happened. Kept as a separate
6
6
  * command from bare `trellis sync` (skills/instructions) since the two
7
7
  * have different write mechanisms entirely, not because MCP removal is
8
- * unsafe (see `src/lib/mcpOwnership.ts`). `list`/`add`/`remove`
9
- * (trellis-canonical-cli-crud) are canonical-side only — an alternative
10
- * to hand-editing `~/.trellis/mcp/servers.yaml`, never touching any
11
- * agent's native config (that stays `sync`'s job).
8
+ * unsafe (see `src/lib/mcpOwnership.ts`). `add`/`remove` mutate the
9
+ * canonical source and then run the same sync `mcp sync` performs, so
10
+ * one command both edits the list and maps it onto the managed agents
11
+ * (removal propagates through the ownership ledger). `list` is
12
+ * read-only. All three are an alternative to hand-editing
13
+ * `~/.trellis/mcp/servers.yaml`.
12
14
  */
13
15
  import { type ServersYamlWriteResult } from "../core/canonical.js";
14
16
  import type { AdapterPlanItem } from "../core/adapter.js";
@@ -128,29 +130,69 @@ export declare function runMcpSetAuth(name: string | undefined, auth: string | u
128
130
  }): {
129
131
  exitCode: number;
130
132
  };
131
- export declare function applyMcpAddPlan(plan: McpAddPlan, homeDir?: string): ServersYamlWriteResult;
133
+ export declare function applyMcpAddPlan(plan: McpAddPlan, homeDir?: string, backup?: BackupSession): ServersYamlWriteResult;
134
+ export interface McpAddOutcome {
135
+ plan: McpAddPlan;
136
+ writeError?: string;
137
+ sync?: McpSyncReport;
138
+ }
139
+ /**
140
+ * The apply-and-cascade-sync orchestration `runMcpAdd` used to do inline
141
+ * and only expose via a console.log'd payload — split out so a
142
+ * non-CLI caller (trellis-gui's sidecar) can get the same structured
143
+ * result a JSON-mode CLI run prints, without scraping stdout or
144
+ * re-deriving the same sequencing itself (trellis-gui design.md
145
+ * Decision 1). Behavior-preserving: `runMcpAdd` below is now a thin
146
+ * wrapper over this plus its own printing, same as every other command
147
+ * in this file already splits compute from CLI I/O.
148
+ *
149
+ * Opens its own backup session around the direct `servers.yaml` write
150
+ * when the caller doesn't already have one open — this canonical write
151
+ * used to be the one path in this file that bypassed backup/rollback
152
+ * entirely (trellis-gui tasks.md 3.5 found this empirically: a real `mcp
153
+ * add` left zero trace in `trellis rollback --list`). Mirrors the
154
+ * `ownSession` pattern `collectMcpSyncReport` below already uses for
155
+ * exactly the same reason.
156
+ */
157
+ export declare function applyMcpAddWithSync(plan: McpAddPlan, opts?: {
158
+ homeDir?: string;
159
+ dryRun?: boolean;
160
+ backupSession?: BackupSession;
161
+ }): Promise<McpAddOutcome>;
132
162
  export declare function runMcpAdd(name: string | undefined, raw: McpAddRawArgs, opts?: {
133
163
  homeDir?: string;
134
164
  json?: boolean;
135
165
  dryRun?: boolean;
136
- }): {
166
+ }): Promise<{
137
167
  exitCode: number;
138
- };
168
+ }>;
139
169
  export type McpRemoveAction = "removed" | "not-found";
140
170
  export interface McpRemovePlan {
141
171
  name: string;
142
172
  action: McpRemoveAction;
143
173
  }
144
- /** Canonical-side only — never touches an agent's native config. Making
145
- * an already-synced agent forget it is `mcp sync`'s own removal-
146
- * propagation gap, tracked separately (roadmap.md P14), not this
147
- * command's job. */
174
+ /** Canonical plan only — `runMcpRemove` follows a real removal with the
175
+ * same sync `mcp sync` performs, which propagates the deletion to every
176
+ * agent whose entry the ownership ledger proves Trellis wrote
177
+ * (trellis-mcp-lifecycle-parity, roadmap P14). */
148
178
  export declare function collectMcpRemovePlan(name: string, homeDir?: string): McpRemovePlan;
149
- export declare function applyMcpRemovePlan(plan: McpRemovePlan, homeDir?: string): ServersYamlWriteResult;
179
+ export declare function applyMcpRemovePlan(plan: McpRemovePlan, homeDir?: string, backup?: BackupSession): ServersYamlWriteResult;
180
+ export interface McpRemoveOutcome {
181
+ plan: McpRemovePlan;
182
+ writeError?: string;
183
+ sync?: McpSyncReport;
184
+ }
185
+ /** See `applyMcpAddWithSync`'s doc comment — same split, same reason, same
186
+ * own-backup-session fix. */
187
+ export declare function applyMcpRemoveWithSync(plan: McpRemovePlan, opts?: {
188
+ homeDir?: string;
189
+ dryRun?: boolean;
190
+ backupSession?: BackupSession;
191
+ }): Promise<McpRemoveOutcome>;
150
192
  export declare function runMcpRemove(name: string, opts?: {
151
193
  homeDir?: string;
152
194
  json?: boolean;
153
195
  dryRun?: boolean;
154
- }): {
196
+ }): Promise<{
155
197
  exitCode: number;
156
- };
198
+ }>;
@@ -5,12 +5,15 @@
5
5
  * gated — remove), and reports what happened. Kept as a separate
6
6
  * command from bare `trellis sync` (skills/instructions) since the two
7
7
  * have different write mechanisms entirely, not because MCP removal is
8
- * unsafe (see `src/lib/mcpOwnership.ts`). `list`/`add`/`remove`
9
- * (trellis-canonical-cli-crud) are canonical-side only — an alternative
10
- * to hand-editing `~/.trellis/mcp/servers.yaml`, never touching any
11
- * agent's native config (that stays `sync`'s job).
8
+ * unsafe (see `src/lib/mcpOwnership.ts`). `add`/`remove` mutate the
9
+ * canonical source and then run the same sync `mcp sync` performs, so
10
+ * one command both edits the list and maps it onto the managed agents
11
+ * (removal propagates through the ownership ledger). `list` is
12
+ * read-only. All three are an alternative to hand-editing
13
+ * `~/.trellis/mcp/servers.yaml`.
12
14
  */
13
15
  import { homedir } from "node:os";
16
+ import { existsSync } from "node:fs";
14
17
  import { join } from "node:path";
15
18
  import { loadCanonicalSource, upsertServerYaml, removeServerYaml } from "../core/canonical.js";
16
19
  import { ALL_AGENTS, resolveScope } from "../core/types.js";
@@ -19,6 +22,7 @@ import { CodexAdapter } from "../adapters/codex.js";
19
22
  import { KiroAdapter } from "../adapters/kiro.js";
20
23
  import { PiAdapter } from "../adapters/pi.js";
21
24
  import { KimiCodeAdapter } from "../adapters/kimi-code.js";
25
+ import { ZcodeAdapter } from "../adapters/zcode.js";
22
26
  import { openBackupSession } from "../lib/backup.js";
23
27
  const ADAPTER_FACTORY = {
24
28
  "claude-code": (homeDir) => new ClaudeCodeAdapter(homeDir),
@@ -26,6 +30,7 @@ const ADAPTER_FACTORY = {
26
30
  kiro: (homeDir) => new KiroAdapter(homeDir),
27
31
  pi: (homeDir) => new PiAdapter(homeDir),
28
32
  "kimi-code": (homeDir) => new KimiCodeAdapter(homeDir),
33
+ zcode: (homeDir) => new ZcodeAdapter(homeDir),
29
34
  };
30
35
  /** Only agents in `canonical.managedAgents` — see src/commands/sync.ts's
31
36
  * own copy of this same restriction (trellis-managed-agents). */
@@ -297,12 +302,48 @@ export function runMcpSetAuth(name, auth, opts = {}) {
297
302
  }
298
303
  return { exitCode: ["invalid-input", "not-found"].includes(plan.action) || Boolean(writeError) ? 1 : 0 };
299
304
  }
300
- export function applyMcpAddPlan(plan, homeDir = homedir()) {
305
+ export function applyMcpAddPlan(plan, homeDir = homedir(), backup) {
301
306
  if (plan.action !== "create" || !plan.def)
302
307
  return { ok: true };
303
- return upsertServerYaml(serversYamlPath(homeDir), plan.name, plan.def);
308
+ return upsertServerYaml(serversYamlPath(homeDir), plan.name, plan.def, backup);
304
309
  }
305
- export function runMcpAdd(name, raw, opts = {}) {
310
+ /**
311
+ * The apply-and-cascade-sync orchestration `runMcpAdd` used to do inline
312
+ * and only expose via a console.log'd payload — split out so a
313
+ * non-CLI caller (trellis-gui's sidecar) can get the same structured
314
+ * result a JSON-mode CLI run prints, without scraping stdout or
315
+ * re-deriving the same sequencing itself (trellis-gui design.md
316
+ * Decision 1). Behavior-preserving: `runMcpAdd` below is now a thin
317
+ * wrapper over this plus its own printing, same as every other command
318
+ * in this file already splits compute from CLI I/O.
319
+ *
320
+ * Opens its own backup session around the direct `servers.yaml` write
321
+ * when the caller doesn't already have one open — this canonical write
322
+ * used to be the one path in this file that bypassed backup/rollback
323
+ * entirely (trellis-gui tasks.md 3.5 found this empirically: a real `mcp
324
+ * add` left zero trace in `trellis rollback --list`). Mirrors the
325
+ * `ownSession` pattern `collectMcpSyncReport` below already uses for
326
+ * exactly the same reason.
327
+ */
328
+ export async function applyMcpAddWithSync(plan, opts = {}) {
329
+ const homeDir = opts.homeDir ?? homedir();
330
+ const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "mcp-add") : undefined;
331
+ const session = opts.backupSession ?? ownSession;
332
+ let writeError;
333
+ if (!opts.dryRun && plan.action === "create") {
334
+ const result = applyMcpAddPlan(plan, homeDir, session);
335
+ if (!result.ok)
336
+ writeError = result.error;
337
+ }
338
+ ownSession?.finalize();
339
+ const planFailed = plan.action === "conflict" || plan.action === "invalid-input" || Boolean(writeError);
340
+ let syncReport;
341
+ if (!planFailed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
342
+ syncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun });
343
+ }
344
+ return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
345
+ }
346
+ export async function runMcpAdd(name, raw, opts = {}) {
306
347
  const homeDir = opts.homeDir ?? homedir();
307
348
  let plan;
308
349
  try {
@@ -312,37 +353,65 @@ export function runMcpAdd(name, raw, opts = {}) {
312
353
  console.error(err instanceof Error ? err.message : String(err));
313
354
  return { exitCode: 1 };
314
355
  }
315
- let writeError;
316
- if (!opts.dryRun && plan.action === "create") {
317
- const result = applyMcpAddPlan(plan, homeDir);
318
- if (!result.ok)
319
- writeError = result.error;
356
+ let outcome;
357
+ try {
358
+ outcome = await applyMcpAddWithSync(plan, { homeDir, dryRun: opts.dryRun });
359
+ }
360
+ catch (err) {
361
+ console.error(err instanceof Error ? err.message : String(err));
362
+ return { exitCode: 1 };
320
363
  }
364
+ const { writeError, sync: syncReport } = outcome;
321
365
  if (opts.json) {
322
- console.log(JSON.stringify(writeError ? { ...plan, writeError } : plan, null, 2));
366
+ const payload = writeError ? { ...plan, writeError } : plan;
367
+ console.log(JSON.stringify(syncReport ? { ...payload, sync: syncReport } : payload, null, 2));
323
368
  }
324
369
  else {
325
370
  console.log(`${opts.dryRun ? "[dry run] " : ""}mcp add ${plan.name}`);
326
371
  console.log(` [${plan.action}] ${plan.detail}`);
327
372
  if (writeError)
328
373
  console.error(` write failed: ${writeError}`);
374
+ if (syncReport)
375
+ printReport(syncReport, opts.dryRun ?? false);
329
376
  }
330
- return { exitCode: plan.action === "conflict" || plan.action === "invalid-input" || writeError ? 1 : 0 };
377
+ const planFailed = plan.action === "conflict" || plan.action === "invalid-input" || Boolean(writeError);
378
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
379
+ return { exitCode: planFailed || syncConflict ? 1 : 0 };
331
380
  }
332
- /** Canonical-side only — never touches an agent's native config. Making
333
- * an already-synced agent forget it is `mcp sync`'s own removal-
334
- * propagation gap, tracked separately (roadmap.md P14), not this
335
- * command's job. */
381
+ /** Canonical plan only — `runMcpRemove` follows a real removal with the
382
+ * same sync `mcp sync` performs, which propagates the deletion to every
383
+ * agent whose entry the ownership ledger proves Trellis wrote
384
+ * (trellis-mcp-lifecycle-parity, roadmap P14). */
336
385
  export function collectMcpRemovePlan(name, homeDir = homedir()) {
337
386
  const canonical = loadCanonicalSource(homeDir);
338
387
  return { name, action: canonical.mcp.servers[name] ? "removed" : "not-found" };
339
388
  }
340
- export function applyMcpRemovePlan(plan, homeDir = homedir()) {
389
+ export function applyMcpRemovePlan(plan, homeDir = homedir(), backup) {
341
390
  if (plan.action !== "removed")
342
391
  return { ok: true };
343
- return removeServerYaml(serversYamlPath(homeDir), plan.name);
392
+ return removeServerYaml(serversYamlPath(homeDir), plan.name, backup);
393
+ }
394
+ /** See `applyMcpAddWithSync`'s doc comment — same split, same reason, same
395
+ * own-backup-session fix. */
396
+ export async function applyMcpRemoveWithSync(plan, opts = {}) {
397
+ const homeDir = opts.homeDir ?? homedir();
398
+ const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "removed" ? openBackupSession(homeDir, "mcp-remove") : undefined;
399
+ const session = opts.backupSession ?? ownSession;
400
+ let writeError;
401
+ if (!opts.dryRun && plan.action === "removed") {
402
+ const result = applyMcpRemovePlan(plan, homeDir, session);
403
+ if (!result.ok)
404
+ writeError = result.error;
405
+ }
406
+ ownSession?.finalize();
407
+ const planFailed = plan.action === "not-found" || Boolean(writeError);
408
+ let syncReport;
409
+ if (!planFailed && existsSync(join(homeDir, ".trellis"))) {
410
+ syncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun });
411
+ }
412
+ return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
344
413
  }
345
- export function runMcpRemove(name, opts = {}) {
414
+ export async function runMcpRemove(name, opts = {}) {
346
415
  const homeDir = opts.homeDir ?? homedir();
347
416
  let plan;
348
417
  try {
@@ -352,14 +421,19 @@ export function runMcpRemove(name, opts = {}) {
352
421
  console.error(err instanceof Error ? err.message : String(err));
353
422
  return { exitCode: 1 };
354
423
  }
355
- let writeError;
356
- if (!opts.dryRun && plan.action === "removed") {
357
- const result = applyMcpRemovePlan(plan, homeDir);
358
- if (!result.ok)
359
- writeError = result.error;
424
+ let outcome;
425
+ try {
426
+ outcome = await applyMcpRemoveWithSync(plan, { homeDir, dryRun: opts.dryRun });
360
427
  }
428
+ catch (err) {
429
+ console.error(err instanceof Error ? err.message : String(err));
430
+ return { exitCode: 1 };
431
+ }
432
+ const { writeError, sync: syncReport } = outcome;
433
+ const planFailed = plan.action === "not-found" || Boolean(writeError);
361
434
  if (opts.json) {
362
- console.log(JSON.stringify(writeError ? { ...plan, writeError } : plan, null, 2));
435
+ const payload = writeError ? { ...plan, writeError } : plan;
436
+ console.log(JSON.stringify(syncReport ? { ...payload, sync: syncReport } : payload, null, 2));
363
437
  }
364
438
  else if (plan.action === "not-found") {
365
439
  console.error(`"${name}" is not a canonical MCP server — nothing to remove.`);
@@ -369,6 +443,9 @@ export function runMcpRemove(name, opts = {}) {
369
443
  }
370
444
  else {
371
445
  console.log(`${opts.dryRun ? "[dry run] " : ""}removed MCP server "${name}" from canonical source.`);
446
+ if (syncReport)
447
+ printReport(syncReport, opts.dryRun ?? false);
372
448
  }
373
- return { exitCode: plan.action === "not-found" || writeError ? 1 : 0 };
449
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
450
+ return { exitCode: planFailed || syncConflict ? 1 : 0 };
374
451
  }
@@ -24,6 +24,7 @@ import { BuiltinRegistry, createRuntimeServer, UpstreamProvider } from "../lib/m
24
24
  import { McpStatusProvider } from "../lib/mcpStatusProvider.js";
25
25
  import { RuntimeControlProvider } from "../lib/runtimeControlProvider.js";
26
26
  import { TaskProvider } from "../lib/taskProvider.js";
27
+ import { AgentBridgeProvider } from "../lib/agentBridgeProvider.js";
27
28
  import { RuntimeMemoryProvider } from "../lib/memoryProvider.js";
28
29
  import { SkillProvider } from "../lib/skillProvider.js";
29
30
  import { ALL_AGENTS } from "../core/types.js";
@@ -121,7 +122,7 @@ async function runMcpEdge(opts, clientInfo, serverInfo) {
121
122
  // process has no terminal to run one in (design.md D9).
122
123
  homeDir,
123
124
  });
124
- const registry = new BuiltinRegistry([new SkillProvider(), new RuntimeMemoryProvider(), new RuntimeControlProvider(backend), new McpStatusProvider(backend), new TaskProvider(), new UpstreamProvider(backend)]);
125
+ const registry = new BuiltinRegistry([new SkillProvider(), new RuntimeMemoryProvider(), new RuntimeControlProvider(backend), new McpStatusProvider(backend), new TaskProvider(), new AgentBridgeProvider(), new UpstreamProvider(backend)]);
125
126
  const server = createRuntimeServer(serverInfo, { agentId: opts.agentId, homeDir }, registry);
126
127
  const transport = new StdioServerTransport();
127
128
  await server.connect(transport);
@@ -6,6 +6,7 @@
6
6
  * read-only; plans never carry secret values.
7
7
  */
8
8
  import { type BackupSession } from "../lib/backup.js";
9
+ import type { McpSyncReport } from "./mcp.js";
9
10
  export type McpImportAction = "create" | "already-present" | "duplicate" | "skipped" | "conflict" | "invalid-input";
10
11
  export interface McpImportItem {
11
12
  name: string;
@@ -18,10 +19,28 @@ export interface McpImportPlan {
18
19
  }
19
20
  export declare function collectMcpImportPlan(path: string, homeDir?: string): McpImportPlan;
20
21
  export declare function applyMcpImportPlan(path: string, homeDir?: string, backup?: BackupSession): McpImportPlan;
22
+ export interface McpImportOutcome {
23
+ plan: McpImportPlan;
24
+ sync?: McpSyncReport;
25
+ }
26
+ /**
27
+ * See `applyMcpAddWithSync`'s doc comment (mcp.ts) — same split, same
28
+ * reason: a non-CLI caller (trellis-gui's sidecar) gets the same
29
+ * structured result a JSON-mode CLI run prints, without scraping
30
+ * stdout or re-deriving the plan-then-apply-then-cascade-sync
31
+ * sequencing itself. Unlike `mcp add`/`mcp remove`, `applyMcpImportPlan`
32
+ * already opens its own backup session unconditionally (it always has,
33
+ * per design.md's original survey of `openBackupSession` call sites) —
34
+ * no tasks.md 3.5-style fix needed here.
35
+ */
36
+ export declare function applyMcpImportWithSync(path: string, opts?: {
37
+ homeDir?: string;
38
+ dryRun?: boolean;
39
+ }): Promise<McpImportOutcome>;
21
40
  export declare function runMcpImport(path: string | undefined, opts?: {
22
41
  homeDir?: string;
23
42
  json?: boolean;
24
43
  dryRun?: boolean;
25
- }): {
44
+ }): Promise<{
26
45
  exitCode: number;
27
- };
46
+ }>;
@@ -11,6 +11,7 @@ import { join } from "node:path";
11
11
  import { loadCanonicalSource, ensureGitignoreEntry, ensureShellEnvSource, upsertServerYaml, writeSecretsPolicyExtraction } from "../core/canonical.js";
12
12
  import { openBackupSession } from "../lib/backup.js";
13
13
  import { parseDotenv, writeLocalSecretValue } from "../lib/secretEnv.js";
14
+ import { collectMcpSyncReport, printReport as printMcpSyncReport } from "./mcp.js";
14
15
  const DEFAULT_ENV_FILE_TILDE = "~/.trellis/mcp/servers.local.env";
15
16
  class SkipImport extends Error {
16
17
  }
@@ -309,25 +310,47 @@ export function applyMcpImportPlan(path, homeDir = homedir(), backup) {
309
310
  ownSession.finalize();
310
311
  return publicPlan(plan);
311
312
  }
312
- export function runMcpImport(path, opts = {}) {
313
+ /**
314
+ * See `applyMcpAddWithSync`'s doc comment (mcp.ts) — same split, same
315
+ * reason: a non-CLI caller (trellis-gui's sidecar) gets the same
316
+ * structured result a JSON-mode CLI run prints, without scraping
317
+ * stdout or re-deriving the plan-then-apply-then-cascade-sync
318
+ * sequencing itself. Unlike `mcp add`/`mcp remove`, `applyMcpImportPlan`
319
+ * already opens its own backup session unconditionally (it always has,
320
+ * per design.md's original survey of `openBackupSession` call sites) —
321
+ * no tasks.md 3.5-style fix needed here.
322
+ */
323
+ export async function applyMcpImportWithSync(path, opts = {}) {
324
+ const homeDir = opts.homeDir ?? homedir();
325
+ const plan = opts.dryRun ? collectMcpImportPlan(path, homeDir) : applyMcpImportPlan(path, homeDir);
326
+ const failed = plan.items.some((item) => item.action === "conflict" || item.action === "invalid-input");
327
+ const changed = plan.items.some((item) => item.action === "create");
328
+ let syncReport;
329
+ if (!failed && changed && existsSync(join(homeDir, ".trellis"))) {
330
+ syncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun });
331
+ }
332
+ return { plan, ...(syncReport ? { sync: syncReport } : {}) };
333
+ }
334
+ export async function runMcpImport(path, opts = {}) {
313
335
  const homeDir = opts.homeDir ?? homedir();
314
336
  if (!path) {
315
337
  console.error("Usage: trellis mcp import <json-file> [--dry-run]");
316
338
  return { exitCode: 1 };
317
339
  }
318
340
  try {
319
- const internal = collectInternalPlan(path, homeDir);
320
- if (!opts.dryRun)
321
- applyMcpImportPlan(path, homeDir);
322
- const result = publicPlan(internal);
341
+ const { plan: result, sync: syncReport } = await applyMcpImportWithSync(path, { homeDir, dryRun: opts.dryRun });
342
+ const failed = result.items.some((item) => item.action === "conflict" || item.action === "invalid-input");
323
343
  if (opts.json)
324
- console.log(JSON.stringify(result, null, 2));
344
+ console.log(JSON.stringify(syncReport ? { ...result, sync: syncReport } : result, null, 2));
325
345
  else {
326
346
  console.log(`${opts.dryRun ? "[dry run] " : ""}mcp import ${path}`);
327
347
  for (const item of result.items)
328
348
  console.log(` [${item.action}] ${item.name} — ${item.detail}`);
349
+ if (syncReport)
350
+ printMcpSyncReport(syncReport, opts.dryRun ?? false);
329
351
  }
330
- return { exitCode: result.items.some((item) => item.action === "conflict" || item.action === "invalid-input") ? 1 : 0 };
352
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
353
+ return { exitCode: failed || syncConflict ? 1 : 0 };
331
354
  }
332
355
  catch (err) {
333
356
  console.error(err instanceof Error ? err.message : String(err));
@@ -12,9 +12,21 @@
12
12
  * server process — both are plain file operations, same posture as
13
13
  * every other Trellis write.
14
14
  */
15
- import type { McpConfig, McpServerDef } from "../core/types.js";
15
+ import type { AgentId, McpConfig, McpServerDef } from "../core/types.js";
16
16
  import type { BackupSession } from "../lib/backup.js";
17
17
  import { type MemoryExtractionPlan, type MemorySyncPlan } from "../lib/memoryGraph.js";
18
+ export interface MemoryListEntry {
19
+ name: string;
20
+ scope: readonly AgentId[];
21
+ }
22
+ /** Canonical `memories/*.md` profile files — the list `trellis-gui`'s
23
+ * Memory view reads (tasks.md 6.2), mirroring `collectSkillList`'s
24
+ * shape exactly (skill.ts) since both are "canonical files with a
25
+ * resolved managed-agent scope" lists. Distinct from this file's own
26
+ * `MEMORY_SERVER_NAME` sync/extraction pipeline below, which is about
27
+ * the separate shared-Memory-MCP-server knowledge graph, not these
28
+ * per-topic markdown files. */
29
+ export declare function collectMemoryList(homeDir?: string): MemoryListEntry[];
18
30
  /** Exported so `onboard` (`trellis onboard --memory on|off`,
19
31
  * trellis-onboard-mcp-mode) can look up and write this same entry
20
32
  * without hardcoding the name a second time. */
@@ -16,7 +16,22 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { homedir } from "node:os";
17
17
  import { dirname, join } from "node:path";
18
18
  import { loadCanonicalSource } from "../core/canonical.js";
19
+ import { resolveScope } from "../core/types.js";
19
20
  import { parseMemoryGraph, planMemoryExtraction, planMemorySync, renderMemoryGraph } from "../lib/memoryGraph.js";
21
+ /** Canonical `memories/*.md` profile files — the list `trellis-gui`'s
22
+ * Memory view reads (tasks.md 6.2), mirroring `collectSkillList`'s
23
+ * shape exactly (skill.ts) since both are "canonical files with a
24
+ * resolved managed-agent scope" lists. Distinct from this file's own
25
+ * `MEMORY_SERVER_NAME` sync/extraction pipeline below, which is about
26
+ * the separate shared-Memory-MCP-server knowledge graph, not these
27
+ * per-topic markdown files. */
28
+ export function collectMemoryList(homeDir = homedir()) {
29
+ const canonical = loadCanonicalSource(homeDir);
30
+ return canonical.memories.map((memory) => ({
31
+ name: memory.name,
32
+ scope: resolveScope(memory.scope, canonical.managedAgents),
33
+ }));
34
+ }
20
35
  /** Exported so `onboard` (`trellis onboard --memory on|off`,
21
36
  * trellis-onboard-mcp-mode) can look up and write this same entry
22
37
  * without hardcoding the name a second time. */
@@ -12,10 +12,11 @@ import * as codexProbe from "../probes/codex.js";
12
12
  import * as kiroProbe from "../probes/kiro.js";
13
13
  import * as piProbe from "../probes/pi.js";
14
14
  import * as kimiCodeProbe from "../probes/kimi-code.js";
15
+ import * as zcodeProbe from "../probes/zcode.js";
15
16
  import { AGENTS_MD_TEMPLATE } from "./init.js";
16
17
  import { decideDirImport } from "../lib/dirEquals.js";
17
18
  import { deepEqual } from "../lib/deepEqual.js";
18
- import { readClaudeCodeMcpDefs, readCodexMcpDefs, readKiroMcpDefs, readKimiCodeMcpDefs, resolvedEnvTextMap } from "../lib/mcpMigrateRead.js";
19
+ import { readClaudeCodeMcpDefs, readCodexMcpDefs, readKiroMcpDefs, readKimiCodeMcpDefs, readZcodeMcpDefs, resolvedEnvTextMap } from "../lib/mcpMigrateRead.js";
19
20
  import { parseDotenv, writeLocalSecretValue } from "../lib/secretEnv.js";
20
21
  import { findLiteralSecret } from "../adapters/mcpPlan.js";
21
22
  import { ensureGitignoreEntry, ensureShellEnvSource, loadCanonicalSource, upsertServerYaml, writeSecretsPolicyExtraction } from "../core/canonical.js";
@@ -51,6 +52,7 @@ const PROBES = {
51
52
  kiro: (homeDir) => kiroProbe.probe(homeDir),
52
53
  pi: (homeDir) => piProbe.probe(homeDir),
53
54
  "kimi-code": (homeDir) => kimiCodeProbe.probe(homeDir),
55
+ zcode: (homeDir) => zcodeProbe.probe(homeDir),
54
56
  };
55
57
  /** pi has no static MCP config to read at all (roadmap.md P14/
56
58
  * trellis-migrate-mcp-servers) — deliberately absent, not an oversight;
@@ -60,6 +62,7 @@ const MCP_READERS = {
60
62
  kiro: readKiroMcpDefs,
61
63
  codex: readCodexMcpDefs,
62
64
  "kimi-code": readKimiCodeMcpDefs,
65
+ zcode: readZcodeMcpDefs,
63
66
  };
64
67
  function planSkill(name, sourceDir, isSymlink, caseCorrect, canonicalDir) {
65
68
  if (isSymlink) {
@@ -23,6 +23,7 @@ import * as codexProbe from "../probes/codex.js";
23
23
  import * as kiroProbe from "../probes/kiro.js";
24
24
  import * as piProbe from "../probes/pi.js";
25
25
  import * as kimiCodeProbe from "../probes/kimi-code.js";
26
+ import * as zcodeProbe from "../probes/zcode.js";
26
27
  import { ALL_AGENTS, capabilityDeliveryForAgent } from "../core/types.js";
27
28
  import { loadCanonicalSource, removeServerYaml, upsertServerYaml, writeMcpModeYaml, writeMcpRoutesYaml, writeMcpRuntimeDeliveryYaml } from "../core/canonical.js";
28
29
  import { buildCapabilityInventory, parseCapabilitySelectionFile, resolveSelectedNames, selectionContains } from "../core/capabilitySelection.js";
@@ -47,6 +48,7 @@ const PROBES = {
47
48
  kiro: (homeDir) => kiroProbe.probe(homeDir),
48
49
  pi: (homeDir) => piProbe.probe(homeDir),
49
50
  "kimi-code": (homeDir) => kimiCodeProbe.probe(homeDir),
51
+ zcode: (homeDir) => zcodeProbe.probe(homeDir),
50
52
  };
51
53
  /**
52
54
  * Scores only real source-side content. The recommendation is a hint for a
@@ -824,14 +826,23 @@ export async function collectOnboardPlan(opts = {}) {
824
826
  if (!requestedRoutes && shouldPromptForRoutes) {
825
827
  requestedRoutes = await resolveInteractiveMcpRoutes(managedAgents, availableMcpNames, currentMcpAfterMigration.hub !== undefined);
826
828
  }
827
- const requestedRuntimeDelivery = capabilitySelection?.runtimeDelivery;
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
+ };
828
839
  let mcpSyncOverride;
829
- if ((requestedRoutes && Object.keys(requestedRoutes).length > 0) || (requestedRuntimeDelivery && Object.keys(requestedRuntimeDelivery).length > 0)) {
840
+ if ((requestedRoutes && Object.keys(requestedRoutes).length > 0) || Object.keys(requestedRuntimeDelivery).length > 0) {
830
841
  const currentMcp = currentMcpAfterMigration;
831
842
  const routes = requestedRoutes && Object.keys(requestedRoutes).length > 0
832
843
  ? { ...(currentMcp.routes ?? {}), ...requestedRoutes }
833
844
  : currentMcp.routes;
834
- const delivery = requestedRuntimeDelivery && Object.keys(requestedRuntimeDelivery).length > 0
845
+ const delivery = Object.keys(requestedRuntimeDelivery).length > 0
835
846
  ? { ...(currentMcp.runtime?.delivery ?? {}), ...requestedRuntimeDelivery }
836
847
  : currentMcp.runtime?.delivery;
837
848
  mcpSyncOverride = {
@@ -842,7 +853,7 @@ export async function collectOnboardPlan(opts = {}) {
842
853
  if (!opts.dryRun && requestedRoutes && Object.keys(requestedRoutes).length > 0) {
843
854
  writeMcpRoutesYaml(join(homeDir, ".trellis", "mcp", "servers.yaml"), routes ?? {}, backupSession);
844
855
  }
845
- if (!opts.dryRun && requestedRuntimeDelivery && Object.keys(requestedRuntimeDelivery).length > 0) {
856
+ if (!opts.dryRun && Object.keys(requestedRuntimeDelivery).length > 0) {
846
857
  writeMcpRuntimeDeliveryYaml(join(homeDir, ".trellis", "mcp", "servers.yaml"), delivery ?? {}, backupSession);
847
858
  }
848
859
  }
@@ -8,7 +8,7 @@
8
8
  * already hold themselves to for every other kind of conflict.
9
9
  */
10
10
  import { createHash } from "node:crypto";
11
- import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
11
+ import { cpSync, existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { readlink, rm, symlink } from "node:fs/promises";
13
13
  import { homedir } from "node:os";
14
14
  import { join } from "node:path";
@@ -99,6 +99,23 @@ async function planOperation(homeDir, runId, op) {
99
99
  }
100
100
  return { action: "restore", path: op.path, description: `recreate ${op.path} -> ${op.beforeLinkTarget}` };
101
101
  }
102
+ // Directory conflict detection is existence-only, not a deep content
103
+ // hash (see BackupOperation's "dir-create"/"dir-remove" doc comment
104
+ // in backup.ts) — a real, documented simplification, not an
105
+ // oversight: something touching a file inside an otherwise-untouched
106
+ // directory won't be caught as a conflict.
107
+ case "dir-create": {
108
+ if (!existsSync(op.path)) {
109
+ return { action: "already-reverted", path: op.path, description: `${op.path} no longer exists — nothing to undo` };
110
+ }
111
+ return { action: "restore", path: op.path, description: `delete ${op.path} (created by this run)` };
112
+ }
113
+ case "dir-remove": {
114
+ if (existsSync(op.path)) {
115
+ return { action: "conflict", path: op.path, description: `${op.path} exists again since this run — left untouched` };
116
+ }
117
+ return { action: "restore", path: op.path, description: `recreate ${op.path} from its content before this run (${join(runDir, op.beforeDir)})` };
118
+ }
102
119
  }
103
120
  }
104
121
  export async function collectRollbackPlan(homeDirInput, runIdInput) {
@@ -130,6 +147,12 @@ async function restoreOperation(homeDir, runId, op) {
130
147
  case "symlink-remove":
131
148
  await symlink(op.beforeLinkTarget, op.path);
132
149
  return;
150
+ case "dir-create":
151
+ rmSync(op.path, { recursive: true, force: true });
152
+ return;
153
+ case "dir-remove":
154
+ cpSync(join(runDir, op.beforeDir), op.path, { recursive: true });
155
+ return;
133
156
  }
134
157
  }
135
158
  export async function applyRollbackPlan(homeDir, runId, manifest, items) {