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
@@ -18,6 +18,8 @@ import { loadCanonicalSource } from "../core/canonical.js";
18
18
  import { resolveScope } from "../core/types.js";
19
19
  import { isBuiltinSkillName } from "../lib/builtinSkills.js";
20
20
  import { openBackupSession } from "../lib/backup.js";
21
+ import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
22
+ import { remoteSkillProvenance } from "./remoteSkill.js";
21
23
  function builtinSkillTemplate() {
22
24
  const here = dirname(fileURLToPath(import.meta.url));
23
25
  return readFileSync(join(here, "..", "..", "schema", "builtin-skills", "trellis-runtime", "SKILL.md"), "utf8");
@@ -56,9 +58,11 @@ export function runSkillUpdateBuiltin(opts = {}) {
56
58
  }
57
59
  export function collectSkillList(homeDir = homedir()) {
58
60
  const canonical = loadCanonicalSource(homeDir);
61
+ const provenance = remoteSkillProvenance(homeDir);
59
62
  return canonical.skills.filter((skill) => !isBuiltinSkillName(skill.name)).map((skill) => ({
60
63
  name: skill.name,
61
64
  scope: resolveScope(skill.scope, canonical.managedAgents),
65
+ ...(provenance[skill.name] ? { remote: provenance[skill.name] } : {}),
62
66
  }));
63
67
  }
64
68
  export function runSkillList(opts = {}) {
@@ -70,8 +74,9 @@ export function runSkillList(opts = {}) {
70
74
  console.log("No skills in canonical source yet.");
71
75
  }
72
76
  else {
73
- for (const { name, scope } of entries) {
74
- console.log(`${name} — ${scope.length > 0 ? scope.join(", ") : "(no managed agent reaches it)"}`);
77
+ for (const { name, scope, remote } of entries) {
78
+ const provenance = remote ? ` — remote: ${remote.source}@${remote.commit.slice(0, 12)}` : "";
79
+ console.log(`${name} — ${scope.length > 0 ? scope.join(", ") : "(no managed agent reaches it)"}${provenance}`);
75
80
  }
76
81
  }
77
82
  return { exitCode: 0 };
@@ -97,27 +102,56 @@ export function collectSkillAddPlan(name, fromPath, homeDir = homedir()) {
97
102
  return { name, action: "conflict", detail: `canonical skills/${name}/ already exists with different content — resolve by hand` };
98
103
  }
99
104
  }
100
- export function applySkillAddPlan(plan, homeDir = homedir()) {
105
+ export function applySkillAddPlan(plan, homeDir = homedir(), backup) {
101
106
  if (plan.action !== "create" || !plan.sourceDir)
102
107
  return;
103
108
  const dest = join(homeDir, ".trellis", "skills", plan.name);
109
+ if (backup) {
110
+ backup.createDirFromSource(dest, plan.sourceDir);
111
+ return;
112
+ }
104
113
  mkdirSync(dest, { recursive: true });
105
114
  cpSync(plan.sourceDir, dest, { recursive: true });
106
115
  }
107
- export function runSkillAdd(name, fromPath, opts = {}) {
116
+ /**
117
+ * See `applyMcpAddWithSync`'s doc comment (mcp.ts) — same split, same
118
+ * reason: a non-CLI caller needs the structured result a JSON-mode CLI
119
+ * run prints, without scraping stdout. Also opens its own backup session
120
+ * around the canonical copy-in, for the same reason `applyMcpAddWithSync`
121
+ * does (trellis-gui tasks.md 3.5): this direct write used to be the one
122
+ * path in this file with no backup/rollback trace at all.
123
+ */
124
+ export async function applySkillAddWithSync(plan, opts = {}) {
108
125
  const homeDir = opts.homeDir ?? homedir();
109
- const plan = collectSkillAddPlan(name, fromPath, homeDir);
126
+ const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "skill-add") : undefined;
127
+ const session = opts.backupSession ?? ownSession;
110
128
  if (!opts.dryRun && plan.action === "create") {
111
- applySkillAddPlan(plan, homeDir);
129
+ applySkillAddPlan(plan, homeDir, session);
112
130
  }
131
+ ownSession?.finalize();
132
+ const failed = plan.action === "conflict" || plan.action === "invalid-source";
133
+ let syncReport;
134
+ if (!failed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
135
+ syncReport = await collectSyncReport({ target: "skills", homeDir, dryRun: opts.dryRun });
136
+ }
137
+ return { plan, ...(syncReport ? { sync: syncReport } : {}) };
138
+ }
139
+ export async function runSkillAdd(name, fromPath, opts = {}) {
140
+ const homeDir = opts.homeDir ?? homedir();
141
+ const plan = collectSkillAddPlan(name, fromPath, homeDir);
142
+ const { sync: syncReport } = await applySkillAddWithSync(plan, { homeDir, dryRun: opts.dryRun });
143
+ const failed = plan.action === "conflict" || plan.action === "invalid-source";
113
144
  if (opts.json) {
114
- console.log(JSON.stringify(plan, null, 2));
145
+ console.log(JSON.stringify(syncReport ? { ...plan, sync: syncReport } : plan, null, 2));
115
146
  }
116
147
  else {
117
148
  console.log(`${opts.dryRun ? "[dry run] " : ""}skill add ${name}`);
118
149
  console.log(` [${plan.action}] ${plan.detail}`);
150
+ if (syncReport)
151
+ printSyncReport(syncReport, opts.dryRun ?? false);
119
152
  }
120
- return { exitCode: plan.action === "conflict" || plan.action === "invalid-source" ? 1 : 0 };
153
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
154
+ return { exitCode: failed || syncConflict ? 1 : 0 };
121
155
  }
122
156
  export function collectSkillRemovePlan(name, homeDir = homedir()) {
123
157
  if (isBuiltinSkillName(name))
@@ -125,25 +159,49 @@ export function collectSkillRemovePlan(name, homeDir = homedir()) {
125
159
  const dir = join(homeDir, ".trellis", "skills", name);
126
160
  return { name, action: existsSync(dir) ? "removed" : "not-found" };
127
161
  }
128
- export function applySkillRemovePlan(plan, homeDir = homedir()) {
162
+ export function applySkillRemovePlan(plan, homeDir = homedir(), backup) {
129
163
  if (plan.action !== "removed")
130
164
  return;
131
- rmSync(join(homeDir, ".trellis", "skills", plan.name), { recursive: true, force: true });
165
+ const dir = join(homeDir, ".trellis", "skills", plan.name);
166
+ if (backup) {
167
+ backup.removeDir(dir);
168
+ return;
169
+ }
170
+ rmSync(dir, { recursive: true, force: true });
132
171
  }
133
- export function runSkillRemove(name, opts = {}) {
172
+ /** See `applySkillAddWithSync`'s doc comment — same split, same reason,
173
+ * same own-backup-session fix. */
174
+ export async function applySkillRemoveWithSync(plan, opts = {}) {
134
175
  const homeDir = opts.homeDir ?? homedir();
135
- const plan = collectSkillRemovePlan(name, homeDir);
176
+ const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "removed" ? openBackupSession(homeDir, "skill-remove") : undefined;
177
+ const session = opts.backupSession ?? ownSession;
136
178
  if (!opts.dryRun) {
137
- applySkillRemovePlan(plan, homeDir);
179
+ applySkillRemovePlan(plan, homeDir, session);
180
+ }
181
+ ownSession?.finalize();
182
+ const failed = plan.action === "not-found";
183
+ let syncReport;
184
+ if (!failed && existsSync(join(homeDir, ".trellis"))) {
185
+ syncReport = await collectSyncReport({ target: "skills", homeDir, dryRun: opts.dryRun });
138
186
  }
187
+ return { plan, ...(syncReport ? { sync: syncReport } : {}) };
188
+ }
189
+ export async function runSkillRemove(name, opts = {}) {
190
+ const homeDir = opts.homeDir ?? homedir();
191
+ const plan = collectSkillRemovePlan(name, homeDir);
192
+ const { sync: syncReport } = await applySkillRemoveWithSync(plan, { homeDir, dryRun: opts.dryRun });
193
+ const failed = plan.action === "not-found";
139
194
  if (opts.json) {
140
- console.log(JSON.stringify(plan, null, 2));
195
+ console.log(JSON.stringify(syncReport ? { ...plan, sync: syncReport } : plan, null, 2));
141
196
  }
142
197
  else if (plan.action === "not-found") {
143
198
  console.error(`"${name}" is not a canonical skill — nothing to remove.`);
144
199
  }
145
200
  else {
146
201
  console.log(`${opts.dryRun ? "[dry run] " : ""}removed skill "${name}" from canonical source.`);
202
+ if (syncReport)
203
+ printSyncReport(syncReport, opts.dryRun ?? false);
147
204
  }
148
- return { exitCode: plan.action === "not-found" ? 1 : 0 };
205
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
206
+ return { exitCode: failed || syncConflict ? 1 : 0 };
149
207
  }
@@ -12,6 +12,7 @@ import { CodexAdapter } from "../adapters/codex.js";
12
12
  import { KiroAdapter } from "../adapters/kiro.js";
13
13
  import { PiAdapter } from "../adapters/pi.js";
14
14
  import { KimiCodeAdapter } from "../adapters/kimi-code.js";
15
+ import { ZcodeAdapter } from "../adapters/zcode.js";
15
16
  import { openBackupSession } from "../lib/backup.js";
16
17
  const ADAPTER_FACTORY = {
17
18
  "claude-code": (homeDir) => new ClaudeCodeAdapter(homeDir),
@@ -19,6 +20,7 @@ const ADAPTER_FACTORY = {
19
20
  kiro: (homeDir) => new KiroAdapter(homeDir),
20
21
  pi: (homeDir) => new PiAdapter(homeDir),
21
22
  "kimi-code": (homeDir) => new KimiCodeAdapter(homeDir),
23
+ zcode: (homeDir) => new ZcodeAdapter(homeDir),
22
24
  };
23
25
  /** Only agents in `canonical.managedAgents` — an agent present on this
24
26
  * machine but not managed gets no adapter at all, not a zero-item plan
@@ -60,7 +62,7 @@ export async function collectSyncReport(opts = {}) {
60
62
  // out (found via the sandbox: every agent reported "already in
61
63
  // sync" even with real create items pending).
62
64
  const kind = opts.target === "skills" ? "skill" : "instructions";
63
- items = items.filter((item) => item.kind === kind);
65
+ items = items.filter((item) => item.kind === kind || (kind === "skill" && item.kind === "zcode-skill-control"));
64
66
  }
65
67
  if (!opts.dryRun) {
66
68
  await adapter.apply(items, backup);
@@ -46,7 +46,7 @@ export interface AdapterPlanItem {
46
46
  * a completely different target file and merge mechanism (see
47
47
  * `approvedEnvVars` below) from `"mcp"`, not something the other
48
48
  * three agents have an equivalent of. */
49
- kind: "skill" | "instructions" | "mcp" | "extension" | "kiro-approved-env-vars";
49
+ kind: "skill" | "instructions" | "mcp" | "extension" | "kiro-approved-env-vars" | "zcode-skill-control";
50
50
  /** Human-readable description of one change this adapter would make
51
51
  * ("create" / "remove") or why it refused to ("conflict"). */
52
52
  description: string;
@@ -88,6 +88,16 @@ export interface AdapterPlanItem {
88
88
  * for Kiro, never a subtraction (trellis-kiro-approved-env-vars
89
89
  * design.md D3/D4). */
90
90
  approvedEnvVars?: string[];
91
+ /** ZCode Runtime-only isolation. `null` means the previous profile did
92
+ * not declare the value and restoration must remove the property. */
93
+ zcodeSkillControls?: {
94
+ featuresSkill: boolean | null;
95
+ skillsEnabled: boolean | null;
96
+ previous?: {
97
+ featuresSkill: boolean | null;
98
+ skillsEnabled: boolean | null;
99
+ };
100
+ };
91
101
  }
92
102
  export interface AdapterVerifyResult {
93
103
  ok: boolean;
@@ -4,7 +4,7 @@
4
4
  * workspace scope"). See openspec/changes/trellis-sync-p1/specs/
5
5
  * canonical-source-loading/spec.md for the exact contract this implements.
6
6
  */
7
- import type { AgentId, CapabilityDelivery, CanonicalSource, McpRoute, McpServerDef } from "./types.js";
7
+ import type { AgentId, CapabilityDelivery, CanonicalSource, McpRoute, McpServerDef, Scope } from "./types.js";
8
8
  import type { BackupSession } from "../lib/backup.js";
9
9
  /**
10
10
  * The on-disk shape for one server entry — `static_env`/`env_aliases`
@@ -28,6 +28,22 @@ export type ServersYamlWriteResult = {
28
28
  ok: false;
29
29
  error: string;
30
30
  };
31
+ export type ScopeYamlWriteResult = {
32
+ ok: true;
33
+ } | {
34
+ ok: false;
35
+ error: string;
36
+ };
37
+ /** Read-only preflight for a later scope edit. Remote import calls this
38
+ * before creating canonical content so an invalid hand-authored scope file
39
+ * cannot leave a half-applied import behind. */
40
+ export declare function validateSkillScopeYaml(path: string): ScopeYamlWriteResult;
41
+ /**
42
+ * Sets one Skill's canonical scope without rebuilding unrelated hand-authored
43
+ * scope entries. `undefined` removes this Skill's explicit entry and
44
+ * therefore restores the normal all-managed default.
45
+ */
46
+ export declare function writeSkillScopeYaml(path: string, name: string, scope: Scope, backup?: BackupSession): ScopeYamlWriteResult;
31
47
  export declare function upsertServerYaml(path: string, name: string, def: McpServerDef, backup?: BackupSession): ServersYamlWriteResult;
32
48
  /** Inverse of `upsertServerYaml` — same preservation guarantee, same
33
49
  * refusal posture on a missing/unparseable file. */
@@ -176,6 +176,60 @@ function persistText(path, content, backup) {
176
176
  else
177
177
  writeFileSync(path, content);
178
178
  }
179
+ /** Read-only preflight for a later scope edit. Remote import calls this
180
+ * before creating canonical content so an invalid hand-authored scope file
181
+ * cannot leave a half-applied import behind. */
182
+ export function validateSkillScopeYaml(path) {
183
+ if (!existsSync(path))
184
+ return { ok: true };
185
+ try {
186
+ const doc = parseDocument(readFileSync(path, "utf-8"));
187
+ if (doc.errors.length > 0)
188
+ throw new Error(doc.errors.map((error) => error.message).join("; "));
189
+ return { ok: true };
190
+ }
191
+ catch (err) {
192
+ return { ok: false, error: `could not parse ${path}: ${err instanceof Error ? err.message : String(err)}` };
193
+ }
194
+ }
195
+ /**
196
+ * Sets one Skill's canonical scope without rebuilding unrelated hand-authored
197
+ * scope entries. `undefined` removes this Skill's explicit entry and
198
+ * therefore restores the normal all-managed default.
199
+ */
200
+ export function writeSkillScopeYaml(path, name, scope, backup) {
201
+ if (!existsSync(path) && scope === undefined)
202
+ return { ok: true };
203
+ let doc;
204
+ try {
205
+ doc = existsSync(path) ? parseDocument(readFileSync(path, "utf-8")) : parseDocument("");
206
+ if (doc.errors.length > 0)
207
+ throw new Error(doc.errors.map((error) => error.message).join("; "));
208
+ }
209
+ catch (err) {
210
+ return { ok: false, error: `could not parse ${path}: ${err instanceof Error ? err.message : String(err)}` };
211
+ }
212
+ const current = doc.getIn(["skills", name], true);
213
+ if (scope === undefined) {
214
+ if (current === undefined)
215
+ return { ok: true };
216
+ doc.deleteIn(["skills", name]);
217
+ const skills = doc.get("skills", true);
218
+ if (isMap(skills) && skills.items.length === 0)
219
+ doc.delete("skills");
220
+ }
221
+ else {
222
+ const currentJson = current && typeof current === "object" && "toJSON" in current && typeof current.toJSON === "function" ? current.toJSON() : current;
223
+ if (Array.isArray(currentJson) && currentJson.length === scope.length && currentJson.every((agent, index) => agent === scope[index])) {
224
+ return { ok: true };
225
+ }
226
+ doc.setIn(["skills", name], [...scope]);
227
+ forceBlockStyle(doc.get("skills", true));
228
+ }
229
+ mkdirSync(dirname(path), { recursive: true });
230
+ persistText(path, doc.toString(), backup);
231
+ return { ok: true };
232
+ }
179
233
  export function upsertServerYaml(path, name, def, backup) {
180
234
  if (!existsSync(path)) {
181
235
  return { ok: false, error: `${path} does not exist — run \`trellis init\` first` };
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export type Transport = "stdio" | "http" | "sse";
8
8
  export type McpAuthMode = "oauth";
9
- export type AgentId = "claude-code" | "codex" | "kiro" | "pi" | "kimi-code";
9
+ export type AgentId = "claude-code" | "codex" | "kiro" | "pi" | "kimi-code" | "zcode";
10
10
  export declare const ALL_AGENTS: readonly AgentId[];
11
11
  /**
12
12
  * Every scopable item (skill, subagent, memory entry, MCP server) defaults
@@ -10,6 +10,7 @@ export const ALL_AGENTS = [
10
10
  "kiro",
11
11
  "pi",
12
12
  "kimi-code",
13
+ "zcode",
13
14
  ];
14
15
  /**
15
16
  * `managedAgents` is the hard outer boundary (trellis-managed-agents
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Cross-Agent delegation (trellis-agent-bridge): lets a managed Agent call
3
+ * another managed Agent's own verified non-interactive/headless mode as a
4
+ * one-shot subprocess call, so each Agent's strengths can be used without a
5
+ * central orchestrator or any cross-machine protocol.
6
+ *
7
+ * Configuration lives at `~/.trellis/mcp/agent-bridge.yaml`, sibling to
8
+ * `servers.yaml`, and is fully optional — its absence means zero behavior
9
+ * change (design.md Migration Plan).
10
+ */
11
+ import { type AgentId } from "../core/types.js";
12
+ export type AgentBridgeOutputFormat = "text" | "json" | "stream-json";
13
+ export type StreamProtocol = "claude" | "zcode";
14
+ export interface AgentBridgeTargetConfig {
15
+ /** Binary to spawn, e.g. "codex", "claude", "pi", "kimi". */
16
+ command: string;
17
+ /** Argv template for a fresh call. One element may be the literal
18
+ * placeholder `"{prompt}"`, substituted with the caller's prompt (and
19
+ * persona, if the target has no dedicated persona flag — see
20
+ * `PERSONA_PLACEHOLDER`) at call time — covers a trailing positional
21
+ * prompt (Codex, Claude Code, pi) and a flag-value prompt (Kimi Code's
22
+ * `-p, --prompt <prompt>`) with the same shape. */
23
+ args: string[];
24
+ /** Argv template used instead of `args` when the caller supplies a
25
+ * `sessionId` to resume — must reference `"{sessionId}"`. Omitting this
26
+ * means the target does not support resuming a prior session; a call
27
+ * that supplies `sessionId` against such a target fails with a clear
28
+ * error rather than silently starting a fresh session. */
29
+ resumeArgs?: string[];
30
+ /** How to interpret stdout. Omit for plain text. */
31
+ outputFormat?: AgentBridgeOutputFormat;
32
+ /** Selects a verified event envelope for stream-json output. Omit for
33
+ * Claude-compatible streams, which remains the existing default. */
34
+ streamProtocol?: StreamProtocol;
35
+ timeoutMs?: number;
36
+ /** Free-text, informational only — surfaced in the exposed tool's
37
+ * description so a caller can judge fit; never validated against a
38
+ * taxonomy. */
39
+ tags?: string[];
40
+ /** Default persona/role text applied to every call to this target
41
+ * unless a call overrides it. Substituted into `"{persona}"` when the
42
+ * chosen argv template has that placeholder; otherwise prepended to the
43
+ * prompt as a framing line, so every target gets persona support even
44
+ * without a native system-prompt flag. */
45
+ persona?: string;
46
+ }
47
+ export interface AgentBridgeConfig {
48
+ targets: Partial<Record<AgentId, AgentBridgeTargetConfig>>;
49
+ /** Maximum delegation chain depth before a call is refused, to bound
50
+ * A→B→A (or longer) cycles. Defaults to `DEFAULT_MAX_DELEGATION_DEPTH`. */
51
+ maxDepth?: number;
52
+ }
53
+ export declare const DEFAULT_DELEGATION_TIMEOUT_MS = 120000;
54
+ export declare const DEFAULT_MAX_DELEGATION_DEPTH = 2;
55
+ /** Carries the current delegation depth down the real process tree — the
56
+ * gateway that receives a delegated call inherits this from its spawning
57
+ * shell, so depth travels with the chain itself without a live shared
58
+ * registry (design.md Decision 3). */
59
+ export declare const DELEGATION_DEPTH_ENV = "TRELLIS_DELEGATION_DEPTH";
60
+ export declare function agentBridgePath(homeDir: string): string;
61
+ /** Reads the current chain depth from an environment map (defaults to
62
+ * `process.env`, injectable for tests). Anything absent or malformed
63
+ * reads as depth 0 — the safe "not part of a chain yet" default. */
64
+ export declare function readDelegationDepth(env?: NodeJS.ProcessEnv): number;
65
+ export declare function loadAgentBridgeConfig(homeDir: string): AgentBridgeConfig;
66
+ export interface DelegatedCallResult {
67
+ status: "completed" | "failed" | "timeout";
68
+ output: string;
69
+ durationMs: number;
70
+ targetAgent: AgentId;
71
+ /** Present when the target reported one (best-effort extraction from
72
+ * `json`/`stream-json` output). Pass back as `sessionId` in a later
73
+ * call to continue this exact conversation, if the target has
74
+ * `resumeArgs` configured. */
75
+ sessionId?: string;
76
+ }
77
+ export interface RunDelegatedCallOptions {
78
+ cwd?: string;
79
+ timeoutMs?: number;
80
+ /** Overrides the target's default `persona` for this call only. */
81
+ persona?: string;
82
+ /** Resume a prior conversation instead of starting a fresh one.
83
+ * Requires the target to have `resumeArgs` configured. */
84
+ sessionId?: string;
85
+ /** The chain depth *before* this call (0 for a call that isn't itself
86
+ * part of a delegation chain). The spawned child sees `depth + 1`. */
87
+ depth?: number;
88
+ }
89
+ /** Spawns the target's verified non-interactive invocation, enforces
90
+ * `timeoutMs` with SIGTERM-then-SIGKILL escalation, and normalizes the
91
+ * result into one envelope regardless of which CLI is underneath. Does
92
+ * NOT itself enforce a depth ceiling — refusing at the limit is the
93
+ * caller's job (AgentBridgeProvider), since only it knows the configured
94
+ * `maxDepth`; this function only propagates whatever depth it's given. */
95
+ export declare function runDelegatedCall(targetAgent: AgentId, target: AgentBridgeTargetConfig, prompt: string, opts?: RunDelegatedCallOptions): Promise<DelegatedCallResult>;
96
+ /** Best-effort structured interpretation of one line of a `stream-json`
97
+ * target's stdout, covering the four shapes documented by the community
98
+ * for Claude Code's non-interactive stream-json output (there is no
99
+ * official Anthropic schema — see upstream issue #24612): a `system/init`
100
+ * line, an `assistant` message carrying text or a `tool_use` block, a
101
+ * `user` message carrying a `tool_result` block, and the final `result`
102
+ * line. Anything that doesn't parse as JSON, or parses but doesn't match
103
+ * one of these shapes, returns `undefined` — the caller falls back to
104
+ * displaying the raw line rather than guessing. */
105
+ export type ParsedStreamEvent = {
106
+ kind: "init";
107
+ sessionId?: string;
108
+ } | {
109
+ kind: "text-delta";
110
+ text: string;
111
+ } | {
112
+ kind: "tool-call";
113
+ toolCallId: string;
114
+ name: string;
115
+ input: unknown;
116
+ } | {
117
+ kind: "tool-result";
118
+ toolCallId: string;
119
+ output: unknown;
120
+ } | {
121
+ kind: "final";
122
+ output: string;
123
+ sessionId?: string;
124
+ protocol?: "zcode";
125
+ };
126
+ export declare function tryParseClaudeStreamJsonLine(line: string): ParsedStreamEvent | undefined;
127
+ /** ZCode's documented headless result envelope is deliberately distinct from
128
+ * Claude-compatible streams: `{ type: "result", response, sessionId }`.
129
+ * Intermediate ZCode protocol events evolve with the runtime, so unknown
130
+ * records remain raw chunks rather than being guessed into false tool/text
131
+ * events. */
132
+ export declare function tryParseZcodeStreamJsonLine(line: string): ParsedStreamEvent | undefined;
133
+ export interface StreamChunk {
134
+ /** Raw text as received from the child, one line (no trailing newline). */
135
+ raw: string;
136
+ /** Present only when `outputFormat: "stream-json"` and the line matched
137
+ * a recognized Claude Code shape — see `tryParseClaudeStreamJsonLine`. */
138
+ parsed?: ParsedStreamEvent;
139
+ }
140
+ export interface DelegatedStreamResult {
141
+ status: "completed" | "failed" | "timeout" | "cancelled";
142
+ output: string;
143
+ durationMs: number;
144
+ /** Chat targets are identified by an arbitrary string id (`chat-agents.yaml`
145
+ * has no `AgentId` constraint — see `chatAgents.ts`), unlike
146
+ * `DelegatedCallResult.targetAgent` which is always a real managed Agent. */
147
+ targetAgent: string;
148
+ sessionId?: string;
149
+ }
150
+ export interface RunDelegatedCallStreamingOptions extends Omit<RunDelegatedCallOptions, "depth"> {
151
+ /** Called once per line of stdout, as it arrives — not batched until the
152
+ * process exits, unlike `runDelegatedCall`. */
153
+ onChunk: (chunk: StreamChunk) => void;
154
+ /** The chain depth before this call, same meaning as
155
+ * `RunDelegatedCallOptions.depth`. */
156
+ depth?: number;
157
+ /** Aborting triggers the same SIGTERM→SIGKILL escalation as a timeout;
158
+ * the returned result has `status: "cancelled"` instead of `"timeout"`. */
159
+ signal?: AbortSignal;
160
+ }
161
+ /** Streaming counterpart to `runDelegatedCall`: same argv-building,
162
+ * timeout escalation, and persona/session handling, but emits `onChunk`
163
+ * for every line of stdout as it arrives instead of buffering until the
164
+ * process exits, and supports cooperative cancellation via `signal`. Takes
165
+ * a plain `string` target id (not `AgentId`) so it can serve both a future
166
+ * streaming MCP delegation tool and today's GUI chat feature, whose
167
+ * targets come from `chat-agents.yaml` and are not required to be one of
168
+ * the fully-managed Agents in `ALL_AGENTS`. */
169
+ export declare function runDelegatedCallStreaming(targetAgent: string, target: AgentBridgeTargetConfig, prompt: string, opts: RunDelegatedCallStreamingOptions): Promise<DelegatedStreamResult>;