agent-trellis 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/README.md +45 -206
  2. package/dist/adapters/zcode.d.ts +21 -0
  3. package/dist/adapters/zcode.js +234 -0
  4. package/dist/cli.js +119 -10
  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/remoteSkill.d.ts +90 -0
  18. package/dist/commands/remoteSkill.js +392 -0
  19. package/dist/commands/rollback.js +55 -1
  20. package/dist/commands/secretsAudit.js +4 -1
  21. package/dist/commands/skill.d.ts +37 -6
  22. package/dist/commands/skill.js +73 -15
  23. package/dist/commands/sync.js +3 -1
  24. package/dist/core/adapter.d.ts +11 -1
  25. package/dist/core/canonical.d.ts +17 -1
  26. package/dist/core/canonical.js +54 -0
  27. package/dist/core/types.d.ts +1 -1
  28. package/dist/core/types.js +1 -0
  29. package/dist/lib/agentBridge.d.ts +169 -0
  30. package/dist/lib/agentBridge.js +367 -0
  31. package/dist/lib/agentBridgeProvider.d.ts +14 -0
  32. package/dist/lib/agentBridgeProvider.js +95 -0
  33. package/dist/lib/backup.d.ts +42 -0
  34. package/dist/lib/backup.js +36 -3
  35. package/dist/lib/chatAgents.d.ts +42 -0
  36. package/dist/lib/chatAgents.js +59 -0
  37. package/dist/lib/dirDigest.d.ts +11 -0
  38. package/dist/lib/dirDigest.js +52 -0
  39. package/dist/lib/envVarNames.d.ts +3 -0
  40. package/dist/lib/envVarNames.js +19 -0
  41. package/dist/lib/installAgent.d.ts +1 -1
  42. package/dist/lib/installAgent.js +1 -1
  43. package/dist/lib/mcpMigrateRead.d.ts +1 -0
  44. package/dist/lib/mcpMigrateRead.js +19 -1
  45. package/dist/lib/realHomeSnapshot.js +5 -0
  46. package/dist/lib/remoteSkillLock.d.ts +26 -0
  47. package/dist/lib/remoteSkillLock.js +88 -0
  48. package/dist/lib/remoteSkillSource.d.ts +45 -0
  49. package/dist/lib/remoteSkillSource.js +206 -0
  50. package/dist/lib/runtimeControlProvider.js +2 -0
  51. package/dist/lib/taskStore.js +1 -1
  52. package/dist/lib/terminalPicker.js +33 -6
  53. package/dist/lib/zcodeOwnership.d.ts +17 -0
  54. package/dist/lib/zcodeOwnership.js +37 -0
  55. package/dist/pi-bridge/bundle.js +2 -1
  56. package/dist/probes/codex.d.ts +7 -0
  57. package/dist/probes/codex.js +37 -5
  58. package/dist/probes/zcode.d.ts +32 -0
  59. package/dist/probes/zcode.js +140 -0
  60. package/docs/architecture.md +9 -5
  61. package/docs/assets/brand-v3/trellis-mark-v3-a.png +0 -0
  62. package/docs/assets/brand-v3/trellis-mark-v3-a.svg +10 -0
  63. package/docs/assets/poster-series-v3/README.md +60 -0
  64. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.png +0 -0
  65. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.svg +51 -0
  66. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.png +0 -0
  67. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.svg +51 -0
  68. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.png +0 -0
  69. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.svg +48 -0
  70. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.png +0 -0
  71. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.svg +48 -0
  72. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.png +0 -0
  73. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.svg +56 -0
  74. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.png +0 -0
  75. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.svg +56 -0
  76. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.png +0 -0
  77. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.svg +47 -0
  78. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.png +0 -0
  79. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.svg +47 -0
  80. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.png +0 -0
  81. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.svg +51 -0
  82. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.png +0 -0
  83. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.svg +51 -0
  84. package/docs/getting-started.md +244 -14
  85. package/docs/overview.md +6 -3
  86. package/docs/overview.zh-CN.md +5 -3
  87. package/docs/roadmap.md +2 -1
  88. package/docs/video/README.md +76 -0
  89. package/docs/video/build/audio/01-opening.aiff +0 -0
  90. package/docs/video/build/audio/01-opening.mp3 +0 -0
  91. package/docs/video/build/audio/02-pain.aiff +0 -0
  92. package/docs/video/build/audio/02-pain.mp3 +0 -0
  93. package/docs/video/build/audio/03-solution.aiff +0 -0
  94. package/docs/video/build/audio/03-solution.mp3 +0 -0
  95. package/docs/video/build/audio/04-install.aiff +0 -0
  96. package/docs/video/build/audio/04-install.mp3 +0 -0
  97. package/docs/video/build/audio/04-setup.mp3 +0 -0
  98. package/docs/video/build/audio/05-dryrun.mp3 +0 -0
  99. package/docs/video/build/audio/05-init.aiff +0 -0
  100. package/docs/video/build/audio/05-init.mp3 +0 -0
  101. package/docs/video/build/audio/05-interactive.mp3 +0 -0
  102. package/docs/video/build/audio/06-dryrun.aiff +0 -0
  103. package/docs/video/build/audio/06-dryrun.mp3 +0 -0
  104. package/docs/video/build/audio/06-onboard.mp3 +0 -0
  105. package/docs/video/build/audio/06-run.mp3 +0 -0
  106. package/docs/video/build/audio/07-onboard.aiff +0 -0
  107. package/docs/video/build/audio/07-onboard.mp3 +0 -0
  108. package/docs/video/build/audio/07-verify.mp3 +0 -0
  109. package/docs/video/build/audio/08-closing.mp3 +0 -0
  110. package/docs/video/build/audio/08-doctor.aiff +0 -0
  111. package/docs/video/build/audio/08-doctor.mp3 +0 -0
  112. package/docs/video/build/audio/09-rollback.aiff +0 -0
  113. package/docs/video/build/audio/09-rollback.mp3 +0 -0
  114. package/docs/video/build/audio/10-principles.aiff +0 -0
  115. package/docs/video/build/audio/10-principles.mp3 +0 -0
  116. package/docs/video/build/audio/11-closing.aiff +0 -0
  117. package/docs/video/build/audio/11-closing.mp3 +0 -0
  118. package/docs/video/build/frames/01-opening-0.png +0 -0
  119. package/docs/video/build/frames/02-pain-1.png +0 -0
  120. package/docs/video/build/frames/02-pain-2.png +0 -0
  121. package/docs/video/build/frames/03-solution-3.png +0 -0
  122. package/docs/video/build/frames/04-install-4.png +0 -0
  123. package/docs/video/build/frames/04-setup-4.png +0 -0
  124. package/docs/video/build/frames/04-setup-5.png +0 -0
  125. package/docs/video/build/frames/05-dryrun-6.png +0 -0
  126. package/docs/video/build/frames/05-init-5.png +0 -0
  127. package/docs/video/build/frames/05-interactive-5.png +0 -0
  128. package/docs/video/build/frames/05-interactive-6.png +0 -0
  129. package/docs/video/build/frames/06-dryrun-6.png +0 -0
  130. package/docs/video/build/frames/06-dryrun-7.png +0 -0
  131. package/docs/video/build/frames/06-onboard-7.png +0 -0
  132. package/docs/video/build/frames/06-onboard-8.png +0 -0
  133. package/docs/video/build/frames/06-run-7.png +0 -0
  134. package/docs/video/build/frames/06-run-8.png +0 -0
  135. package/docs/video/build/frames/07-onboard-10.png +0 -0
  136. package/docs/video/build/frames/07-onboard-7.png +0 -0
  137. package/docs/video/build/frames/07-onboard-8.png +0 -0
  138. package/docs/video/build/frames/07-onboard-9.png +0 -0
  139. package/docs/video/build/frames/07-verify-10.png +0 -0
  140. package/docs/video/build/frames/07-verify-8.png +0 -0
  141. package/docs/video/build/frames/07-verify-9.png +0 -0
  142. package/docs/video/build/frames/08-closing-10.png +0 -0
  143. package/docs/video/build/frames/08-closing-11.png +0 -0
  144. package/docs/video/build/frames/08-doctor-11.png +0 -0
  145. package/docs/video/build/frames/08-doctor-9.png +0 -0
  146. package/docs/video/build/frames/09-rollback-10.png +0 -0
  147. package/docs/video/build/frames/09-rollback-12.png +0 -0
  148. package/docs/video/build/frames/10-principles-11.png +0 -0
  149. package/docs/video/build/frames/10-principles-13.png +0 -0
  150. package/docs/video/build/frames/11-closing-12.png +0 -0
  151. package/docs/video/build/frames/11-closing-14.png +0 -0
  152. package/docs/video/build/segments/01-opening.mp4 +0 -0
  153. package/docs/video/build/segments/01-opening.txt +3 -0
  154. package/docs/video/build/segments/02-pain.mp4 +0 -0
  155. package/docs/video/build/segments/02-pain.txt +5 -0
  156. package/docs/video/build/segments/03-solution.mp4 +0 -0
  157. package/docs/video/build/segments/03-solution.txt +3 -0
  158. package/docs/video/build/segments/04-install.mp4 +0 -0
  159. package/docs/video/build/segments/04-install.txt +3 -0
  160. package/docs/video/build/segments/04-setup.mp4 +0 -0
  161. package/docs/video/build/segments/04-setup.txt +3 -0
  162. package/docs/video/build/segments/05-dryrun.mp4 +0 -0
  163. package/docs/video/build/segments/05-dryrun.txt +3 -0
  164. package/docs/video/build/segments/05-init.mp4 +0 -0
  165. package/docs/video/build/segments/05-init.txt +3 -0
  166. package/docs/video/build/segments/05-interactive.mp4 +0 -0
  167. package/docs/video/build/segments/05-interactive.txt +5 -0
  168. package/docs/video/build/segments/06-dryrun.mp4 +0 -0
  169. package/docs/video/build/segments/06-dryrun.txt +3 -0
  170. package/docs/video/build/segments/06-onboard.mp4 +0 -0
  171. package/docs/video/build/segments/06-onboard.txt +5 -0
  172. package/docs/video/build/segments/06-run.mp4 +0 -0
  173. package/docs/video/build/segments/06-run.txt +3 -0
  174. package/docs/video/build/segments/07-onboard.mp4 +0 -0
  175. package/docs/video/build/segments/07-onboard.txt +5 -0
  176. package/docs/video/build/segments/07-verify.mp4 +0 -0
  177. package/docs/video/build/segments/07-verify.txt +5 -0
  178. package/docs/video/build/segments/08-closing.mp4 +0 -0
  179. package/docs/video/build/segments/08-closing.txt +3 -0
  180. package/docs/video/build/segments/08-doctor.mp4 +0 -0
  181. package/docs/video/build/segments/08-doctor.txt +3 -0
  182. package/docs/video/build/segments/09-rollback.mp4 +0 -0
  183. package/docs/video/build/segments/09-rollback.txt +3 -0
  184. package/docs/video/build/segments/10-principles.mp4 +0 -0
  185. package/docs/video/build/segments/10-principles.txt +3 -0
  186. package/docs/video/build/segments/11-closing.mp4 +0 -0
  187. package/docs/video/build/segments/11-closing.txt +3 -0
  188. package/docs/video/build/segments/final.txt +8 -0
  189. package/docs/video/build-ai/audio/01-hook.mp3 +0 -0
  190. package/docs/video/build-ai/audio/02-pain.mp3 +0 -0
  191. package/docs/video/build-ai/audio/03-solution.mp3 +0 -0
  192. package/docs/video/build-ai/audio/04-how.mp3 +0 -0
  193. package/docs/video/build-ai/audio/05-safe.mp3 +0 -0
  194. package/docs/video/build-ai/audio/06-closing.mp3 +0 -0
  195. package/docs/video/build-ai/clips/01-hook.mp4 +0 -0
  196. package/docs/video/build-ai/clips/02-pain.mp4 +0 -0
  197. package/docs/video/build-ai/clips/03-solution.mp4 +0 -0
  198. package/docs/video/build-ai/clips/04-how.mp4 +0 -0
  199. package/docs/video/build-ai/clips/05-safe.mp4 +0 -0
  200. package/docs/video/build-ai/clips/06-closing.mp4 +0 -0
  201. package/docs/video/build-ai/first-frame-02-pain.png +0 -0
  202. package/docs/video/build-ai/first-frame-03-solution.png +0 -0
  203. package/docs/video/build-ai/first-frame-04-how.png +0 -0
  204. package/docs/video/build-ai/first-frame-05-safe.png +0 -0
  205. package/docs/video/build-ai/segments/01-hook.caption.txt +2 -0
  206. package/docs/video/build-ai/segments/01-hook.mp4 +0 -0
  207. package/docs/video/build-ai/segments/02-pain.mp4 +0 -0
  208. package/docs/video/build-ai/segments/03-solution.mp4 +0 -0
  209. package/docs/video/build-ai/segments/04-how.mp4 +0 -0
  210. package/docs/video/build-ai/segments/05-safe.mp4 +0 -0
  211. package/docs/video/build-ai/segments/06-closing.mp4 +0 -0
  212. package/docs/video/build-ai/segments/cap-01-hook.png +0 -0
  213. package/docs/video/build-ai/segments/cap-02-pain.png +0 -0
  214. package/docs/video/build-ai/segments/cap-03-solution.png +0 -0
  215. package/docs/video/build-ai/segments/cap-04-how.png +0 -0
  216. package/docs/video/build-ai/segments/cap-05-safe.png +0 -0
  217. package/docs/video/build-ai/segments/cap-06-closing.png +0 -0
  218. package/docs/video/build-ai/segments/final.txt +6 -0
  219. package/docs/video/build-posters/audio/01-hook.mp3 +0 -0
  220. package/docs/video/build-posters/audio/02-pain.mp3 +0 -0
  221. package/docs/video/build-posters/audio/03-solution.mp3 +0 -0
  222. package/docs/video/build-posters/audio/04-how.mp3 +0 -0
  223. package/docs/video/build-posters/audio/05-safe.mp3 +0 -0
  224. package/docs/video/build-posters/audio/06-closing.mp3 +0 -0
  225. package/docs/video/build-posters/cap-01-hook.png +0 -0
  226. package/docs/video/build-posters/cap-02-pain.png +0 -0
  227. package/docs/video/build-posters/cap-03-solution.png +0 -0
  228. package/docs/video/build-posters/cap-04-how.png +0 -0
  229. package/docs/video/build-posters/cap-05-safe.png +0 -0
  230. package/docs/video/build-posters/cap-06-closing.png +0 -0
  231. package/docs/video/build-posters/card-01-hook.png +0 -0
  232. package/docs/video/build-posters/card-06-closing.png +0 -0
  233. package/docs/video/build-posters/segments/01-hook.mp4 +0 -0
  234. package/docs/video/build-posters/segments/02-pain.mp4 +0 -0
  235. package/docs/video/build-posters/segments/03-solution.mp4 +0 -0
  236. package/docs/video/build-posters/segments/04-how.mp4 +0 -0
  237. package/docs/video/build-posters/segments/05-safe.mp4 +0 -0
  238. package/docs/video/build-posters/segments/06-closing.mp4 +0 -0
  239. package/docs/video/build-posters/segments/final.txt +6 -0
  240. package/docs/video/generate-video-ai.mjs +334 -0
  241. package/docs/video/generate-video-posters.mjs +204 -0
  242. package/docs/video/generate-video.mjs +426 -0
  243. package/docs/video/outline.zh-CN.md +102 -0
  244. package/docs/video/trellis-demo-video-ai.mp4 +0 -0
  245. package/docs/video/trellis-demo-video.mp4 +0 -0
  246. package/docs/video/voiceover.zh-CN.md +49 -0
  247. package/package.json +5 -2
  248. package/schema/servers.example.yaml +1 -1
  249. package/docs/assets/agent-trellis-architecture-horizontal.svg +0 -80
  250. package/docs/assets/agent-trellis-architecture-vertical.svg +0 -82
  251. package/docs/assets/trellis-mark.svg +0 -12
@@ -0,0 +1,392 @@
1
+ /**
2
+ * `trellis add` and `trellis update`: GitHub-hosted Skill import with
3
+ * Trellis-owned canonical storage, provenance, scope, backup, and sync.
4
+ */
5
+ import { existsSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { validateSkillScopeYaml, writeSkillScopeYaml } from "../core/canonical.js";
9
+ import { loadCanonicalSource } from "../core/canonical.js";
10
+ import { ALL_AGENTS } from "../core/types.js";
11
+ import { decideDirImport } from "../lib/dirEquals.js";
12
+ import { directoryDigest } from "../lib/dirDigest.js";
13
+ import { openBackupSession } from "../lib/backup.js";
14
+ import { isBuiltinSkillName } from "../lib/builtinSkills.js";
15
+ import { emptyRemoteSkillLock, readRemoteSkillLock, writeRemoteSkillLock } from "../lib/remoteSkillLock.js";
16
+ import { fetchRemoteRepository, normalizeGitHubRepository } from "../lib/remoteSkillSource.js";
17
+ import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
18
+ function safeSkillName(name) {
19
+ return typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name) && name !== "." && name !== "..";
20
+ }
21
+ function cloneLock(lock) {
22
+ return { version: lock.version, skills: Object.fromEntries(Object.entries(lock.skills).map(([name, entry]) => [name, { ...entry }])) };
23
+ }
24
+ function lockEntryFor(source, requestedRef, commit, selected) {
25
+ return { source, requestedRef, commit, subdirectory: selected.subdirectory, digest: selected.digest };
26
+ }
27
+ function sameLockEntry(a, b) {
28
+ return a !== undefined
29
+ && a.source === b.source
30
+ && a.requestedRef === b.requestedRef
31
+ && a.commit === b.commit
32
+ && a.subdirectory === b.subdirectory
33
+ && a.digest === b.digest;
34
+ }
35
+ function scopeForRequest(homeDir, agents) {
36
+ let canonical;
37
+ try {
38
+ canonical = loadCanonicalSource(homeDir);
39
+ }
40
+ catch (error) {
41
+ return { error: error instanceof Error ? error.message : String(error) };
42
+ }
43
+ if (!agents || agents.length === 0)
44
+ return { scope: undefined };
45
+ const values = agents.flatMap((value) => value.split(",").map((id) => id.trim()).filter(Boolean));
46
+ if (values.length === 0)
47
+ return { error: "--agent needs at least one Agent id" };
48
+ if (values.includes("*")) {
49
+ if (values.length !== 1)
50
+ return { error: '--agent "*" cannot be combined with named Agents' };
51
+ return { scope: [...canonical.managedAgents] };
52
+ }
53
+ const resolved = [];
54
+ for (const id of values) {
55
+ if (!ALL_AGENTS.includes(id))
56
+ return { error: `Unknown Agent id: ${id}` };
57
+ const agent = id;
58
+ if (!canonical.managedAgents.includes(agent))
59
+ return { error: `${agent} is not managed. Add it with \`trellis manage add ${agent}\` before targeting it.` };
60
+ if (!resolved.includes(agent))
61
+ resolved.push(agent);
62
+ }
63
+ return { scope: resolved };
64
+ }
65
+ /** Builds an import plan from an already-fetched source. Keeping fetching
66
+ * outside this function makes the real Git transport testable with a local
67
+ * bare repository while public CLI input remains GitHub-only. */
68
+ export function collectRemoteSkillAddPlan(fetched, name, opts = {}) {
69
+ const homeDir = opts.homeDir ?? homedir();
70
+ if (!safeSkillName(name))
71
+ return { name: name ?? "", action: "invalid-options", detail: "--skill must be a safe Skill directory name" };
72
+ if (isBuiltinSkillName(name))
73
+ return { name, action: "conflict", detail: `"${name}" is a Trellis package-owned Skill and cannot be replaced` };
74
+ const scopeResult = scopeForRequest(homeDir, opts.agents);
75
+ if (scopeResult.error)
76
+ return { name, action: "invalid-options", detail: scopeResult.error };
77
+ let selected;
78
+ try {
79
+ selected = fetched.select(name);
80
+ }
81
+ catch (error) {
82
+ return { name, action: "invalid-source", detail: error instanceof Error ? error.message : String(error) };
83
+ }
84
+ const lockRead = readRemoteSkillLock(homeDir);
85
+ if (!lockRead.ok)
86
+ return { name, action: "conflict", detail: lockRead.error };
87
+ const canonicalPath = join(homeDir, ".trellis", "skills", name);
88
+ const incoming = lockEntryFor(fetched.repository.url, fetched.requestedRef, fetched.commit, selected);
89
+ const existingLock = lockRead.lock.skills[name];
90
+ const decision = decideDirImport(selected.directory, canonicalPath);
91
+ if (decision === "conflict") {
92
+ return { name, action: "conflict", detail: `canonical skills/${name}/ already exists with different content — resolve it by hand`, canonicalPath, scope: scopeResult.scope };
93
+ }
94
+ if (decision === "already-present") {
95
+ if (!sameLockEntry(existingLock, incoming)) {
96
+ return { name, action: "conflict", detail: `canonical skills/${name}/ already exists without matching remote provenance — resolve it by hand`, canonicalPath, scope: scopeResult.scope };
97
+ }
98
+ return { name, action: "already-present", detail: "canonical content and remote provenance are already current", source: incoming.source, requestedRef: incoming.requestedRef, commit: incoming.commit, subdirectory: incoming.subdirectory, digest: incoming.digest, canonicalPath, scope: scopeResult.scope };
99
+ }
100
+ if (existingLock) {
101
+ return { name, action: "conflict", detail: `provenance already tracks ${name} but its canonical directory is absent — resolve it by hand`, canonicalPath, scope: scopeResult.scope };
102
+ }
103
+ return { name, action: "create", detail: `will import ${incoming.subdirectory} at ${incoming.commit}`, source: incoming.source, requestedRef: incoming.requestedRef, commit: incoming.commit, subdirectory: incoming.subdirectory, digest: incoming.digest, canonicalPath, scope: scopeResult.scope, sourceDir: selected.directory };
104
+ }
105
+ function successfulAdd(plan) {
106
+ return plan.action === "create" || plan.action === "already-present";
107
+ }
108
+ function displayAddPlan(plan) {
109
+ const { sourceDir: _sourceDir, ...safe } = plan;
110
+ return safe;
111
+ }
112
+ export async function applyRemoteSkillAddPlan(plan, opts = {}) {
113
+ const homeDir = opts.homeDir ?? homedir();
114
+ if (!successfulAdd(plan))
115
+ return { plan: displayAddPlan(plan) };
116
+ if (opts.dryRun)
117
+ return { plan: displayAddPlan(plan) };
118
+ const ownSession = !opts.backupSession ? openBackupSession(homeDir, "remote-skill-add") : undefined;
119
+ const session = opts.backupSession ?? ownSession;
120
+ if (plan.action === "create") {
121
+ const scopePath = join(homeDir, ".trellis", "scope.yaml");
122
+ const scopeValidation = validateSkillScopeYaml(scopePath);
123
+ if (!scopeValidation.ok)
124
+ throw new Error(scopeValidation.error);
125
+ const lockRead = readRemoteSkillLock(homeDir);
126
+ if (!lockRead.ok)
127
+ throw new Error(lockRead.error);
128
+ session.createDirFromSource(plan.canonicalPath, plan.sourceDir);
129
+ const scopeWrite = writeSkillScopeYaml(scopePath, plan.name, plan.scope, session);
130
+ if (!scopeWrite.ok)
131
+ throw new Error(scopeWrite.error);
132
+ const lock = cloneLock(lockRead.lock);
133
+ lock.skills[plan.name] = {
134
+ source: plan.source, requestedRef: plan.requestedRef, commit: plan.commit, subdirectory: plan.subdirectory, digest: plan.digest,
135
+ };
136
+ writeRemoteSkillLock(homeDir, lock, session);
137
+ }
138
+ else {
139
+ // An idempotent add may still deliberately change scope. The writer is
140
+ // no-op when it already matches, so it does not create a needless backup.
141
+ const scopeWrite = writeSkillScopeYaml(join(homeDir, ".trellis", "scope.yaml"), plan.name, plan.scope, session);
142
+ if (!scopeWrite.ok)
143
+ throw new Error(scopeWrite.error);
144
+ }
145
+ ownSession?.finalize();
146
+ let sync;
147
+ if (session.hasOperations() && existsSync(join(homeDir, ".trellis"))) {
148
+ sync = await collectSyncReport({ target: "skills", homeDir });
149
+ }
150
+ return { plan: displayAddPlan(plan), ...(sync ? { sync } : {}) };
151
+ }
152
+ /** CLI-facing remote add. The only source accepted here is normalized
153
+ * GitHub input; tests use collectRemoteSkillAddPlan with a local fetch. */
154
+ export async function runRemoteSkillAdd(sourceInput, name, opts = {}) {
155
+ let repository;
156
+ try {
157
+ repository = normalizeGitHubRepository(sourceInput ?? "");
158
+ }
159
+ catch (error) {
160
+ const plan = { name: name ?? "", action: "invalid-source", detail: error instanceof Error ? error.message : String(error) };
161
+ if (opts.json)
162
+ console.log(JSON.stringify(displayAddPlan(plan), null, 2));
163
+ else
164
+ console.error(plan.detail);
165
+ return { exitCode: 1 };
166
+ }
167
+ let fetched;
168
+ try {
169
+ fetched = fetchRemoteRepository(repository, { branch: opts.branch });
170
+ }
171
+ catch (error) {
172
+ const plan = { name: name ?? "", action: "invalid-source", detail: error instanceof Error ? error.message : String(error), source: repository.url };
173
+ if (opts.json)
174
+ console.log(JSON.stringify(displayAddPlan(plan), null, 2));
175
+ else
176
+ console.error(plan.detail);
177
+ return { exitCode: 1 };
178
+ }
179
+ try {
180
+ const plan = collectRemoteSkillAddPlan(fetched, name, opts);
181
+ const outcome = await applyRemoteSkillAddPlan(plan, { homeDir: opts.homeDir, dryRun: opts.dryRun });
182
+ if (opts.json) {
183
+ console.log(JSON.stringify(outcome, null, 2));
184
+ }
185
+ else {
186
+ console.log(`${opts.dryRun ? "[dry run] " : ""}remote skill add ${plan.name}`);
187
+ console.log(` [${plan.action}] ${plan.detail}`);
188
+ if (plan.source)
189
+ console.log(` source: ${plan.source}@${plan.commit} (${plan.subdirectory})`);
190
+ if (plan.canonicalPath)
191
+ console.log(` canonical: ${plan.canonicalPath}`);
192
+ if (plan.scope !== undefined)
193
+ console.log(` scope: ${plan.scope.join(", ") || "(no managed agents)"}`);
194
+ if (outcome.sync)
195
+ printSyncReport(outcome.sync, false);
196
+ }
197
+ const syncConflict = outcome.sync?.reports.some((report) => report.items.some((item) => item.action === "conflict")) ?? false;
198
+ return { exitCode: successfulAdd(plan) && !syncConflict ? 0 : 1 };
199
+ }
200
+ finally {
201
+ fetched.dispose();
202
+ }
203
+ }
204
+ export async function runRemoteSkillList(sourceInput, opts = {}) {
205
+ let repository;
206
+ try {
207
+ repository = normalizeGitHubRepository(sourceInput ?? "");
208
+ }
209
+ catch (error) {
210
+ const detail = error instanceof Error ? error.message : String(error);
211
+ if (opts.json)
212
+ console.log(JSON.stringify({ action: "invalid-source", detail }, null, 2));
213
+ else
214
+ console.error(detail);
215
+ return { exitCode: 1 };
216
+ }
217
+ try {
218
+ const fetched = fetchRemoteRepository(repository, { branch: opts.branch });
219
+ try {
220
+ const report = { source: repository.url, requestedRef: fetched.requestedRef, commit: fetched.commit, skills: fetched.candidates.map(({ name, subdirectory }) => ({ name, subdirectory })) };
221
+ if (opts.json)
222
+ console.log(JSON.stringify(report, null, 2));
223
+ else if (report.skills.length === 0)
224
+ console.log("No exact-case SKILL.md entries found in the remote source.");
225
+ else
226
+ for (const skill of report.skills)
227
+ console.log(`${skill.name} — ${skill.subdirectory}`);
228
+ return { exitCode: 0 };
229
+ }
230
+ finally {
231
+ fetched.dispose();
232
+ }
233
+ }
234
+ catch (error) {
235
+ const detail = error instanceof Error ? error.message : String(error);
236
+ if (opts.json)
237
+ console.log(JSON.stringify({ action: "invalid-source", detail, source: repository.url }, null, 2));
238
+ else
239
+ console.error(detail);
240
+ return { exitCode: 1 };
241
+ }
242
+ }
243
+ function displayUpdatePlan(plan) {
244
+ const { sourceDir: _sourceDir, ...safe } = plan;
245
+ return safe;
246
+ }
247
+ export function collectRemoteSkillUpdatePlan(name, fetched, homeDir = homedir()) {
248
+ const lockRead = readRemoteSkillLock(homeDir);
249
+ if (!lockRead.ok)
250
+ return { name, action: "conflict", detail: lockRead.error };
251
+ const entry = lockRead.lock.skills[name];
252
+ if (!entry)
253
+ return { name, action: "not-tracked", detail: `${name} is not a remotely tracked Skill` };
254
+ const canonicalPath = join(homeDir, ".trellis", "skills", name);
255
+ if (!existsSync(canonicalPath))
256
+ return { name, action: "conflict", detail: `canonical skills/${name}/ is missing while provenance remains`, canonicalPath };
257
+ let currentDigest;
258
+ try {
259
+ currentDigest = directoryDigest(canonicalPath);
260
+ }
261
+ catch (error) {
262
+ return { name, action: "conflict", detail: `could not read canonical skills/${name}/ safely: ${error instanceof Error ? error.message : String(error)}`, canonicalPath };
263
+ }
264
+ if (currentDigest !== entry.digest) {
265
+ return { name, action: "conflict", detail: `canonical skills/${name}/ has local edits and will not be overwritten`, source: entry.source, requestedRef: entry.requestedRef, previousCommit: entry.commit, previousDigest: entry.digest, canonicalPath };
266
+ }
267
+ if (!fetched)
268
+ return { name, action: "invalid-source", detail: "Remote source was not fetched", source: entry.source, requestedRef: entry.requestedRef, previousCommit: entry.commit, previousDigest: entry.digest, canonicalPath };
269
+ let selected;
270
+ try {
271
+ selected = fetched.select(name);
272
+ }
273
+ catch (error) {
274
+ return { name, action: "invalid-source", detail: error instanceof Error ? error.message : String(error), source: entry.source, requestedRef: entry.requestedRef, previousCommit: entry.commit, previousDigest: entry.digest, canonicalPath };
275
+ }
276
+ if (selected.subdirectory !== entry.subdirectory) {
277
+ return { name, action: "conflict", detail: `remote Skill ${name} moved from ${entry.subdirectory} to ${selected.subdirectory}; resolve provenance by hand`, source: entry.source, requestedRef: entry.requestedRef, previousCommit: entry.commit, previousDigest: entry.digest, canonicalPath };
278
+ }
279
+ if (fetched.commit === entry.commit && selected.digest === entry.digest) {
280
+ return { name, action: "already-current", detail: "remote commit and canonical content are already current", source: entry.source, requestedRef: entry.requestedRef, previousCommit: entry.commit, commit: fetched.commit, previousDigest: entry.digest, digest: selected.digest, canonicalPath };
281
+ }
282
+ return { name, action: "update", detail: `will update ${entry.commit} -> ${fetched.commit}`, source: entry.source, requestedRef: entry.requestedRef, previousCommit: entry.commit, commit: fetched.commit, previousDigest: entry.digest, digest: selected.digest, canonicalPath, sourceDir: selected.directory, replaceDirectory: selected.digest !== entry.digest };
283
+ }
284
+ export async function applyRemoteSkillUpdatePlans(plans, opts = {}) {
285
+ const homeDir = opts.homeDir ?? homedir();
286
+ const updates = plans.filter((plan) => plan.action === "update");
287
+ if (opts.dryRun || updates.length === 0)
288
+ return { plans: plans.map(displayUpdatePlan) };
289
+ const ownSession = !opts.backupSession ? openBackupSession(homeDir, "remote-skill-update") : undefined;
290
+ const session = opts.backupSession ?? ownSession;
291
+ const lockRead = readRemoteSkillLock(homeDir);
292
+ if (!lockRead.ok)
293
+ throw new Error(lockRead.error);
294
+ const lock = cloneLock(lockRead.lock);
295
+ // Recheck all mutable inputs before the first replacement. The plans may
296
+ // have been previewed some time before application, so no remote update
297
+ // should proceed if either the lock or canonical directory changed.
298
+ for (const plan of updates) {
299
+ const current = lock.skills[plan.name];
300
+ if (!current || current.commit !== plan.previousCommit || current.digest !== plan.previousDigest) {
301
+ throw new Error(`Remote Skill ${plan.name} provenance changed after planning; run \`trellis update\` again.`);
302
+ }
303
+ if (directoryDigest(plan.canonicalPath) !== plan.previousDigest) {
304
+ throw new Error(`Canonical Skill ${plan.name} changed after planning; run \`trellis update\` again.`);
305
+ }
306
+ }
307
+ for (const plan of updates) {
308
+ if (plan.replaceDirectory)
309
+ session.replaceDirFromSource(plan.canonicalPath, plan.sourceDir);
310
+ lock.skills[plan.name] = { source: plan.source, requestedRef: plan.requestedRef, commit: plan.commit, subdirectory: lock.skills[plan.name].subdirectory, digest: plan.digest };
311
+ }
312
+ writeRemoteSkillLock(homeDir, lock, session);
313
+ ownSession?.finalize();
314
+ let sync;
315
+ if (session.hasOperations() && existsSync(join(homeDir, ".trellis")))
316
+ sync = await collectSyncReport({ target: "skills", homeDir });
317
+ return { plans: plans.map(displayUpdatePlan), ...(sync ? { sync } : {}) };
318
+ }
319
+ export async function runRemoteSkillUpdate(names, opts = {}) {
320
+ const homeDir = opts.homeDir ?? homedir();
321
+ const lockRead = readRemoteSkillLock(homeDir);
322
+ const selectedNames = names.length > 0 ? [...new Set(names)] : lockRead.ok ? Object.keys(lockRead.lock.skills).sort() : [];
323
+ if (!lockRead.ok) {
324
+ const report = [{ name: "", action: "conflict", detail: lockRead.error }];
325
+ if (opts.json)
326
+ console.log(JSON.stringify({ plans: report }, null, 2));
327
+ else
328
+ console.error(lockRead.error);
329
+ return { exitCode: 1 };
330
+ }
331
+ if (selectedNames.length === 0) {
332
+ const report = { plans: [] };
333
+ if (opts.json)
334
+ console.log(JSON.stringify(report, null, 2));
335
+ else
336
+ console.log("No remotely tracked Skills to update.");
337
+ return { exitCode: 0 };
338
+ }
339
+ const fetched = new Map();
340
+ const plans = [];
341
+ try {
342
+ for (const name of selectedNames) {
343
+ const entry = lockRead.lock.skills[name];
344
+ if (!entry) {
345
+ plans.push(collectRemoteSkillUpdatePlan(name, undefined, homeDir));
346
+ continue;
347
+ }
348
+ // Avoid network work when the canonical tree has been edited: its plan
349
+ // is already a safe conflict and must never be overwritten.
350
+ const preliminary = collectRemoteSkillUpdatePlan(name, undefined, homeDir);
351
+ if (preliminary.action === "conflict") {
352
+ plans.push(preliminary);
353
+ continue;
354
+ }
355
+ try {
356
+ const repository = fetchRemoteRepository({ url: entry.source }, { branch: entry.requestedRef });
357
+ fetched.set(name, repository);
358
+ plans.push(collectRemoteSkillUpdatePlan(name, repository, homeDir));
359
+ }
360
+ catch (error) {
361
+ plans.push({ name, action: "invalid-source", detail: error instanceof Error ? error.message : String(error), source: entry.source, requestedRef: entry.requestedRef, previousCommit: entry.commit, previousDigest: entry.digest, canonicalPath: join(homeDir, ".trellis", "skills", name) });
362
+ }
363
+ }
364
+ // Multiple requested updates behave as a safe batch: a bad tracked Skill
365
+ // does not leave the rest unexpectedly changed in the same command.
366
+ const blocked = plans.some((plan) => plan.action === "conflict" || plan.action === "not-tracked" || plan.action === "invalid-source");
367
+ const outcome = blocked ? { plans: plans.map(displayUpdatePlan) } : await applyRemoteSkillUpdatePlans(plans, { homeDir, dryRun: opts.dryRun });
368
+ if (opts.json) {
369
+ console.log(JSON.stringify(outcome, null, 2));
370
+ }
371
+ else {
372
+ if (opts.dryRun)
373
+ console.log("[dry run]");
374
+ for (const plan of plans)
375
+ console.log(`update ${plan.name}: [${plan.action}] ${plan.detail}`);
376
+ if (outcome.sync)
377
+ printSyncReport(outcome.sync, false);
378
+ }
379
+ const syncConflict = outcome.sync?.reports.some((report) => report.items.some((item) => item.action === "conflict")) ?? false;
380
+ return { exitCode: blocked || syncConflict ? 1 : 0 };
381
+ }
382
+ finally {
383
+ for (const repository of fetched.values())
384
+ repository.dispose();
385
+ }
386
+ }
387
+ /** Used by Skill listing to distinguish remote provenance from local content
388
+ * without making a malformed bookkeeping file crash a read-only list. */
389
+ export function remoteSkillProvenance(homeDir) {
390
+ const lock = readRemoteSkillLock(homeDir);
391
+ return lock.ok ? lock.lock.skills : emptyRemoteSkillLock().skills;
392
+ }
@@ -8,11 +8,12 @@
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";
15
15
  import { backupsRoot } from "../lib/backup.js";
16
+ import { directoryDigest } from "../lib/dirDigest.js";
16
17
  function sha256(content) {
17
18
  return `sha256:${createHash("sha256").update(content).digest("hex")}`;
18
19
  }
@@ -99,6 +100,49 @@ async function planOperation(homeDir, runId, op) {
99
100
  }
100
101
  return { action: "restore", path: op.path, description: `recreate ${op.path} -> ${op.beforeLinkTarget}` };
101
102
  }
103
+ // Historical `dir-remove` operations remain existence-only because they
104
+ // predate directory manifests. New directory creates/replacements carry a
105
+ // deterministic digest, so remote Skill rollback refuses an edited tree.
106
+ case "dir-create": {
107
+ if (!existsSync(op.path)) {
108
+ return { action: "already-reverted", path: op.path, description: `${op.path} no longer exists — nothing to undo` };
109
+ }
110
+ if (op.afterDigest !== undefined) {
111
+ let digest;
112
+ try {
113
+ digest = directoryDigest(op.path);
114
+ }
115
+ catch {
116
+ return { action: "conflict", path: op.path, description: `${op.path} can no longer be read safely — left untouched` };
117
+ }
118
+ if (digest !== op.afterDigest) {
119
+ return { action: "conflict", path: op.path, description: `${op.path} has changed since this run — left untouched` };
120
+ }
121
+ }
122
+ return { action: "restore", path: op.path, description: `delete ${op.path} (created by this run)` };
123
+ }
124
+ case "dir-remove": {
125
+ if (existsSync(op.path)) {
126
+ return { action: "conflict", path: op.path, description: `${op.path} exists again since this run — left untouched` };
127
+ }
128
+ return { action: "restore", path: op.path, description: `recreate ${op.path} from its content before this run (${join(runDir, op.beforeDir)})` };
129
+ }
130
+ case "dir-replace": {
131
+ if (!existsSync(op.path)) {
132
+ return { action: "already-reverted", path: op.path, description: `${op.path} no longer exists — nothing to undo` };
133
+ }
134
+ let digest;
135
+ try {
136
+ digest = directoryDigest(op.path);
137
+ }
138
+ catch {
139
+ return { action: "conflict", path: op.path, description: `${op.path} can no longer be read safely — left untouched` };
140
+ }
141
+ if (digest !== op.afterDigest) {
142
+ return { action: "conflict", path: op.path, description: `${op.path} has changed since this run — left untouched` };
143
+ }
144
+ return { action: "restore", path: op.path, description: `restore ${op.path} to its content before this run (${join(runDir, op.beforeDir)})` };
145
+ }
102
146
  }
103
147
  }
104
148
  export async function collectRollbackPlan(homeDirInput, runIdInput) {
@@ -130,6 +174,16 @@ async function restoreOperation(homeDir, runId, op) {
130
174
  case "symlink-remove":
131
175
  await symlink(op.beforeLinkTarget, op.path);
132
176
  return;
177
+ case "dir-create":
178
+ rmSync(op.path, { recursive: true, force: true });
179
+ return;
180
+ case "dir-remove":
181
+ cpSync(join(runDir, op.beforeDir), op.path, { recursive: true });
182
+ return;
183
+ case "dir-replace":
184
+ rmSync(op.path, { recursive: true, force: true });
185
+ cpSync(join(runDir, op.beforeDir), op.path, { recursive: true });
186
+ return;
133
187
  }
134
188
  }
135
189
  export async function applyRollbackPlan(homeDir, runId, manifest, items) {
@@ -26,7 +26,9 @@ import { loadCanonicalSource } from "../core/canonical.js";
26
26
  import { ClaudeCodeAdapter } from "../adapters/claude-code.js";
27
27
  import { CodexAdapter } from "../adapters/codex.js";
28
28
  import { KiroAdapter } from "../adapters/kiro.js";
29
- import { declaredEnvNames, extractJsonEnvVarNames, extractTomlEnvVarNames } from "../lib/envVarNames.js";
29
+ import { ZcodeAdapter } from "../adapters/zcode.js";
30
+ import { declaredEnvNames, extractJsonEnvVarNames, extractTomlEnvVarNames, extractZcodeEnvVarNames } from "../lib/envVarNames.js";
31
+ import { resolveZcodeProfile } from "../probes/zcode.js";
30
32
  import { resolveSecretEnv } from "../lib/secretEnv.js";
31
33
  /** Only agents in `managedAgents` — a present-but-unmanaged agent's config
32
34
  * file is never even read (trellis-managed-agents), same restriction as
@@ -38,6 +40,7 @@ function auditedAgents(homeDir, managedAgents) {
38
40
  { id: "claude-code", probe: () => new ClaudeCodeAdapter(homeDir).probe(), configPath: (h) => join(h, ".claude.json"), extractNames: extractJsonEnvVarNames },
39
41
  { id: "codex", probe: () => new CodexAdapter(homeDir).probe(), configPath: (h) => join(h, ".codex", "config.toml"), extractNames: extractTomlEnvVarNames },
40
42
  { id: "kiro", probe: () => new KiroAdapter(homeDir).probe(), configPath: (h) => join(h, ".kiro", "settings", "mcp.json"), extractNames: extractJsonEnvVarNames },
43
+ { id: "zcode", probe: () => new ZcodeAdapter(homeDir).probe(), configPath: (h) => resolveZcodeProfile(h)?.configPath ?? join(h, ".zcode", "cli", "config.json"), extractNames: extractZcodeEnvVarNames },
41
44
  ];
42
45
  return all.filter((a) => managedAgents.includes(a.id));
43
46
  }
@@ -9,6 +9,8 @@
9
9
  */
10
10
  import type { AgentId } from "../core/types.js";
11
11
  import { type BackupSession } from "../lib/backup.js";
12
+ import type { SyncReport } from "./sync.js";
13
+ import type { RemoteSkillLockEntry } from "../lib/remoteSkillLock.js";
12
14
  export type BuiltinSkillUpdateAction = "create" | "update" | "already-current";
13
15
  export interface BuiltinSkillUpdatePlan {
14
16
  name: "trellis-runtime";
@@ -28,6 +30,7 @@ export declare function runSkillUpdateBuiltin(opts?: {
28
30
  export interface SkillListEntry {
29
31
  name: string;
30
32
  scope: readonly AgentId[];
33
+ remote?: Pick<RemoteSkillLockEntry, "source" | "requestedRef" | "commit" | "subdirectory" | "digest">;
31
34
  }
32
35
  export declare function collectSkillList(homeDir?: string): SkillListEntry[];
33
36
  export declare function runSkillList(opts?: {
@@ -44,25 +47,53 @@ export interface SkillAddPlan {
44
47
  sourceDir?: string;
45
48
  }
46
49
  export declare function collectSkillAddPlan(name: string, fromPath: string, homeDir?: string): SkillAddPlan;
47
- export declare function applySkillAddPlan(plan: SkillAddPlan, homeDir?: string): void;
50
+ export declare function applySkillAddPlan(plan: SkillAddPlan, homeDir?: string, backup?: BackupSession): void;
51
+ export interface SkillAddOutcome {
52
+ plan: SkillAddPlan;
53
+ sync?: SyncReport;
54
+ }
55
+ /**
56
+ * See `applyMcpAddWithSync`'s doc comment (mcp.ts) — same split, same
57
+ * reason: a non-CLI caller needs the structured result a JSON-mode CLI
58
+ * run prints, without scraping stdout. Also opens its own backup session
59
+ * around the canonical copy-in, for the same reason `applyMcpAddWithSync`
60
+ * does (trellis-gui tasks.md 3.5): this direct write used to be the one
61
+ * path in this file with no backup/rollback trace at all.
62
+ */
63
+ export declare function applySkillAddWithSync(plan: SkillAddPlan, opts?: {
64
+ homeDir?: string;
65
+ dryRun?: boolean;
66
+ backupSession?: BackupSession;
67
+ }): Promise<SkillAddOutcome>;
48
68
  export declare function runSkillAdd(name: string, fromPath: string, opts?: {
49
69
  homeDir?: string;
50
70
  json?: boolean;
51
71
  dryRun?: boolean;
52
- }): {
72
+ }): Promise<{
53
73
  exitCode: number;
54
- };
74
+ }>;
55
75
  export type SkillRemoveAction = "removed" | "not-found";
56
76
  export interface SkillRemovePlan {
57
77
  name: string;
58
78
  action: SkillRemoveAction;
59
79
  }
60
80
  export declare function collectSkillRemovePlan(name: string, homeDir?: string): SkillRemovePlan;
61
- export declare function applySkillRemovePlan(plan: SkillRemovePlan, homeDir?: string): void;
81
+ export declare function applySkillRemovePlan(plan: SkillRemovePlan, homeDir?: string, backup?: BackupSession): void;
82
+ export interface SkillRemoveOutcome {
83
+ plan: SkillRemovePlan;
84
+ sync?: SyncReport;
85
+ }
86
+ /** See `applySkillAddWithSync`'s doc comment — same split, same reason,
87
+ * same own-backup-session fix. */
88
+ export declare function applySkillRemoveWithSync(plan: SkillRemovePlan, opts?: {
89
+ homeDir?: string;
90
+ dryRun?: boolean;
91
+ backupSession?: BackupSession;
92
+ }): Promise<SkillRemoveOutcome>;
62
93
  export declare function runSkillRemove(name: string, opts?: {
63
94
  homeDir?: string;
64
95
  json?: boolean;
65
96
  dryRun?: boolean;
66
- }): {
97
+ }): Promise<{
67
98
  exitCode: number;
68
- };
99
+ }>;