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,367 @@
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 { spawn } from "node:child_process";
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { parse as parseYaml } from "yaml";
15
+ import { ALL_AGENTS } from "../core/types.js";
16
+ export const DEFAULT_DELEGATION_TIMEOUT_MS = 120_000;
17
+ export const DEFAULT_MAX_DELEGATION_DEPTH = 2;
18
+ /** Carries the current delegation depth down the real process tree — the
19
+ * gateway that receives a delegated call inherits this from its spawning
20
+ * shell, so depth travels with the chain itself without a live shared
21
+ * registry (design.md Decision 3). */
22
+ export const DELEGATION_DEPTH_ENV = "TRELLIS_DELEGATION_DEPTH";
23
+ const PROMPT_PLACEHOLDER = "{prompt}";
24
+ const PERSONA_PLACEHOLDER = "{persona}";
25
+ const SESSION_PLACEHOLDER = "{sessionId}";
26
+ const FORCE_KILL_GRACE_MS = 5_000;
27
+ export function agentBridgePath(homeDir) {
28
+ return join(homeDir, ".trellis", "mcp", "agent-bridge.yaml");
29
+ }
30
+ /** Reads the current chain depth from an environment map (defaults to
31
+ * `process.env`, injectable for tests). Anything absent or malformed
32
+ * reads as depth 0 — the safe "not part of a chain yet" default. */
33
+ export function readDelegationDepth(env = process.env) {
34
+ const raw = env[DELEGATION_DEPTH_ENV];
35
+ const parsed = raw ? Number.parseInt(raw, 10) : 0;
36
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
37
+ }
38
+ const OUTPUT_FORMATS = ["text", "json", "stream-json"];
39
+ const STREAM_PROTOCOLS = ["claude", "zcode"];
40
+ export function loadAgentBridgeConfig(homeDir) {
41
+ const path = agentBridgePath(homeDir);
42
+ if (!existsSync(path))
43
+ return { targets: {} };
44
+ const parsed = (parseYaml(readFileSync(path, "utf-8")) ?? {});
45
+ if (parsed.maxDepth !== undefined && (!Number.isInteger(parsed.maxDepth) || parsed.maxDepth < 1)) {
46
+ throw new Error(`agent-bridge.yaml: "maxDepth" must be a positive integer`);
47
+ }
48
+ const targets = {};
49
+ for (const [agentId, raw] of Object.entries(parsed.targets ?? {})) {
50
+ if (!raw)
51
+ continue;
52
+ if (!ALL_AGENTS.includes(agentId)) {
53
+ throw new Error(`agent-bridge.yaml: "${agentId}" is not a supported Agent id`);
54
+ }
55
+ if (!raw.command || !Array.isArray(raw.args) || raw.args.length === 0) {
56
+ throw new Error(`agent-bridge.yaml: target "${agentId}" must declare "command" and a non-empty "args"`);
57
+ }
58
+ if (raw.resumeArgs !== undefined && (!Array.isArray(raw.resumeArgs) || raw.resumeArgs.length === 0)) {
59
+ throw new Error(`agent-bridge.yaml: target "${agentId}" has an invalid "resumeArgs"`);
60
+ }
61
+ if (raw.outputFormat !== undefined && !OUTPUT_FORMATS.includes(raw.outputFormat)) {
62
+ throw new Error(`agent-bridge.yaml: target "${agentId}" has unknown outputFormat "${raw.outputFormat}"`);
63
+ }
64
+ if (raw.streamProtocol !== undefined && !STREAM_PROTOCOLS.includes(raw.streamProtocol)) {
65
+ throw new Error(`agent-bridge.yaml: target "${agentId}" has unknown streamProtocol "${raw.streamProtocol}"`);
66
+ }
67
+ targets[agentId] = {
68
+ command: raw.command,
69
+ args: raw.args,
70
+ ...(raw.resumeArgs ? { resumeArgs: raw.resumeArgs } : {}),
71
+ ...(raw.outputFormat ? { outputFormat: raw.outputFormat } : {}),
72
+ ...(raw.streamProtocol ? { streamProtocol: raw.streamProtocol } : {}),
73
+ ...(raw.timeoutMs ? { timeoutMs: raw.timeoutMs } : {}),
74
+ ...(raw.tags ? { tags: raw.tags } : {}),
75
+ ...(raw.persona ? { persona: raw.persona } : {}),
76
+ };
77
+ }
78
+ return { targets, ...(parsed.maxDepth !== undefined ? { maxDepth: parsed.maxDepth } : {}) };
79
+ }
80
+ /** Shared SIGTERM→SIGKILL escalation used by both a timeout firing and a
81
+ * caller-initiated cancellation (streaming only) — kept as one function so
82
+ * the two call sites can never drift. */
83
+ function killWithEscalation(child) {
84
+ child.kill("SIGTERM");
85
+ const forceKill = setTimeout(() => child.kill("SIGKILL"), FORCE_KILL_GRACE_MS);
86
+ forceKill.unref?.();
87
+ }
88
+ function buildArgs(template, vars) {
89
+ return template.map((arg) => {
90
+ if (arg === PROMPT_PLACEHOLDER)
91
+ return vars.prompt;
92
+ if (arg === PERSONA_PLACEHOLDER)
93
+ return vars.persona ?? "";
94
+ if (arg === SESSION_PLACEHOLDER)
95
+ return vars.sessionId ?? "";
96
+ return arg;
97
+ });
98
+ }
99
+ /** Best-effort output extraction from a `json`/`stream-json` target's
100
+ * stdout: the response text plus, when present, a session id the caller
101
+ * can pass back as `sessionId` in a later call to resume this exact
102
+ * conversation. A parse failure or unrecognized shape degrades to the raw
103
+ * trimmed output with no session id, rather than throwing — a CLI's
104
+ * output-format change should surface as a stranger answer, not a crash
105
+ * (design.md Risks). */
106
+ function extractOutput(raw, outputFormat) {
107
+ const trimmed = raw.trim();
108
+ if (outputFormat !== "json" && outputFormat !== "stream-json")
109
+ return { output: trimmed };
110
+ const lines = trimmed.split("\n").filter((line) => line.trim().length > 0);
111
+ const lastLine = lines[lines.length - 1] ?? trimmed;
112
+ try {
113
+ const parsed = JSON.parse(lastLine);
114
+ if (typeof parsed === "string")
115
+ return { output: parsed };
116
+ if (parsed && typeof parsed === "object") {
117
+ const record = parsed;
118
+ let output;
119
+ for (const key of ["result", "output", "text", "response", "message"]) {
120
+ if (typeof record[key] === "string") {
121
+ output = record[key];
122
+ break;
123
+ }
124
+ }
125
+ let sessionId;
126
+ for (const key of ["session_id", "sessionId"]) {
127
+ if (typeof record[key] === "string") {
128
+ sessionId = record[key];
129
+ break;
130
+ }
131
+ }
132
+ return { output: output ?? JSON.stringify(parsed), ...(sessionId ? { sessionId } : {}) };
133
+ }
134
+ }
135
+ catch {
136
+ // fall through to raw text
137
+ }
138
+ return { output: trimmed };
139
+ }
140
+ /** Spawns the target's verified non-interactive invocation, enforces
141
+ * `timeoutMs` with SIGTERM-then-SIGKILL escalation, and normalizes the
142
+ * result into one envelope regardless of which CLI is underneath. Does
143
+ * NOT itself enforce a depth ceiling — refusing at the limit is the
144
+ * caller's job (AgentBridgeProvider), since only it knows the configured
145
+ * `maxDepth`; this function only propagates whatever depth it's given. */
146
+ export async function runDelegatedCall(targetAgent, target, prompt, opts) {
147
+ const timeoutMs = opts?.timeoutMs ?? target.timeoutMs ?? DEFAULT_DELEGATION_TIMEOUT_MS;
148
+ const persona = opts?.persona ?? target.persona;
149
+ let template = target.args;
150
+ if (opts?.sessionId) {
151
+ if (!target.resumeArgs) {
152
+ return { status: "failed", output: `agent "${targetAgent}" has no resumeArgs configured — cannot resume sessionId "${opts.sessionId}"`, durationMs: 0, targetAgent };
153
+ }
154
+ template = target.resumeArgs;
155
+ }
156
+ const hasPersonaSlot = template.includes(PERSONA_PLACEHOLDER);
157
+ const effectivePrompt = persona && !hasPersonaSlot ? `[Persona: ${persona}]\n\n${prompt}` : prompt;
158
+ const args = buildArgs(template, { prompt: effectivePrompt, persona, sessionId: opts?.sessionId });
159
+ const childEnv = { ...process.env, [DELEGATION_DEPTH_ENV]: String((opts?.depth ?? 0) + 1) };
160
+ const start = Date.now();
161
+ return new Promise((resolve) => {
162
+ let settled = false;
163
+ let stdout = "";
164
+ let stderr = "";
165
+ const child = spawn(target.command, args, { cwd: opts?.cwd, env: childEnv, stdio: ["ignore", "pipe", "pipe"] });
166
+ const killTimer = setTimeout(() => killWithEscalation(child), timeoutMs);
167
+ const finish = (value) => {
168
+ if (settled)
169
+ return;
170
+ settled = true;
171
+ clearTimeout(killTimer);
172
+ resolve(value);
173
+ };
174
+ child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf-8"); });
175
+ child.stderr?.on("data", (chunk) => { stderr += chunk.toString("utf-8"); });
176
+ child.on("error", (err) => finish({ status: "failed", output: err.message, durationMs: Date.now() - start, targetAgent }));
177
+ child.on("close", (code, signal) => {
178
+ const durationMs = Date.now() - start;
179
+ if (signal === "SIGTERM" || signal === "SIGKILL") {
180
+ finish({ status: "timeout", output: (stdout || stderr).trim(), durationMs, targetAgent });
181
+ return;
182
+ }
183
+ if (code !== 0) {
184
+ finish({ status: "failed", output: (stderr || stdout).trim(), durationMs, targetAgent });
185
+ return;
186
+ }
187
+ finish({ status: "completed", ...extractOutput(stdout, target.outputFormat), durationMs, targetAgent });
188
+ });
189
+ });
190
+ }
191
+ export function tryParseClaudeStreamJsonLine(line) {
192
+ const trimmed = line.trim();
193
+ if (!trimmed)
194
+ return undefined;
195
+ let json;
196
+ try {
197
+ json = JSON.parse(trimmed);
198
+ }
199
+ catch {
200
+ return undefined;
201
+ }
202
+ if (!json || typeof json !== "object")
203
+ return undefined;
204
+ const record = json;
205
+ if (record.type === "system" && record.subtype === "init") {
206
+ const sessionId = typeof record.session_id === "string" ? record.session_id : undefined;
207
+ return { kind: "init", ...(sessionId ? { sessionId } : {}) };
208
+ }
209
+ if (record.type === "result") {
210
+ const output = typeof record.result === "string" ? record.result : "";
211
+ const sessionId = typeof record.session_id === "string" ? record.session_id : undefined;
212
+ return { kind: "final", output, ...(sessionId ? { sessionId } : {}) };
213
+ }
214
+ if (record.type === "assistant" || record.type === "user") {
215
+ const message = record.message;
216
+ const content = Array.isArray(message?.content) ? message.content : undefined;
217
+ if (!content)
218
+ return undefined;
219
+ for (const block of content) {
220
+ if (!block || typeof block !== "object")
221
+ continue;
222
+ const b = block;
223
+ if (b.type === "text" && typeof b.text === "string")
224
+ return { kind: "text-delta", text: b.text };
225
+ if (b.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string") {
226
+ return { kind: "tool-call", toolCallId: b.id, name: b.name, input: b.input };
227
+ }
228
+ if (b.type === "tool_result" && typeof b.tool_use_id === "string") {
229
+ return { kind: "tool-result", toolCallId: b.tool_use_id, output: b.content };
230
+ }
231
+ }
232
+ return undefined;
233
+ }
234
+ return undefined;
235
+ }
236
+ /** ZCode's documented headless result envelope is deliberately distinct from
237
+ * Claude-compatible streams: `{ type: "result", response, sessionId }`.
238
+ * Intermediate ZCode protocol events evolve with the runtime, so unknown
239
+ * records remain raw chunks rather than being guessed into false tool/text
240
+ * events. */
241
+ export function tryParseZcodeStreamJsonLine(line) {
242
+ const trimmed = line.trim();
243
+ if (!trimmed)
244
+ return undefined;
245
+ let json;
246
+ try {
247
+ json = JSON.parse(trimmed);
248
+ }
249
+ catch {
250
+ return undefined;
251
+ }
252
+ if (!json || typeof json !== "object")
253
+ return undefined;
254
+ const record = json;
255
+ if (record.type !== "result" || typeof record.response !== "string")
256
+ return undefined;
257
+ const sessionId = typeof record.sessionId === "string" ? record.sessionId : undefined;
258
+ return { kind: "final", output: record.response, protocol: "zcode", ...(sessionId ? { sessionId } : {}) };
259
+ }
260
+ /** Streaming counterpart to `runDelegatedCall`: same argv-building,
261
+ * timeout escalation, and persona/session handling, but emits `onChunk`
262
+ * for every line of stdout as it arrives instead of buffering until the
263
+ * process exits, and supports cooperative cancellation via `signal`. Takes
264
+ * a plain `string` target id (not `AgentId`) so it can serve both a future
265
+ * streaming MCP delegation tool and today's GUI chat feature, whose
266
+ * targets come from `chat-agents.yaml` and are not required to be one of
267
+ * the fully-managed Agents in `ALL_AGENTS`. */
268
+ export async function runDelegatedCallStreaming(targetAgent, target, prompt, opts) {
269
+ const timeoutMs = opts.timeoutMs ?? target.timeoutMs ?? DEFAULT_DELEGATION_TIMEOUT_MS;
270
+ const persona = opts.persona ?? target.persona;
271
+ let template = target.args;
272
+ if (opts.sessionId) {
273
+ if (!target.resumeArgs) {
274
+ return {
275
+ status: "failed",
276
+ output: `agent "${targetAgent}" has no resumeArgs configured — cannot resume sessionId "${opts.sessionId}"`,
277
+ durationMs: 0,
278
+ targetAgent,
279
+ };
280
+ }
281
+ template = target.resumeArgs;
282
+ }
283
+ const hasPersonaSlot = template.includes(PERSONA_PLACEHOLDER);
284
+ const effectivePrompt = persona && !hasPersonaSlot ? `[Persona: ${persona}]\n\n${prompt}` : prompt;
285
+ const args = buildArgs(template, { prompt: effectivePrompt, persona, sessionId: opts.sessionId });
286
+ const childEnv = { ...process.env, [DELEGATION_DEPTH_ENV]: String((opts.depth ?? 0) + 1) };
287
+ const start = Date.now();
288
+ const isStreamJson = target.outputFormat === "stream-json";
289
+ return new Promise((resolve) => {
290
+ let settled = false;
291
+ let lineBuffer = "";
292
+ let stdoutAll = "";
293
+ let stderr = "";
294
+ let structuredOutput = "";
295
+ let sessionId;
296
+ let cancelled = false;
297
+ const child = spawn(target.command, args, { cwd: opts.cwd, env: childEnv, stdio: ["ignore", "pipe", "pipe"] });
298
+ const killTimer = setTimeout(() => killWithEscalation(child), timeoutMs);
299
+ const onAbort = () => {
300
+ cancelled = true;
301
+ killWithEscalation(child);
302
+ };
303
+ opts.signal?.addEventListener("abort", onAbort);
304
+ const finish = (value) => {
305
+ if (settled)
306
+ return;
307
+ settled = true;
308
+ clearTimeout(killTimer);
309
+ opts.signal?.removeEventListener("abort", onAbort);
310
+ resolve(value);
311
+ };
312
+ const emitLine = (line) => {
313
+ if (!line)
314
+ return;
315
+ let parsed;
316
+ if (isStreamJson) {
317
+ parsed = target.streamProtocol === "zcode" ? tryParseZcodeStreamJsonLine(line) : tryParseClaudeStreamJsonLine(line);
318
+ if (parsed?.kind === "text-delta")
319
+ structuredOutput += structuredOutput ? `\n${parsed.text}` : parsed.text;
320
+ if (parsed?.kind === "final")
321
+ structuredOutput = parsed.output;
322
+ if (parsed?.kind === "init" && parsed.sessionId)
323
+ sessionId = parsed.sessionId;
324
+ if (parsed?.kind === "final" && parsed.sessionId)
325
+ sessionId = parsed.sessionId;
326
+ }
327
+ opts.onChunk({ raw: line, ...(parsed ? { parsed } : {}) });
328
+ };
329
+ child.stdout?.on("data", (chunk) => {
330
+ const text = chunk.toString("utf-8");
331
+ stdoutAll += text;
332
+ lineBuffer += text;
333
+ const lines = lineBuffer.split("\n");
334
+ lineBuffer = lines.pop() ?? "";
335
+ for (const line of lines)
336
+ emitLine(line);
337
+ });
338
+ child.stderr?.on("data", (chunk) => { stderr += chunk.toString("utf-8"); });
339
+ child.on("error", (err) => finish({ status: "failed", output: err.message, durationMs: Date.now() - start, targetAgent }));
340
+ child.on("close", (code, signal) => {
341
+ if (lineBuffer) {
342
+ emitLine(lineBuffer);
343
+ lineBuffer = "";
344
+ }
345
+ const durationMs = Date.now() - start;
346
+ const finalOutput = isStreamJson ? structuredOutput : stdoutAll;
347
+ if (cancelled) {
348
+ finish({ status: "cancelled", output: finalOutput.trim(), durationMs, targetAgent, ...(sessionId ? { sessionId } : {}) });
349
+ return;
350
+ }
351
+ if (signal === "SIGTERM" || signal === "SIGKILL") {
352
+ finish({ status: "timeout", output: (finalOutput || stderr).trim(), durationMs, targetAgent, ...(sessionId ? { sessionId } : {}) });
353
+ return;
354
+ }
355
+ if (code !== 0) {
356
+ finish({ status: "failed", output: (stderr || finalOutput).trim(), durationMs, targetAgent });
357
+ return;
358
+ }
359
+ if (isStreamJson) {
360
+ finish({ status: "completed", output: finalOutput.trim(), durationMs, targetAgent, ...(sessionId ? { sessionId } : {}) });
361
+ return;
362
+ }
363
+ const extracted = extractOutput(stdoutAll, target.outputFormat);
364
+ finish({ status: "completed", output: extracted.output, durationMs, targetAgent, ...(extracted.sessionId ? { sessionId: extracted.sessionId } : {}) });
365
+ });
366
+ });
367
+ }
@@ -0,0 +1,14 @@
1
+ import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
3
+ /** Exposes every `agent-bridge.yaml` target — other than the calling
4
+ * Agent itself — as one MCP tool per target Agent. Routing which target
5
+ * to call stays with the caller's own model, exactly like ordinary MCP
6
+ * tool selection; this provider only executes the call once chosen, and
7
+ * refuses one that would exceed the configured delegation depth before
8
+ * ever spawning a process. */
9
+ export declare class AgentBridgeProvider implements TrellisProvider {
10
+ readonly id = "agent_bridge";
11
+ private readonly toolTargets;
12
+ listTools(context: RuntimeContext): Tool[];
13
+ callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
14
+ }
@@ -0,0 +1,95 @@
1
+ import { DEFAULT_MAX_DELEGATION_DEPTH, loadAgentBridgeConfig, readDelegationDepth, runDelegatedCall } from "./agentBridge.js";
2
+ import { resolveZcodeProfile } from "../probes/zcode.js";
3
+ function toolNameFor(agentId) {
4
+ return `trellis.agent_bridge.run_${agentId.replaceAll("-", "_")}`;
5
+ }
6
+ function toolFor(agentId, target) {
7
+ const tagText = target.tags?.length ? ` Good for: ${target.tags.join(", ")}.` : "";
8
+ const resumeText = target.resumeArgs
9
+ ? " Pass a prior call's returned sessionId to continue that exact conversation."
10
+ : " Does not support resuming a prior conversation — every call starts fresh.";
11
+ return {
12
+ name: toolNameFor(agentId),
13
+ description: `Delegate a one-shot prompt to "${agentId}"'s own non-interactive mode and return its result.${tagText} Optionally set "persona" to shape how it responds for this call.${resumeText} Requires explicit confirm=true — this spawns a real subprocess with real side effects, not a read-only lookup.`,
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {
17
+ prompt: { type: "string" },
18
+ persona: { type: "string", description: "Overrides this target's default persona/role for this call only." },
19
+ sessionId: { type: "string", description: "Resume a prior call's conversation instead of starting fresh; requires the target to support resuming." },
20
+ cwd: { type: "string" },
21
+ timeoutMs: { type: "number" },
22
+ confirm: { type: "boolean" },
23
+ },
24
+ required: ["prompt", "confirm"],
25
+ additionalProperties: false,
26
+ },
27
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
28
+ };
29
+ }
30
+ function result(value) {
31
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], structuredContent: value };
32
+ }
33
+ function failure(message) {
34
+ return { content: [{ type: "text", text: message }], isError: true };
35
+ }
36
+ /** Exposes every `agent-bridge.yaml` target — other than the calling
37
+ * Agent itself — as one MCP tool per target Agent. Routing which target
38
+ * to call stays with the caller's own model, exactly like ordinary MCP
39
+ * tool selection; this provider only executes the call once chosen, and
40
+ * refuses one that would exceed the configured delegation depth before
41
+ * ever spawning a process. */
42
+ export class AgentBridgeProvider {
43
+ id = "agent_bridge";
44
+ toolTargets = new Map();
45
+ listTools(context) {
46
+ const config = loadAgentBridgeConfig(context.homeDir);
47
+ this.toolTargets.clear();
48
+ const tools = [];
49
+ for (const [agentId, target] of Object.entries(config.targets)) {
50
+ if (agentId === context.agentId)
51
+ continue;
52
+ const tool = toolFor(agentId, target);
53
+ this.toolTargets.set(tool.name, agentId);
54
+ tools.push(tool);
55
+ }
56
+ return tools;
57
+ }
58
+ async callTool(name, args, context) {
59
+ const input = (args ?? {});
60
+ const targetAgent = this.toolTargets.get(name);
61
+ if (!targetAgent)
62
+ return failure(`unknown agent bridge tool "${name}"`);
63
+ if (input.confirm !== true)
64
+ return failure(`agent bridge call "${name}" requires confirm=true — it spawns a real subprocess with real side effects`);
65
+ const prompt = String(input.prompt ?? "").trim();
66
+ if (!prompt)
67
+ return failure("prompt must not be empty");
68
+ const config = loadAgentBridgeConfig(context.homeDir);
69
+ const target = config.targets[targetAgent];
70
+ if (!target)
71
+ return failure(`agent "${targetAgent}" is no longer configured for delegation`);
72
+ if (targetAgent === "zcode") {
73
+ const profile = resolveZcodeProfile(context.homeDir);
74
+ if (!profile?.supportsExecution || target.command !== profile.executable) {
75
+ return failure("zcode delegation requires a compatible public zcode CLI selected by the local ZCode probe");
76
+ }
77
+ }
78
+ const maxDepth = config.maxDepth ?? DEFAULT_MAX_DELEGATION_DEPTH;
79
+ const currentDepth = readDelegationDepth();
80
+ if (currentDepth >= maxDepth) {
81
+ return failure(`delegation depth limit reached (${currentDepth}/${maxDepth}) — refusing to call "${targetAgent}" to avoid a delegation cycle`);
82
+ }
83
+ try {
84
+ const cwd = typeof input.cwd === "string" ? input.cwd : undefined;
85
+ const timeoutMs = typeof input.timeoutMs === "number" ? input.timeoutMs : undefined;
86
+ const persona = typeof input.persona === "string" && input.persona.trim() ? input.persona : undefined;
87
+ const sessionId = typeof input.sessionId === "string" && input.sessionId.trim() ? input.sessionId : undefined;
88
+ const outcome = await runDelegatedCall(targetAgent, target, prompt, { cwd, timeoutMs, persona, sessionId, depth: currentDepth });
89
+ return result(outcome);
90
+ }
91
+ catch (error) {
92
+ return failure(error instanceof Error ? error.message : String(error));
93
+ }
94
+ }
95
+ }
@@ -31,6 +31,38 @@ export type BackupOperation = {
31
31
  kind: "symlink-remove";
32
32
  path: string;
33
33
  beforeLinkTarget: string;
34
+ }
35
+ /** A whole directory created where none existed — e.g. `skill add`
36
+ * copying a canonical Skill in. New operations carry the resulting
37
+ * directory digest so rollback can refuse a tree edited after the run;
38
+ * the optional field preserves compatibility with old backup manifests. */
39
+ | {
40
+ kind: "dir-create";
41
+ path: string;
42
+ afterDigest?: string;
43
+ }
44
+ /** A whole directory removed — e.g. `skill remove`. `beforeDir` is a
45
+ * full recursive copy of the directory's pre-removal content, stored
46
+ * under this run's own directory — the directory equivalent of
47
+ * `file-overwrite`'s single-file `beforeFile` snapshot. Conflict
48
+ * detection is existence-only (does the path exist again since this
49
+ * run), not a deep content hash — a directory tree's content-drift
50
+ * check would need its own manifest of per-file hashes, which no
51
+ * caller of this module has needed yet; if that changes, extend here
52
+ * rather than approximating silently. */
53
+ | {
54
+ kind: "dir-remove";
55
+ path: string;
56
+ beforeDir: string;
57
+ }
58
+ /** A whole directory replaced in-place. The before-tree snapshot and the
59
+ * exact after-tree digest let rollback refuse a locally edited remote Skill
60
+ * instead of overwriting it. */
61
+ | {
62
+ kind: "dir-replace";
63
+ path: string;
64
+ beforeDir: string;
65
+ afterDigest: string;
34
66
  };
35
67
  export interface BackupManifest {
36
68
  runId: string;
@@ -44,6 +76,16 @@ export interface BackupSession {
44
76
  createSymlink(path: string, linkTarget: string): Promise<void>;
45
77
  repairSymlink(path: string, oldLinkTarget: string, newLinkTarget: string): Promise<void>;
46
78
  removeSymlink(path: string, oldLinkTarget: string): Promise<void>;
79
+ /** Copies `sourceDir`'s content to `path` (which must not already
80
+ * exist) and records it as a `dir-create`. */
81
+ createDirFromSource(path: string, sourceDir: string): void;
82
+ /** Snapshots `path`'s current content into this run's own storage,
83
+ * then removes it, and records it as a `dir-remove`. */
84
+ removeDir(path: string): void;
85
+ /** Snapshots an existing directory then copies `sourceDir` in its place,
86
+ * recorded as one operation so rollback can restore it without an
87
+ * intermediate same-path conflict. */
88
+ replaceDirFromSource(path: string, sourceDir: string): void;
47
89
  hasOperations(): boolean;
48
90
  /** Writes manifest.json. No-op (creates nothing) if zero operations
49
91
  * were ever recorded. */
@@ -9,9 +9,10 @@
9
9
  * that never records anything creates nothing on disk.
10
10
  */
11
11
  import { createHash } from "node:crypto";
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
13
13
  import { readlink, rm, symlink } from "node:fs/promises";
14
- import { basename, join } from "node:path";
14
+ import { basename, dirname, join } from "node:path";
15
+ import { directoryDigest } from "./dirDigest.js";
15
16
  function sha256(content) {
16
17
  return `sha256:${createHash("sha256").update(content).digest("hex")}`;
17
18
  }
@@ -60,6 +61,7 @@ export function openBackupSession(homeDir, command) {
60
61
  const operations = [];
61
62
  let dirCreated = false;
62
63
  let fileIndex = 0;
64
+ let dirIndex = 0;
63
65
  function ensureDir() {
64
66
  if (dirCreated)
65
67
  return;
@@ -87,7 +89,12 @@ export function openBackupSession(homeDir, command) {
87
89
  else {
88
90
  operations.push({ kind: "file-create", path, afterHash });
89
91
  }
90
- writeFileSync(path, content);
92
+ // A lock/provenance update must not leave a partially-written JSON file
93
+ // if the process stops mid-write. This also strengthens the existing
94
+ // backup-aware file writes without changing their manifest contract.
95
+ const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${fileIndex}.tmp`);
96
+ writeFileSync(temporary, content);
97
+ renameSync(temporary, path);
91
98
  },
92
99
  async createSymlink(path, linkTarget) {
93
100
  ensureDir();
@@ -106,6 +113,32 @@ export function openBackupSession(homeDir, command) {
106
113
  await rm(path, { force: true });
107
114
  operations.push({ kind: "symlink-remove", path, beforeLinkTarget: oldLinkTarget });
108
115
  },
116
+ createDirFromSource(path, sourceDir) {
117
+ ensureDir();
118
+ mkdirSync(path, { recursive: true });
119
+ cpSync(sourceDir, path, { recursive: true });
120
+ operations.push({ kind: "dir-create", path, afterDigest: directoryDigest(path) });
121
+ },
122
+ removeDir(path) {
123
+ ensureDir();
124
+ const relSnapshot = join("dirs", `${dirIndex}-${basename(path)}`);
125
+ dirIndex += 1;
126
+ cpSync(path, join(runDir, relSnapshot), { recursive: true });
127
+ rmSync(path, { recursive: true, force: true });
128
+ operations.push({ kind: "dir-remove", path, beforeDir: relSnapshot });
129
+ },
130
+ replaceDirFromSource(path, sourceDir) {
131
+ if (!existsSync(path))
132
+ throw new Error(`Cannot replace missing directory: ${path}`);
133
+ ensureDir();
134
+ const relSnapshot = join("dirs", `${dirIndex}-${basename(path)}`);
135
+ dirIndex += 1;
136
+ cpSync(path, join(runDir, relSnapshot), { recursive: true });
137
+ rmSync(path, { recursive: true, force: true });
138
+ mkdirSync(path, { recursive: true });
139
+ cpSync(sourceDir, path, { recursive: true });
140
+ operations.push({ kind: "dir-replace", path, beforeDir: relSnapshot, afterDigest: directoryDigest(path) });
141
+ },
109
142
  hasOperations() {
110
143
  return operations.length > 0;
111
144
  },
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Config for the GUI's in-app chat feature: which CLI agents can be chatted
3
+ * with from the desktop app, keyed by an arbitrary string id (not `AgentId`
4
+ * — chat targets are not necessarily fully-managed Agents with adapters,
5
+ * sync, and probes; they're just "a non-interactive CLI I can spawn and
6
+ * stream from"). Kept as its own file + config path, sibling to
7
+ * `agent-bridge.yaml`, so delegation's `ALL_AGENTS` constraint (needed for
8
+ * the cross-Agent-delegation use case) never has to bend for chat's needs.
9
+ *
10
+ * Configuration lives at `~/.trellis/mcp/chat-agents.yaml`. Its absence
11
+ * means zero targets, not an error — the same "optional, additive" shape
12
+ * as `agent-bridge.yaml`.
13
+ */
14
+ import type { StreamProtocol } from "./agentBridge.js";
15
+ export type ChatAgentOutputFormat = "text" | "json" | "stream-json";
16
+ export interface ChatAgentTargetConfig {
17
+ /** Human-readable name shown in the GUI's agent picker, e.g. "Claude Code". */
18
+ label: string;
19
+ /** Binary to spawn, e.g. "claude", "qoder". */
20
+ command: string;
21
+ /** Argv template for a fresh call. One element may be the literal
22
+ * placeholder `"{prompt}"`, substituted with the caller's message at
23
+ * call time. */
24
+ args: string[];
25
+ /** Argv template used instead of `args` when the caller supplies a
26
+ * `sessionId` to resume — must reference `"{sessionId}"`. */
27
+ resumeArgs?: string[];
28
+ /** How to interpret stdout. Omit for plain text. */
29
+ outputFormat?: ChatAgentOutputFormat;
30
+ /** Optional stream parser for a CLI with a non-Claude event envelope. */
31
+ streamProtocol?: StreamProtocol;
32
+ timeoutMs?: number;
33
+ /** Free-text, informational only — surfaced in the GUI as a hint. */
34
+ tags?: string[];
35
+ /** Default persona/role text applied to every call to this target. */
36
+ persona?: string;
37
+ }
38
+ export interface ChatAgentConfig {
39
+ targets: Record<string, ChatAgentTargetConfig>;
40
+ }
41
+ export declare function chatAgentsPath(homeDir: string): string;
42
+ export declare function loadChatAgentConfig(homeDir: string): ChatAgentConfig;