agent-trellis 0.7.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/README.md +48 -153
  2. package/dist/adapters/claude-code.js +3 -2
  3. package/dist/adapters/codex.js +3 -2
  4. package/dist/adapters/jsonMcp.d.ts +3 -1
  5. package/dist/adapters/jsonMcp.js +18 -4
  6. package/dist/adapters/kimi-code.d.ts +21 -0
  7. package/dist/adapters/kimi-code.js +107 -0
  8. package/dist/adapters/kiro.js +3 -2
  9. package/dist/adapters/mcpPlan.d.ts +6 -1
  10. package/dist/adapters/mcpPlan.js +106 -8
  11. package/dist/adapters/pi.js +22 -3
  12. package/dist/adapters/symlinkPlan.d.ts +7 -0
  13. package/dist/adapters/symlinkPlan.js +16 -7
  14. package/dist/adapters/zcode.d.ts +21 -0
  15. package/dist/adapters/zcode.js +234 -0
  16. package/dist/cli.js +145 -29
  17. package/dist/commands/doctor.d.ts +7 -2
  18. package/dist/commands/doctor.js +40 -2
  19. package/dist/commands/init.d.ts +4 -2
  20. package/dist/commands/init.js +29 -13
  21. package/dist/commands/kimi.d.ts +16 -0
  22. package/dist/commands/kimi.js +64 -0
  23. package/dist/commands/manage.d.ts +39 -0
  24. package/dist/commands/manage.js +126 -0
  25. package/dist/commands/mcp.d.ts +77 -14
  26. package/dist/commands/mcp.js +169 -29
  27. package/dist/commands/mcpGateway.d.ts +6 -0
  28. package/dist/commands/mcpGateway.js +35 -14
  29. package/dist/commands/mcpImport.d.ts +46 -0
  30. package/dist/commands/mcpImport.js +359 -0
  31. package/dist/commands/memory.d.ts +17 -3
  32. package/dist/commands/memory.js +27 -8
  33. package/dist/commands/migrate.d.ts +16 -5
  34. package/dist/commands/migrate.js +93 -17
  35. package/dist/commands/onboard.d.ts +74 -2
  36. package/dist/commands/onboard.js +469 -71
  37. package/dist/commands/rollback.js +28 -2
  38. package/dist/commands/secretsAudit.js +4 -1
  39. package/dist/commands/skill.d.ts +52 -6
  40. package/dist/commands/skill.js +114 -15
  41. package/dist/commands/sync.js +5 -1
  42. package/dist/core/adapter.d.ts +11 -1
  43. package/dist/core/canonical.d.ts +15 -7
  44. package/dist/core/canonical.js +101 -13
  45. package/dist/core/capabilitySelection.d.ts +45 -0
  46. package/dist/core/capabilitySelection.js +124 -0
  47. package/dist/core/types.d.ts +25 -2
  48. package/dist/core/types.js +8 -0
  49. package/dist/lib/agentBridge.d.ts +169 -0
  50. package/dist/lib/agentBridge.js +367 -0
  51. package/dist/lib/agentBridgeProvider.d.ts +14 -0
  52. package/dist/lib/agentBridgeProvider.js +95 -0
  53. package/dist/lib/backup.d.ts +32 -0
  54. package/dist/lib/backup.js +20 -1
  55. package/dist/lib/builtinSkills.d.ts +2 -0
  56. package/dist/lib/builtinSkills.js +4 -0
  57. package/dist/lib/chatAgents.d.ts +42 -0
  58. package/dist/lib/chatAgents.js +59 -0
  59. package/dist/lib/cliMetadata.d.ts +1 -0
  60. package/dist/lib/cliMetadata.js +14 -0
  61. package/dist/lib/envVarNames.d.ts +3 -0
  62. package/dist/lib/envVarNames.js +19 -0
  63. package/dist/lib/gatewayBackend.d.ts +28 -0
  64. package/dist/lib/gatewayBackend.js +72 -1
  65. package/dist/lib/installAgent.d.ts +7 -5
  66. package/dist/lib/installAgent.js +4 -3
  67. package/dist/lib/mcpConnect.js +5 -1
  68. package/dist/lib/mcpMigrateRead.d.ts +2 -0
  69. package/dist/lib/mcpMigrateRead.js +22 -1
  70. package/dist/lib/mcpRuntime.d.ts +46 -0
  71. package/dist/lib/mcpRuntime.js +185 -0
  72. package/dist/lib/mcpStatusProvider.d.ts +10 -0
  73. package/dist/lib/mcpStatusProvider.js +25 -0
  74. package/dist/lib/mcpToolRegistry.d.ts +18 -0
  75. package/dist/lib/mcpToolRegistry.js +107 -14
  76. package/dist/lib/memoryGraph.d.ts +1 -1
  77. package/dist/lib/memoryGraph.js +9 -3
  78. package/dist/lib/memoryProvider.d.ts +58 -0
  79. package/dist/lib/memoryProvider.js +175 -0
  80. package/dist/lib/memoryStatus.d.ts +17 -0
  81. package/dist/lib/memoryStatus.js +88 -0
  82. package/dist/lib/nativeMemory.d.ts +19 -0
  83. package/dist/lib/nativeMemory.js +129 -0
  84. package/dist/lib/realHomeSnapshot.js +5 -0
  85. package/dist/lib/runtimeControlProvider.d.ts +16 -0
  86. package/dist/lib/runtimeControlProvider.js +170 -0
  87. package/dist/lib/secretEnv.d.ts +2 -1
  88. package/dist/lib/secretEnv.js +6 -2
  89. package/dist/lib/skillProvider.d.ts +13 -0
  90. package/dist/lib/skillProvider.js +210 -0
  91. package/dist/lib/taskProvider.d.ts +7 -0
  92. package/dist/lib/taskProvider.js +43 -0
  93. package/dist/lib/taskStore.d.ts +46 -0
  94. package/dist/lib/taskStore.js +156 -0
  95. package/dist/lib/terminalPicker.d.ts +23 -49
  96. package/dist/lib/terminalPicker.js +80 -218
  97. package/dist/lib/zcodeOwnership.d.ts +17 -0
  98. package/dist/lib/zcodeOwnership.js +37 -0
  99. package/dist/pi-bridge/bundle.js +476 -37
  100. package/dist/pi-bridge/index.js +42 -21
  101. package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
  102. package/dist/pi-bridge/schemaTranslate.js +4 -8
  103. package/dist/probes/codex.d.ts +7 -0
  104. package/dist/probes/codex.js +37 -5
  105. package/dist/probes/kimi-code.d.ts +12 -0
  106. package/dist/probes/kimi-code.js +55 -0
  107. package/dist/probes/zcode.d.ts +32 -0
  108. package/dist/probes/zcode.js +140 -0
  109. package/dist/sdk.d.ts +1 -1
  110. package/docs/architecture.md +230 -28
  111. package/docs/assets/brand-v3/trellis-mark-v3-a.png +0 -0
  112. package/docs/assets/brand-v3/trellis-mark-v3-a.svg +10 -0
  113. package/docs/assets/poster-series-v3/README.md +60 -0
  114. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.png +0 -0
  115. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.svg +51 -0
  116. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.png +0 -0
  117. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.svg +51 -0
  118. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.png +0 -0
  119. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.svg +48 -0
  120. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.png +0 -0
  121. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.svg +48 -0
  122. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.png +0 -0
  123. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.svg +56 -0
  124. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.png +0 -0
  125. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.svg +56 -0
  126. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.png +0 -0
  127. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.svg +47 -0
  128. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.png +0 -0
  129. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.svg +47 -0
  130. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.png +0 -0
  131. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.svg +51 -0
  132. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.png +0 -0
  133. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.svg +51 -0
  134. package/docs/getting-started.md +496 -42
  135. package/docs/overview.md +134 -0
  136. package/docs/overview.zh-CN.md +102 -0
  137. package/docs/research.md +59 -0
  138. package/docs/roadmap.md +11 -6
  139. package/docs/video/README.md +76 -0
  140. package/docs/video/build/audio/01-opening.aiff +0 -0
  141. package/docs/video/build/audio/01-opening.mp3 +0 -0
  142. package/docs/video/build/audio/02-pain.aiff +0 -0
  143. package/docs/video/build/audio/02-pain.mp3 +0 -0
  144. package/docs/video/build/audio/03-solution.aiff +0 -0
  145. package/docs/video/build/audio/03-solution.mp3 +0 -0
  146. package/docs/video/build/audio/04-install.aiff +0 -0
  147. package/docs/video/build/audio/04-install.mp3 +0 -0
  148. package/docs/video/build/audio/04-setup.mp3 +0 -0
  149. package/docs/video/build/audio/05-dryrun.mp3 +0 -0
  150. package/docs/video/build/audio/05-init.aiff +0 -0
  151. package/docs/video/build/audio/05-init.mp3 +0 -0
  152. package/docs/video/build/audio/05-interactive.mp3 +0 -0
  153. package/docs/video/build/audio/06-dryrun.aiff +0 -0
  154. package/docs/video/build/audio/06-dryrun.mp3 +0 -0
  155. package/docs/video/build/audio/06-onboard.mp3 +0 -0
  156. package/docs/video/build/audio/06-run.mp3 +0 -0
  157. package/docs/video/build/audio/07-onboard.aiff +0 -0
  158. package/docs/video/build/audio/07-onboard.mp3 +0 -0
  159. package/docs/video/build/audio/07-verify.mp3 +0 -0
  160. package/docs/video/build/audio/08-closing.mp3 +0 -0
  161. package/docs/video/build/audio/08-doctor.aiff +0 -0
  162. package/docs/video/build/audio/08-doctor.mp3 +0 -0
  163. package/docs/video/build/audio/09-rollback.aiff +0 -0
  164. package/docs/video/build/audio/09-rollback.mp3 +0 -0
  165. package/docs/video/build/audio/10-principles.aiff +0 -0
  166. package/docs/video/build/audio/10-principles.mp3 +0 -0
  167. package/docs/video/build/audio/11-closing.aiff +0 -0
  168. package/docs/video/build/audio/11-closing.mp3 +0 -0
  169. package/docs/video/build/frames/01-opening-0.png +0 -0
  170. package/docs/video/build/frames/02-pain-1.png +0 -0
  171. package/docs/video/build/frames/02-pain-2.png +0 -0
  172. package/docs/video/build/frames/03-solution-3.png +0 -0
  173. package/docs/video/build/frames/04-install-4.png +0 -0
  174. package/docs/video/build/frames/04-setup-4.png +0 -0
  175. package/docs/video/build/frames/04-setup-5.png +0 -0
  176. package/docs/video/build/frames/05-dryrun-6.png +0 -0
  177. package/docs/video/build/frames/05-init-5.png +0 -0
  178. package/docs/video/build/frames/05-interactive-5.png +0 -0
  179. package/docs/video/build/frames/05-interactive-6.png +0 -0
  180. package/docs/video/build/frames/06-dryrun-6.png +0 -0
  181. package/docs/video/build/frames/06-dryrun-7.png +0 -0
  182. package/docs/video/build/frames/06-onboard-7.png +0 -0
  183. package/docs/video/build/frames/06-onboard-8.png +0 -0
  184. package/docs/video/build/frames/06-run-7.png +0 -0
  185. package/docs/video/build/frames/06-run-8.png +0 -0
  186. package/docs/video/build/frames/07-onboard-10.png +0 -0
  187. package/docs/video/build/frames/07-onboard-7.png +0 -0
  188. package/docs/video/build/frames/07-onboard-8.png +0 -0
  189. package/docs/video/build/frames/07-onboard-9.png +0 -0
  190. package/docs/video/build/frames/07-verify-10.png +0 -0
  191. package/docs/video/build/frames/07-verify-8.png +0 -0
  192. package/docs/video/build/frames/07-verify-9.png +0 -0
  193. package/docs/video/build/frames/08-closing-10.png +0 -0
  194. package/docs/video/build/frames/08-closing-11.png +0 -0
  195. package/docs/video/build/frames/08-doctor-11.png +0 -0
  196. package/docs/video/build/frames/08-doctor-9.png +0 -0
  197. package/docs/video/build/frames/09-rollback-10.png +0 -0
  198. package/docs/video/build/frames/09-rollback-12.png +0 -0
  199. package/docs/video/build/frames/10-principles-11.png +0 -0
  200. package/docs/video/build/frames/10-principles-13.png +0 -0
  201. package/docs/video/build/frames/11-closing-12.png +0 -0
  202. package/docs/video/build/frames/11-closing-14.png +0 -0
  203. package/docs/video/build/segments/01-opening.mp4 +0 -0
  204. package/docs/video/build/segments/01-opening.txt +3 -0
  205. package/docs/video/build/segments/02-pain.mp4 +0 -0
  206. package/docs/video/build/segments/02-pain.txt +5 -0
  207. package/docs/video/build/segments/03-solution.mp4 +0 -0
  208. package/docs/video/build/segments/03-solution.txt +3 -0
  209. package/docs/video/build/segments/04-install.mp4 +0 -0
  210. package/docs/video/build/segments/04-install.txt +3 -0
  211. package/docs/video/build/segments/04-setup.mp4 +0 -0
  212. package/docs/video/build/segments/04-setup.txt +3 -0
  213. package/docs/video/build/segments/05-dryrun.mp4 +0 -0
  214. package/docs/video/build/segments/05-dryrun.txt +3 -0
  215. package/docs/video/build/segments/05-init.mp4 +0 -0
  216. package/docs/video/build/segments/05-init.txt +3 -0
  217. package/docs/video/build/segments/05-interactive.mp4 +0 -0
  218. package/docs/video/build/segments/05-interactive.txt +5 -0
  219. package/docs/video/build/segments/06-dryrun.mp4 +0 -0
  220. package/docs/video/build/segments/06-dryrun.txt +3 -0
  221. package/docs/video/build/segments/06-onboard.mp4 +0 -0
  222. package/docs/video/build/segments/06-onboard.txt +5 -0
  223. package/docs/video/build/segments/06-run.mp4 +0 -0
  224. package/docs/video/build/segments/06-run.txt +3 -0
  225. package/docs/video/build/segments/07-onboard.mp4 +0 -0
  226. package/docs/video/build/segments/07-onboard.txt +5 -0
  227. package/docs/video/build/segments/07-verify.mp4 +0 -0
  228. package/docs/video/build/segments/07-verify.txt +5 -0
  229. package/docs/video/build/segments/08-closing.mp4 +0 -0
  230. package/docs/video/build/segments/08-closing.txt +3 -0
  231. package/docs/video/build/segments/08-doctor.mp4 +0 -0
  232. package/docs/video/build/segments/08-doctor.txt +3 -0
  233. package/docs/video/build/segments/09-rollback.mp4 +0 -0
  234. package/docs/video/build/segments/09-rollback.txt +3 -0
  235. package/docs/video/build/segments/10-principles.mp4 +0 -0
  236. package/docs/video/build/segments/10-principles.txt +3 -0
  237. package/docs/video/build/segments/11-closing.mp4 +0 -0
  238. package/docs/video/build/segments/11-closing.txt +3 -0
  239. package/docs/video/build/segments/final.txt +8 -0
  240. package/docs/video/build-ai/audio/01-hook.mp3 +0 -0
  241. package/docs/video/build-ai/audio/02-pain.mp3 +0 -0
  242. package/docs/video/build-ai/audio/03-solution.mp3 +0 -0
  243. package/docs/video/build-ai/audio/04-how.mp3 +0 -0
  244. package/docs/video/build-ai/audio/05-safe.mp3 +0 -0
  245. package/docs/video/build-ai/audio/06-closing.mp3 +0 -0
  246. package/docs/video/build-ai/clips/01-hook.mp4 +0 -0
  247. package/docs/video/build-ai/clips/02-pain.mp4 +0 -0
  248. package/docs/video/build-ai/clips/03-solution.mp4 +0 -0
  249. package/docs/video/build-ai/clips/04-how.mp4 +0 -0
  250. package/docs/video/build-ai/clips/05-safe.mp4 +0 -0
  251. package/docs/video/build-ai/clips/06-closing.mp4 +0 -0
  252. package/docs/video/build-ai/first-frame-02-pain.png +0 -0
  253. package/docs/video/build-ai/first-frame-03-solution.png +0 -0
  254. package/docs/video/build-ai/first-frame-04-how.png +0 -0
  255. package/docs/video/build-ai/first-frame-05-safe.png +0 -0
  256. package/docs/video/build-ai/segments/01-hook.caption.txt +2 -0
  257. package/docs/video/build-ai/segments/01-hook.mp4 +0 -0
  258. package/docs/video/build-ai/segments/02-pain.mp4 +0 -0
  259. package/docs/video/build-ai/segments/03-solution.mp4 +0 -0
  260. package/docs/video/build-ai/segments/04-how.mp4 +0 -0
  261. package/docs/video/build-ai/segments/05-safe.mp4 +0 -0
  262. package/docs/video/build-ai/segments/06-closing.mp4 +0 -0
  263. package/docs/video/build-ai/segments/cap-01-hook.png +0 -0
  264. package/docs/video/build-ai/segments/cap-02-pain.png +0 -0
  265. package/docs/video/build-ai/segments/cap-03-solution.png +0 -0
  266. package/docs/video/build-ai/segments/cap-04-how.png +0 -0
  267. package/docs/video/build-ai/segments/cap-05-safe.png +0 -0
  268. package/docs/video/build-ai/segments/cap-06-closing.png +0 -0
  269. package/docs/video/build-ai/segments/final.txt +6 -0
  270. package/docs/video/build-posters/audio/01-hook.mp3 +0 -0
  271. package/docs/video/build-posters/audio/02-pain.mp3 +0 -0
  272. package/docs/video/build-posters/audio/03-solution.mp3 +0 -0
  273. package/docs/video/build-posters/audio/04-how.mp3 +0 -0
  274. package/docs/video/build-posters/audio/05-safe.mp3 +0 -0
  275. package/docs/video/build-posters/audio/06-closing.mp3 +0 -0
  276. package/docs/video/build-posters/cap-01-hook.png +0 -0
  277. package/docs/video/build-posters/cap-02-pain.png +0 -0
  278. package/docs/video/build-posters/cap-03-solution.png +0 -0
  279. package/docs/video/build-posters/cap-04-how.png +0 -0
  280. package/docs/video/build-posters/cap-05-safe.png +0 -0
  281. package/docs/video/build-posters/cap-06-closing.png +0 -0
  282. package/docs/video/build-posters/card-01-hook.png +0 -0
  283. package/docs/video/build-posters/card-06-closing.png +0 -0
  284. package/docs/video/build-posters/segments/01-hook.mp4 +0 -0
  285. package/docs/video/build-posters/segments/02-pain.mp4 +0 -0
  286. package/docs/video/build-posters/segments/03-solution.mp4 +0 -0
  287. package/docs/video/build-posters/segments/04-how.mp4 +0 -0
  288. package/docs/video/build-posters/segments/05-safe.mp4 +0 -0
  289. package/docs/video/build-posters/segments/06-closing.mp4 +0 -0
  290. package/docs/video/build-posters/segments/final.txt +6 -0
  291. package/docs/video/generate-video-ai.mjs +334 -0
  292. package/docs/video/generate-video-posters.mjs +204 -0
  293. package/docs/video/generate-video.mjs +426 -0
  294. package/docs/video/outline.zh-CN.md +102 -0
  295. package/docs/video/trellis-demo-video-ai.mp4 +0 -0
  296. package/docs/video/trellis-demo-video.mp4 +0 -0
  297. package/docs/video/voiceover.zh-CN.md +49 -0
  298. package/package.json +6 -2
  299. package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
  300. package/schema/capability-selection.example.yaml +23 -0
  301. package/schema/scope.example.yaml +1 -1
  302. package/schema/servers.example.yaml +15 -1
@@ -8,7 +8,7 @@
8
8
  * already hold themselves to for every other kind of conflict.
9
9
  */
10
10
  import { createHash } from "node:crypto";
11
- import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
11
+ import { cpSync, existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { readlink, rm, symlink } from "node:fs/promises";
13
13
  import { homedir } from "node:os";
14
14
  import { join } from "node:path";
@@ -99,6 +99,23 @@ async function planOperation(homeDir, runId, op) {
99
99
  }
100
100
  return { action: "restore", path: op.path, description: `recreate ${op.path} -> ${op.beforeLinkTarget}` };
101
101
  }
102
+ // Directory conflict detection is existence-only, not a deep content
103
+ // hash (see BackupOperation's "dir-create"/"dir-remove" doc comment
104
+ // in backup.ts) — a real, documented simplification, not an
105
+ // oversight: something touching a file inside an otherwise-untouched
106
+ // directory won't be caught as a conflict.
107
+ case "dir-create": {
108
+ if (!existsSync(op.path)) {
109
+ return { action: "already-reverted", path: op.path, description: `${op.path} no longer exists — nothing to undo` };
110
+ }
111
+ return { action: "restore", path: op.path, description: `delete ${op.path} (created by this run)` };
112
+ }
113
+ case "dir-remove": {
114
+ if (existsSync(op.path)) {
115
+ return { action: "conflict", path: op.path, description: `${op.path} exists again since this run — left untouched` };
116
+ }
117
+ return { action: "restore", path: op.path, description: `recreate ${op.path} from its content before this run (${join(runDir, op.beforeDir)})` };
118
+ }
102
119
  }
103
120
  }
104
121
  export async function collectRollbackPlan(homeDirInput, runIdInput) {
@@ -130,11 +147,20 @@ async function restoreOperation(homeDir, runId, op) {
130
147
  case "symlink-remove":
131
148
  await symlink(op.beforeLinkTarget, op.path);
132
149
  return;
150
+ case "dir-create":
151
+ rmSync(op.path, { recursive: true, force: true });
152
+ return;
153
+ case "dir-remove":
154
+ cpSync(join(runDir, op.beforeDir), op.path, { recursive: true });
155
+ return;
133
156
  }
134
157
  }
135
158
  export async function applyRollbackPlan(homeDir, runId, manifest, items) {
136
159
  const restorePaths = new Set(items.filter((i) => i.action === "restore").map((i) => i.path));
137
- for (const op of manifest.operations) {
160
+ // Restore in reverse write order. A single onboarding transaction may
161
+ // update servers.yaml several times (mode, memory, routes); replaying
162
+ // restores forward would leave the file at an intermediate snapshot.
163
+ for (const op of [...manifest.operations].reverse()) {
138
164
  if (!restorePaths.has(op.path))
139
165
  continue;
140
166
  await restoreOperation(homeDir, runId, op);
@@ -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
  }
@@ -8,6 +8,24 @@
8
8
  * symlinkPlan.ts`'s pre-existing stale-symlink removal.
9
9
  */
10
10
  import type { AgentId } from "../core/types.js";
11
+ import { type BackupSession } from "../lib/backup.js";
12
+ import type { SyncReport } from "./sync.js";
13
+ export type BuiltinSkillUpdateAction = "create" | "update" | "already-current";
14
+ export interface BuiltinSkillUpdatePlan {
15
+ name: "trellis-runtime";
16
+ action: BuiltinSkillUpdateAction;
17
+ detail: string;
18
+ path: string;
19
+ }
20
+ export declare function collectBuiltinSkillUpdatePlan(homeDir?: string): BuiltinSkillUpdatePlan;
21
+ export declare function applyBuiltinSkillUpdatePlan(plan: BuiltinSkillUpdatePlan, backup?: BackupSession): void;
22
+ export declare function runSkillUpdateBuiltin(opts?: {
23
+ homeDir?: string;
24
+ json?: boolean;
25
+ dryRun?: boolean;
26
+ }): {
27
+ exitCode: number;
28
+ };
11
29
  export interface SkillListEntry {
12
30
  name: string;
13
31
  scope: readonly AgentId[];
@@ -27,25 +45,53 @@ export interface SkillAddPlan {
27
45
  sourceDir?: string;
28
46
  }
29
47
  export declare function collectSkillAddPlan(name: string, fromPath: string, homeDir?: string): SkillAddPlan;
30
- export declare function applySkillAddPlan(plan: SkillAddPlan, homeDir?: string): void;
48
+ export declare function applySkillAddPlan(plan: SkillAddPlan, homeDir?: string, backup?: BackupSession): void;
49
+ export interface SkillAddOutcome {
50
+ plan: SkillAddPlan;
51
+ sync?: SyncReport;
52
+ }
53
+ /**
54
+ * See `applyMcpAddWithSync`'s doc comment (mcp.ts) — same split, same
55
+ * reason: a non-CLI caller needs the structured result a JSON-mode CLI
56
+ * run prints, without scraping stdout. Also opens its own backup session
57
+ * around the canonical copy-in, for the same reason `applyMcpAddWithSync`
58
+ * does (trellis-gui tasks.md 3.5): this direct write used to be the one
59
+ * path in this file with no backup/rollback trace at all.
60
+ */
61
+ export declare function applySkillAddWithSync(plan: SkillAddPlan, opts?: {
62
+ homeDir?: string;
63
+ dryRun?: boolean;
64
+ backupSession?: BackupSession;
65
+ }): Promise<SkillAddOutcome>;
31
66
  export declare function runSkillAdd(name: string, fromPath: string, opts?: {
32
67
  homeDir?: string;
33
68
  json?: boolean;
34
69
  dryRun?: boolean;
35
- }): {
70
+ }): Promise<{
36
71
  exitCode: number;
37
- };
72
+ }>;
38
73
  export type SkillRemoveAction = "removed" | "not-found";
39
74
  export interface SkillRemovePlan {
40
75
  name: string;
41
76
  action: SkillRemoveAction;
42
77
  }
43
78
  export declare function collectSkillRemovePlan(name: string, homeDir?: string): SkillRemovePlan;
44
- export declare function applySkillRemovePlan(plan: SkillRemovePlan, homeDir?: string): void;
79
+ export declare function applySkillRemovePlan(plan: SkillRemovePlan, homeDir?: string, backup?: BackupSession): void;
80
+ export interface SkillRemoveOutcome {
81
+ plan: SkillRemovePlan;
82
+ sync?: SyncReport;
83
+ }
84
+ /** See `applySkillAddWithSync`'s doc comment — same split, same reason,
85
+ * same own-backup-session fix. */
86
+ export declare function applySkillRemoveWithSync(plan: SkillRemovePlan, opts?: {
87
+ homeDir?: string;
88
+ dryRun?: boolean;
89
+ backupSession?: BackupSession;
90
+ }): Promise<SkillRemoveOutcome>;
45
91
  export declare function runSkillRemove(name: string, opts?: {
46
92
  homeDir?: string;
47
93
  json?: boolean;
48
94
  dryRun?: boolean;
49
- }): {
95
+ }): Promise<{
50
96
  exitCode: number;
51
- };
97
+ }>;
@@ -7,16 +7,57 @@
7
7
  * `decideDirImport` (src/lib/dirEquals.ts) and `src/adapters/
8
8
  * symlinkPlan.ts`'s pre-existing stale-symlink removal.
9
9
  */
10
- import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
10
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
12
  import { join } from "node:path";
13
+ import { dirname } from "node:path";
14
+ import { fileURLToPath } from "node:url";
13
15
  import { decideDirImport } from "../lib/dirEquals.js";
14
16
  import { findSkillFile } from "../lib/skillFile.js";
15
17
  import { loadCanonicalSource } from "../core/canonical.js";
16
18
  import { resolveScope } from "../core/types.js";
19
+ import { isBuiltinSkillName } from "../lib/builtinSkills.js";
20
+ import { openBackupSession } from "../lib/backup.js";
21
+ import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
22
+ function builtinSkillTemplate() {
23
+ const here = dirname(fileURLToPath(import.meta.url));
24
+ return readFileSync(join(here, "..", "..", "schema", "builtin-skills", "trellis-runtime", "SKILL.md"), "utf8");
25
+ }
26
+ export function collectBuiltinSkillUpdatePlan(homeDir = homedir()) {
27
+ const path = join(homeDir, ".trellis", "skills", "trellis-runtime", "SKILL.md");
28
+ if (!existsSync(path))
29
+ return { name: "trellis-runtime", action: "create", detail: "will install the package-owned Runtime Skill", path };
30
+ if (readFileSync(path, "utf8") === builtinSkillTemplate())
31
+ return { name: "trellis-runtime", action: "already-current", detail: "package-owned Runtime Skill is already current", path };
32
+ return { name: "trellis-runtime", action: "update", detail: "will refresh the package-owned Runtime Skill; existing Agent symlinks will see the update", path };
33
+ }
34
+ export function applyBuiltinSkillUpdatePlan(plan, backup) {
35
+ if (plan.action === "already-current")
36
+ return;
37
+ const content = builtinSkillTemplate();
38
+ mkdirSync(dirname(plan.path), { recursive: true });
39
+ if (backup)
40
+ backup.writeFile(plan.path, content);
41
+ else
42
+ writeFileSync(plan.path, content);
43
+ }
44
+ export function runSkillUpdateBuiltin(opts = {}) {
45
+ const homeDir = opts.homeDir ?? homedir();
46
+ const plan = collectBuiltinSkillUpdatePlan(homeDir);
47
+ if (!opts.dryRun && plan.action !== "already-current") {
48
+ const backup = openBackupSession(homeDir, "skill-update-builtin");
49
+ applyBuiltinSkillUpdatePlan(plan, backup);
50
+ backup.finalize();
51
+ }
52
+ if (opts.json)
53
+ console.log(JSON.stringify(plan, null, 2));
54
+ else
55
+ console.log(`${opts.dryRun ? "[dry run] " : ""}skill update-builtin\n [${plan.action}] ${plan.detail}`);
56
+ return { exitCode: 0 };
57
+ }
17
58
  export function collectSkillList(homeDir = homedir()) {
18
59
  const canonical = loadCanonicalSource(homeDir);
19
- return canonical.skills.map((skill) => ({
60
+ return canonical.skills.filter((skill) => !isBuiltinSkillName(skill.name)).map((skill) => ({
20
61
  name: skill.name,
21
62
  scope: resolveScope(skill.scope, canonical.managedAgents),
22
63
  }));
@@ -37,6 +78,9 @@ export function runSkillList(opts = {}) {
37
78
  return { exitCode: 0 };
38
79
  }
39
80
  export function collectSkillAddPlan(name, fromPath, homeDir = homedir()) {
81
+ if (isBuiltinSkillName(name)) {
82
+ return { name, action: "conflict", detail: `"${name}" is a Trellis package-owned Skill and cannot be replaced with skill add` };
83
+ }
40
84
  const skillFile = findSkillFile(fromPath);
41
85
  if (!skillFile) {
42
86
  return { name, action: "invalid-source", detail: `${fromPath} has no SKILL.md` };
@@ -54,51 +98,106 @@ export function collectSkillAddPlan(name, fromPath, homeDir = homedir()) {
54
98
  return { name, action: "conflict", detail: `canonical skills/${name}/ already exists with different content — resolve by hand` };
55
99
  }
56
100
  }
57
- export function applySkillAddPlan(plan, homeDir = homedir()) {
101
+ export function applySkillAddPlan(plan, homeDir = homedir(), backup) {
58
102
  if (plan.action !== "create" || !plan.sourceDir)
59
103
  return;
60
104
  const dest = join(homeDir, ".trellis", "skills", plan.name);
105
+ if (backup) {
106
+ backup.createDirFromSource(dest, plan.sourceDir);
107
+ return;
108
+ }
61
109
  mkdirSync(dest, { recursive: true });
62
110
  cpSync(plan.sourceDir, dest, { recursive: true });
63
111
  }
64
- export function runSkillAdd(name, fromPath, opts = {}) {
112
+ /**
113
+ * See `applyMcpAddWithSync`'s doc comment (mcp.ts) — same split, same
114
+ * reason: a non-CLI caller needs the structured result a JSON-mode CLI
115
+ * run prints, without scraping stdout. Also opens its own backup session
116
+ * around the canonical copy-in, for the same reason `applyMcpAddWithSync`
117
+ * does (trellis-gui tasks.md 3.5): this direct write used to be the one
118
+ * path in this file with no backup/rollback trace at all.
119
+ */
120
+ export async function applySkillAddWithSync(plan, opts = {}) {
65
121
  const homeDir = opts.homeDir ?? homedir();
66
- const plan = collectSkillAddPlan(name, fromPath, homeDir);
122
+ const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "skill-add") : undefined;
123
+ const session = opts.backupSession ?? ownSession;
67
124
  if (!opts.dryRun && plan.action === "create") {
68
- applySkillAddPlan(plan, homeDir);
125
+ applySkillAddPlan(plan, homeDir, session);
126
+ }
127
+ ownSession?.finalize();
128
+ const failed = plan.action === "conflict" || plan.action === "invalid-source";
129
+ let syncReport;
130
+ if (!failed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
131
+ syncReport = await collectSyncReport({ target: "skills", homeDir, dryRun: opts.dryRun });
69
132
  }
133
+ return { plan, ...(syncReport ? { sync: syncReport } : {}) };
134
+ }
135
+ export async function runSkillAdd(name, fromPath, opts = {}) {
136
+ const homeDir = opts.homeDir ?? homedir();
137
+ const plan = collectSkillAddPlan(name, fromPath, homeDir);
138
+ const { sync: syncReport } = await applySkillAddWithSync(plan, { homeDir, dryRun: opts.dryRun });
139
+ const failed = plan.action === "conflict" || plan.action === "invalid-source";
70
140
  if (opts.json) {
71
- console.log(JSON.stringify(plan, null, 2));
141
+ console.log(JSON.stringify(syncReport ? { ...plan, sync: syncReport } : plan, null, 2));
72
142
  }
73
143
  else {
74
144
  console.log(`${opts.dryRun ? "[dry run] " : ""}skill add ${name}`);
75
145
  console.log(` [${plan.action}] ${plan.detail}`);
146
+ if (syncReport)
147
+ printSyncReport(syncReport, opts.dryRun ?? false);
76
148
  }
77
- return { exitCode: plan.action === "conflict" || plan.action === "invalid-source" ? 1 : 0 };
149
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
150
+ return { exitCode: failed || syncConflict ? 1 : 0 };
78
151
  }
79
152
  export function collectSkillRemovePlan(name, homeDir = homedir()) {
153
+ if (isBuiltinSkillName(name))
154
+ return { name, action: "not-found" };
80
155
  const dir = join(homeDir, ".trellis", "skills", name);
81
156
  return { name, action: existsSync(dir) ? "removed" : "not-found" };
82
157
  }
83
- export function applySkillRemovePlan(plan, homeDir = homedir()) {
158
+ export function applySkillRemovePlan(plan, homeDir = homedir(), backup) {
84
159
  if (plan.action !== "removed")
85
160
  return;
86
- rmSync(join(homeDir, ".trellis", "skills", plan.name), { recursive: true, force: true });
161
+ const dir = join(homeDir, ".trellis", "skills", plan.name);
162
+ if (backup) {
163
+ backup.removeDir(dir);
164
+ return;
165
+ }
166
+ rmSync(dir, { recursive: true, force: true });
87
167
  }
88
- export function runSkillRemove(name, opts = {}) {
168
+ /** See `applySkillAddWithSync`'s doc comment — same split, same reason,
169
+ * same own-backup-session fix. */
170
+ export async function applySkillRemoveWithSync(plan, opts = {}) {
89
171
  const homeDir = opts.homeDir ?? homedir();
90
- const plan = collectSkillRemovePlan(name, homeDir);
172
+ const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "removed" ? openBackupSession(homeDir, "skill-remove") : undefined;
173
+ const session = opts.backupSession ?? ownSession;
91
174
  if (!opts.dryRun) {
92
- applySkillRemovePlan(plan, homeDir);
175
+ applySkillRemovePlan(plan, homeDir, session);
93
176
  }
177
+ ownSession?.finalize();
178
+ const failed = plan.action === "not-found";
179
+ let syncReport;
180
+ if (!failed && existsSync(join(homeDir, ".trellis"))) {
181
+ syncReport = await collectSyncReport({ target: "skills", homeDir, dryRun: opts.dryRun });
182
+ }
183
+ return { plan, ...(syncReport ? { sync: syncReport } : {}) };
184
+ }
185
+ export async function runSkillRemove(name, opts = {}) {
186
+ const homeDir = opts.homeDir ?? homedir();
187
+ const plan = collectSkillRemovePlan(name, homeDir);
188
+ const { sync: syncReport } = await applySkillRemoveWithSync(plan, { homeDir, dryRun: opts.dryRun });
189
+ const failed = plan.action === "not-found";
94
190
  if (opts.json) {
95
- console.log(JSON.stringify(plan, null, 2));
191
+ console.log(JSON.stringify(syncReport ? { ...plan, sync: syncReport } : plan, null, 2));
96
192
  }
97
193
  else if (plan.action === "not-found") {
98
194
  console.error(`"${name}" is not a canonical skill — nothing to remove.`);
99
195
  }
100
196
  else {
101
197
  console.log(`${opts.dryRun ? "[dry run] " : ""}removed skill "${name}" from canonical source.`);
198
+ if (syncReport)
199
+ printSyncReport(syncReport, opts.dryRun ?? false);
102
200
  }
103
- return { exitCode: plan.action === "not-found" ? 1 : 0 };
201
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
202
+ return { exitCode: failed || syncConflict ? 1 : 0 };
104
203
  }
@@ -11,12 +11,16 @@ import { ClaudeCodeAdapter } from "../adapters/claude-code.js";
11
11
  import { CodexAdapter } from "../adapters/codex.js";
12
12
  import { KiroAdapter } from "../adapters/kiro.js";
13
13
  import { PiAdapter } from "../adapters/pi.js";
14
+ import { KimiCodeAdapter } from "../adapters/kimi-code.js";
15
+ import { ZcodeAdapter } from "../adapters/zcode.js";
14
16
  import { openBackupSession } from "../lib/backup.js";
15
17
  const ADAPTER_FACTORY = {
16
18
  "claude-code": (homeDir) => new ClaudeCodeAdapter(homeDir),
17
19
  codex: (homeDir) => new CodexAdapter(homeDir),
18
20
  kiro: (homeDir) => new KiroAdapter(homeDir),
19
21
  pi: (homeDir) => new PiAdapter(homeDir),
22
+ "kimi-code": (homeDir) => new KimiCodeAdapter(homeDir),
23
+ zcode: (homeDir) => new ZcodeAdapter(homeDir),
20
24
  };
21
25
  /** Only agents in `canonical.managedAgents` — an agent present on this
22
26
  * machine but not managed gets no adapter at all, not a zero-item plan
@@ -58,7 +62,7 @@ export async function collectSyncReport(opts = {}) {
58
62
  // out (found via the sandbox: every agent reported "already in
59
63
  // sync" even with real create items pending).
60
64
  const kind = opts.target === "skills" ? "skill" : "instructions";
61
- items = items.filter((item) => item.kind === kind);
65
+ items = items.filter((item) => item.kind === kind || (kind === "skill" && item.kind === "zcode-skill-control"));
62
66
  }
63
67
  if (!opts.dryRun) {
64
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,8 @@
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, CanonicalSource, McpServerDef } from "./types.js";
7
+ import type { AgentId, CapabilityDelivery, CanonicalSource, McpRoute, McpServerDef } from "./types.js";
8
+ import type { BackupSession } from "../lib/backup.js";
8
9
  /**
9
10
  * The on-disk shape for one server entry — `static_env`/`env_aliases`
10
11
  * (snake_case, like every other multi-word key across `.trellis/*.yaml`)
@@ -27,10 +28,10 @@ export type ServersYamlWriteResult = {
27
28
  ok: false;
28
29
  error: string;
29
30
  };
30
- export declare function upsertServerYaml(path: string, name: string, def: McpServerDef): ServersYamlWriteResult;
31
+ export declare function upsertServerYaml(path: string, name: string, def: McpServerDef, backup?: BackupSession): ServersYamlWriteResult;
31
32
  /** Inverse of `upsertServerYaml` — same preservation guarantee, same
32
33
  * refusal posture on a missing/unparseable file. */
33
- export declare function removeServerYaml(path: string, name: string): ServersYamlWriteResult;
34
+ export declare function removeServerYaml(path: string, name: string, backup?: BackupSession): ServersYamlWriteResult;
34
35
  /**
35
36
  * `trellis onboard --mcp-mode <value>` (trellis-onboard-mcp-mode) —
36
37
  * everything needed to select or clear the top-level `hub`/`gateway`
@@ -56,7 +57,14 @@ export type McpMode = {
56
57
  * starter file). Selecting one mode always clears the other two
57
58
  * (design.md D3) — never left for the caller to remember.
58
59
  */
59
- export declare function writeMcpModeYaml(path: string, mode: McpMode): ServersYamlWriteResult;
60
+ export declare function writeMcpModeYaml(path: string, mode: McpMode, backup?: BackupSession): ServersYamlWriteResult;
61
+ export type McpRoutesWriteResult = ServersYamlWriteResult;
62
+ /** Writes explicit per-agent routes while preserving legacy hub/gateway
63
+ * shorthand. An empty route map removes the optional key. */
64
+ export declare function writeMcpRoutesYaml(path: string, routes: Partial<Record<AgentId, McpRoute>>, backup?: BackupSession): McpRoutesWriteResult;
65
+ /** Writes per-agent capability delivery preferences into servers.yaml.
66
+ * Omitted entries continue to use the native default. */
67
+ export declare function writeMcpRuntimeDeliveryYaml(path: string, delivery: Partial<Record<AgentId, CapabilityDelivery>>, backup?: BackupSession): ServersYamlWriteResult;
60
68
  export type SecretsPolicyWriteResult = {
61
69
  ok: true;
62
70
  } | {
@@ -78,7 +86,7 @@ export type SecretsPolicyWriteResult = {
78
86
  export declare function writeSecretsPolicyExtraction(path: string, extraction: {
79
87
  varName: string;
80
88
  envFilePath: string;
81
- }): SecretsPolicyWriteResult;
89
+ }, backup?: BackupSession): SecretsPolicyWriteResult;
82
90
  /**
83
91
  * Ensures one exact line is present in a `.gitignore` file, creating the
84
92
  * file if it doesn't exist yet — idempotent (a no-op if the line is
@@ -89,7 +97,7 @@ export declare function writeSecretsPolicyExtraction(path: string, extraction: {
89
97
  * init`'s own bootstrap, so a machine that never extracts a secret never
90
98
  * gains this file at all.
91
99
  */
92
- export declare function ensureGitignoreEntry(gitignorePath: string, line: string): void;
100
+ export declare function ensureGitignoreEntry(gitignorePath: string, line: string, backup?: BackupSession): void;
93
101
  /**
94
102
  * Ensures a shell rc file sources the local secrets env file on every new
95
103
  * shell — one generic pointer block, appended once, idempotent (detected
@@ -102,7 +110,7 @@ export declare function ensureGitignoreEntry(gitignorePath: string, line: string
102
110
  * lines `parseDotenv` already expects, so that format never needs an
103
111
  * `export` prefix of its own.
104
112
  */
105
- export declare function ensureShellEnvSource(rcPath: string, envFilePath: string): void;
113
+ export declare function ensureShellEnvSource(rcPath: string, envFilePath: string, backup?: BackupSession): void;
106
114
  /**
107
115
  * `homeDir` defaults to the real `~` and is only ever overridden for tests
108
116
  * and `scripts/sandbox.sh` — the same seam P0's probes use