infra-kit 0.1.114 → 0.1.117

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 (204) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-1fcfe0ec-7567-41c3-9bfd-501e99e86819.jsonl +19 -0
  3. package/.omc/state/agent-replay-38382db5-3088-4b5a-aff7-2561a60827c7.jsonl +17 -0
  4. package/.omc/state/agent-replay-50c36be9-a3a2-42b9-9c85-5a06d3e22f6a.jsonl +5 -0
  5. package/.omc/state/agent-replay-8198b3e5-fad6-4ee0-9c59-ef773ad89af4.jsonl +1 -0
  6. package/.omc/state/agent-replay-8cd7652a-b8b5-49cf-9b1f-bee8e5972458.jsonl +3 -0
  7. package/.omc/state/agent-replay-9bea518f-a9ee-4d14-bb53-a936ebaf647c.jsonl +7 -0
  8. package/.omc/state/agent-replay-b52d89fb-0e24-4901-bc66-27e3c1697126.jsonl +2 -0
  9. package/.omc/state/agent-replay-f581a1ae-0ee4-48a3-96f3-ce31a0633bb0.jsonl +1 -0
  10. package/.omc/state/agent-replay-fe1057b4-9a25-4e8e-8c3f-3e11652ee697.jsonl +5 -0
  11. package/.omc/state/idle-notif-cooldown.json +1 -1
  12. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/last-tool-error-state.json +7 -0
  13. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/mission-state.json +109 -0
  14. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/pre-tool-advisory-throttle.json +42 -0
  15. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ralph-state.json +13 -0
  16. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/skill-active-state.json +15 -0
  17. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/subagent-tracking-state.json +62 -0
  18. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ultrawork-state.json +11 -0
  19. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/last-tool-error-state.json +7 -0
  20. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/pre-tool-advisory-throttle.json +18 -0
  21. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/last-tool-error-state.json +7 -0
  22. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/mission-state.json +145 -0
  23. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/pre-tool-advisory-throttle.json +38 -0
  24. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/ralplan-state.json +11 -0
  25. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/subagent-tracking-state.json +98 -0
  26. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  27. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  28. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/mission-state.json +61 -0
  29. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/pre-tool-advisory-throttle.json +22 -0
  30. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/skill-active-state.json +15 -0
  31. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/subagent-tracking-state.json +26 -0
  32. package/{src/lib/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2 → .omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4}/pre-tool-advisory-throttle.json +3 -3
  33. package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/subagent-tracking-state.json +17 -0
  34. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/pre-tool-advisory-throttle.json +18 -0
  35. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/ralplan-state.json +11 -0
  36. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/subagent-tracking-state.json +35 -0
  37. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/last-tool-error-state.json +7 -0
  38. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/mission-state.json +79 -0
  39. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/pre-tool-advisory-throttle.json +22 -0
  40. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/progress.txt +7 -0
  41. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/skill-active-state.json +15 -0
  42. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/subagent-tracking-state.json +26 -0
  43. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/pre-tool-advisory-throttle.json +14 -0
  44. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/subagent-tracking-state.json +17 -0
  45. package/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +18 -0
  46. package/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  47. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/pre-tool-advisory-throttle.json +18 -0
  48. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/subagent-tracking-state.json +17 -0
  49. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/last-tool-error-state.json +7 -0
  50. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/mission-state.json +61 -0
  51. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/prd.json +22 -0
  52. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/pre-tool-advisory-throttle.json +22 -0
  53. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/progress.txt +22 -0
  54. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/skill-active-state.json +15 -0
  55. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/subagent-tracking-state.json +26 -0
  56. package/.turbo/turbo-build.log +3 -3
  57. package/.turbo/turbo-eslint-check.log +2 -0
  58. package/.turbo/turbo-test.log +90 -42
  59. package/dist/boot-K4QUAFSP.js +2 -0
  60. package/dist/boot-K4QUAFSP.js.map +7 -0
  61. package/dist/chunk-FEGOOVZE.js +166 -0
  62. package/dist/chunk-FEGOOVZE.js.map +7 -0
  63. package/dist/chunk-WA4BQRDC.js +2 -0
  64. package/dist/chunk-WA4BQRDC.js.map +7 -0
  65. package/dist/cli.js +11 -107
  66. package/dist/cli.js.map +4 -4
  67. package/dist/entry/index.d.ts +2 -0
  68. package/dist/index.js +1 -1
  69. package/dist/index.js.map +4 -4
  70. package/dist/lib/vendor/config-schema.d.ts +58 -0
  71. package/dist/mcp.js +1 -54
  72. package/dist/mcp.js.map +4 -4
  73. package/eslint.config.js +33 -1
  74. package/package.json +5 -1
  75. package/scripts/build.js +11 -1
  76. package/src/.omc/state/agent-replay-c513ef38-cc3e-4a29-9c4c-1e0137245dd5.jsonl +4 -0
  77. package/src/.omc/state/agent-replay-ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a.jsonl +2 -0
  78. package/src/.omc/state/idle-notif-cooldown.json +1 -1
  79. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/mission-state.json +79 -0
  80. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +22 -0
  81. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/subagent-tracking-state.json +26 -0
  82. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  83. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ralph-state.json +13 -0
  84. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ultrawork-state.json +11 -0
  85. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  86. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  87. package/src/commands/config/config.ts +7 -37
  88. package/src/commands/doctor/__tests__/agent-files.test.ts +8 -30
  89. package/src/commands/doctor/__tests__/check-legacy-user-global-config.test.ts +110 -0
  90. package/src/commands/doctor/doctor.ts +104 -44
  91. package/src/commands/env-autoload/env-autoload.ts +12 -0
  92. package/src/commands/env-autoload/index.ts +1 -0
  93. package/src/commands/env-clear/__tests__/env-clear.test.ts +27 -0
  94. package/src/commands/env-clear/env-clear.ts +22 -8
  95. package/src/commands/env-load/__tests__/env-load.test.ts +36 -1
  96. package/src/commands/env-load/env-load.ts +152 -35
  97. package/src/commands/env-load/index.ts +2 -1
  98. package/src/commands/env-status/__tests__/env-status.test.ts +78 -0
  99. package/src/commands/env-status/env-status.ts +19 -7
  100. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +45 -1
  101. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +46 -1
  102. package/src/commands/init/__tests__/agent-files.test.ts +145 -27
  103. package/src/commands/init/__tests__/migrate-config.test.ts +136 -5
  104. package/src/commands/init/__tests__/migrate-factory-config.test.ts +93 -0
  105. package/src/commands/init/__tests__/seed-user-global-config.test.ts +64 -0
  106. package/src/commands/init/__tests__/shell-body.test.ts +30 -0
  107. package/src/commands/init/agent-files.ts +65 -45
  108. package/src/commands/init/init.ts +140 -31
  109. package/src/commands/init/migrate-config.ts +160 -32
  110. package/src/commands/vendor-check/__tests__/runtime-isolation.test.ts +71 -0
  111. package/src/commands/vendor-check/__tests__/vendor-check.test.ts +87 -0
  112. package/src/commands/vendor-check/index.ts +1 -0
  113. package/src/commands/vendor-check/vendor-check.ts +127 -0
  114. package/src/commands/vendor-config/__tests__/vendor-config.test.ts +103 -0
  115. package/src/commands/vendor-config/index.ts +1 -0
  116. package/src/commands/vendor-config/vendor-config.ts +162 -0
  117. package/src/commands/vendor-diff/__tests__/vendor-diff.test.ts +74 -0
  118. package/src/commands/vendor-diff/index.ts +1 -0
  119. package/src/commands/vendor-diff/vendor-diff.ts +77 -0
  120. package/src/commands/vendor-manifest/index.ts +1 -0
  121. package/src/commands/vendor-manifest/vendor-manifest.ts +60 -0
  122. package/src/commands/vendor-sync/__tests__/vendor-write.test.ts +86 -0
  123. package/src/commands/vendor-sync/index.ts +1 -0
  124. package/src/commands/vendor-sync/vendor-sync.ts +90 -0
  125. package/src/commands/worktrees-add/worktrees-add.ts +1 -1
  126. package/src/commands/worktrees-reload/worktrees-reload.ts +6 -5
  127. package/src/commands/worktrees-remove/worktrees-remove.ts +10 -1
  128. package/src/commands/worktrees-sync/worktrees-sync.ts +9 -1
  129. package/src/entry/cli.ts +417 -210
  130. package/src/entry/index.ts +7 -1
  131. package/src/integrations/cmux/__tests__/open-workspace-with-layout.test.ts +127 -0
  132. package/src/integrations/cmux/close-workspace-by-title.ts +2 -2
  133. package/src/integrations/cmux/list-workspace-titles.ts +1 -1
  134. package/src/integrations/cmux/open-workspace-with-layout.ts +15 -4
  135. package/src/integrations/cursor/open-cursor-workspace.ts +6 -11
  136. package/src/integrations/doppler/__tests__/doppler-errors.test.ts +76 -0
  137. package/src/integrations/doppler/doppler-cli-auth.ts +12 -4
  138. package/src/integrations/doppler/doppler-errors.ts +88 -0
  139. package/src/integrations/doppler/doppler-project.ts +80 -0
  140. package/src/integrations/doppler/index.ts +3 -1
  141. package/src/integrations/gh/gh-cli-auth/gh-cli-auth.ts +9 -5
  142. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +16 -8
  143. package/src/integrations/ide/__tests__/ide-facade.test.ts +49 -10
  144. package/src/integrations/ide/open-ide-workspace.ts +6 -6
  145. package/src/integrations/ide/remove-ide-worktree-folders.ts +95 -30
  146. package/src/integrations/ide/types.ts +10 -1
  147. package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +1 -17
  148. package/src/integrations/zed/__tests__/reuse-zed-workspace.test.ts +86 -0
  149. package/src/integrations/zed/index.ts +1 -0
  150. package/src/integrations/zed/open-zed-workspace.ts +4 -10
  151. package/src/integrations/zed/reuse-zed-workspace.ts +57 -0
  152. package/src/lib/command-catalog/__tests__/__snapshots__/command-catalog.test.ts.snap +212 -0
  153. package/src/lib/command-catalog/__tests__/command-catalog.test.ts +193 -0
  154. package/src/lib/command-catalog/command-catalog.ts +141 -0
  155. package/src/lib/command-catalog/index.ts +2 -0
  156. package/src/lib/command-echo/__tests__/command-echo.test.ts +127 -0
  157. package/src/lib/constants/constants.ts +13 -0
  158. package/src/lib/constants/index.ts +2 -0
  159. package/src/lib/env-autoload/__tests__/env-autoload.test.ts +288 -0
  160. package/src/lib/env-autoload/env-autoload.ts +294 -0
  161. package/src/lib/env-autoload/index.ts +9 -0
  162. package/src/lib/errors/operation-error.ts +1 -1
  163. package/src/lib/infra-kit-config/__tests__/env-autoload-schema.test.ts +71 -0
  164. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +77 -6
  165. package/src/lib/infra-kit-config/index.ts +9 -1
  166. package/src/lib/infra-kit-config/infra-kit-config.ts +71 -11
  167. package/src/lib/json-output/__tests__/json-output.test.ts +139 -0
  168. package/src/lib/json-output/index.ts +1 -0
  169. package/src/lib/json-output/json-output.ts +47 -0
  170. package/src/lib/managed-block/managed-block.ts +4 -4
  171. package/src/lib/package-config/package-config.ts +1 -3
  172. package/src/lib/path-display/index.ts +1 -0
  173. package/src/lib/path-display/path-display.ts +42 -0
  174. package/src/lib/render/__tests__/render.test.ts +48 -0
  175. package/src/lib/render/index.ts +1 -0
  176. package/src/lib/render/render.ts +24 -0
  177. package/src/lib/vendor/__tests__/config-schema.test.ts +31 -0
  178. package/src/lib/vendor/__tests__/config.test.ts +63 -0
  179. package/src/lib/vendor/__tests__/factory-config.test.ts +92 -0
  180. package/src/lib/vendor/__tests__/vendor-lib.test.ts +158 -0
  181. package/src/lib/vendor/__tests__/zx-mock.ts +32 -0
  182. package/src/lib/vendor/config-schema.ts +78 -0
  183. package/src/lib/vendor/config.ts +58 -0
  184. package/src/lib/vendor/factory-config-schema.ts +35 -0
  185. package/src/lib/vendor/factory-config.ts +91 -0
  186. package/src/lib/vendor/hash.ts +16 -0
  187. package/src/lib/vendor/index.ts +23 -0
  188. package/src/lib/vendor/manifest.ts +151 -0
  189. package/src/lib/vendor/skip-sets.ts +48 -0
  190. package/src/lib/vendor/sync-ops.ts +290 -0
  191. package/src/lib/vendor/walk.ts +56 -0
  192. package/src/mcp/tools/index.ts +5 -42
  193. package/src/sanity.test.tsx +1 -0
  194. package/src/tui/__tests__/no-react-boundary.test.ts +51 -0
  195. package/src/tui/boot.tsx +45 -0
  196. package/src/tui/screens/__tests__/command-palette.test.tsx +136 -0
  197. package/src/tui/screens/command-palette.tsx +159 -0
  198. package/src/tui/types.ts +8 -0
  199. package/tsconfig.json +1 -0
  200. package/tsconfig.tsbuildinfo +1 -1
  201. package/vitest.config.ts +8 -0
  202. package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +0 -96
  203. package/src/commands/worktrees-open/index.ts +0 -1
  204. package/src/commands/worktrees-open/worktrees-open.ts +0 -195
@@ -0,0 +1,162 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { pathToFileURL } from 'node:url'
5
+
6
+ import { getProjectRoot } from 'src/lib/git-utils'
7
+ import { logger } from 'src/lib/logger'
8
+ import { fileExists, tildify } from 'src/lib/path-display'
9
+ import { VENDOR_CONFIG_FILE } from 'src/lib/vendor/config-schema'
10
+ import { expandTilde, getFactoryConfigPath, loadFactoryConfig } from 'src/lib/vendor/factory-config'
11
+
12
+ interface VendorConfigOptions {
13
+ /** Scaffold `~/.infra-kit/vendor.json` instead of printing the current one. */
14
+ init?: boolean
15
+ /** Source repo root used for legacy `targets` auto-seeding. Defaults to the git toplevel. */
16
+ cwd?: string
17
+ }
18
+
19
+ /** Placeholder workspace dir written by `--init`; the user edits it to their layout. */
20
+ const PLACEHOLDER_WORKSPACE_DIR = '~/projects'
21
+
22
+ /**
23
+ * Surface or scaffold the machine-local factory config
24
+ * (`~/.infra-kit/vendor.json`). CLI-only — NOT an MCP tool; returns nothing
25
+ * and signals problems via `process.exitCode`.
26
+ *
27
+ * Without `--init`: prints the factory file path + existence, the resolved
28
+ * `workspaceDir` + existence, and per-target reachability (`[✓]`/`[ ]`). Exits
29
+ * non-zero if the file is missing, the workspace dir is missing, or any target
30
+ * is unreachable, so it is usable as a doctor check.
31
+ *
32
+ * With `--init`: scaffolds the file (skipping if it already exists), seeding
33
+ * `targets` from a legacy source `vendor.config.ts` when one is readable.
34
+ */
35
+ export const vendorConfig = async (options: VendorConfigOptions = {}): Promise<void> => {
36
+ if (options.init) {
37
+ await initFactoryConfig(options.cwd)
38
+
39
+ return
40
+ }
41
+
42
+ await printFactoryConfig()
43
+ }
44
+
45
+ /** Render the factory config chain with `[✓]`/`[ ]` reachability markers. */
46
+ const printFactoryConfig = async (): Promise<void> => {
47
+ const factoryPath = getFactoryConfigPath()
48
+ const exists = await fileExists(factoryPath)
49
+
50
+ logger.info(`Factory config: ${tildify(factoryPath)} ${exists ? '[✓]' : '[ ]'}`)
51
+
52
+ if (!exists) {
53
+ logger.info('\nNot found — run `infra-kit vendor-config --init` to scaffold it.')
54
+ process.exitCode = 1
55
+
56
+ return
57
+ }
58
+
59
+ const { workspaceDir, targets } = await loadFactoryConfig()
60
+ const resolvedWorkspace = expandTilde(workspaceDir)
61
+ const workspaceExists = await fileExists(resolvedWorkspace)
62
+
63
+ logger.info(
64
+ `workspaceDir: ${workspaceDir} (resolved: ${resolvedWorkspace}) ${workspaceExists ? '[✓ exists]' : '[ ] not found'}`,
65
+ )
66
+ logger.info('Targets:')
67
+
68
+ let allReachable = workspaceExists
69
+
70
+ for (const repo of targets) {
71
+ const targetPath = path.join(resolvedWorkspace, repo)
72
+ const reachable = await fileExists(targetPath)
73
+
74
+ if (!reachable) {
75
+ allReachable = false
76
+ }
77
+
78
+ const marker = reachable ? '[✓]' : '[ ]'
79
+ const suffix = reachable ? '' : ' (not found — clone or remove)'
80
+
81
+ logger.info(` ${marker} ${repo} ${tildify(targetPath)}${suffix}`)
82
+ }
83
+
84
+ if (!allReachable) {
85
+ process.exitCode = 1
86
+ }
87
+ }
88
+
89
+ /** Scaffold `~/.infra-kit/vendor.json`, skipping if it already exists. */
90
+ const initFactoryConfig = async (cwd?: string): Promise<void> => {
91
+ const factoryPath = getFactoryConfigPath()
92
+
93
+ if (await fileExists(factoryPath)) {
94
+ logger.info(`Factory config already exists at ${tildify(factoryPath)} — leaving it untouched.`)
95
+
96
+ return
97
+ }
98
+
99
+ const sourceRoot = cwd ?? (await getProjectRoot())
100
+ const seededTargets = await readLegacyTargets(sourceRoot)
101
+
102
+ await fs.mkdir(path.dirname(factoryPath), { recursive: true })
103
+ await fs.writeFile(factoryPath, buildScaffold(seededTargets), 'utf-8')
104
+
105
+ logger.info(`✓ Created ${tildify(factoryPath)}`)
106
+
107
+ if (seededTargets.length > 0) {
108
+ logger.info(` Seeded ${seededTargets.length} target(s) from the source ${VENDOR_CONFIG_FILE}.`)
109
+ }
110
+
111
+ logger.info(` Edit \`workspaceDir\` (placeholder: ${PLACEHOLDER_WORKSPACE_DIR}) to point at where your repos live.`)
112
+
113
+ if (seededTargets.length === 0) {
114
+ logger.info(' Add at least one repo name to `targets` before running vendor sync/manifest/diff.')
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Best-effort read of a legacy `targets` array from the source repo's
120
+ * `vendor.config.ts`. The current schema no longer accepts `targets`, so this
121
+ * reads the raw default export directly (bypassing validation). Returns `[]` on
122
+ * any failure — seeding is a convenience, never a hard requirement.
123
+ */
124
+ const readLegacyTargets = async (sourceRoot: string): Promise<string[]> => {
125
+ try {
126
+ const configPath = path.join(sourceRoot, VENDOR_CONFIG_FILE)
127
+ const stat = await fs.stat(configPath)
128
+ const moduleUrl = `${pathToFileURL(configPath).href}?mtime=${Number(stat.mtimeMs)}`
129
+ const imported = (await import(moduleUrl)) as { default?: unknown }
130
+ const raw = imported.default
131
+ const resolved = typeof raw === 'function' ? await (raw as () => unknown)() : raw
132
+
133
+ if (resolved && typeof resolved === 'object' && 'targets' in resolved) {
134
+ const targets = (resolved as { targets?: unknown }).targets
135
+
136
+ if (
137
+ Array.isArray(targets) &&
138
+ targets.every((t) => {
139
+ return typeof t === 'string'
140
+ })
141
+ ) {
142
+ return targets as string[]
143
+ }
144
+ }
145
+ } catch {
146
+ // Absent or unreadable source config — fall through to an empty placeholder.
147
+ }
148
+
149
+ return []
150
+ }
151
+
152
+ /**
153
+ * Render the scaffold file body as strict JSON (`vendor.json`). The factory config
154
+ * is static JSON, loaded with `JSON.parse` — no comments, no executable code. When
155
+ * `targets` is empty the stub writes `"targets": []`, which fails the schema's
156
+ * `targets.min(1)` on load: this is intentional — `--init` produces an incomplete
157
+ * stub the user must edit before running vendor sync/manifest/diff. The annotated
158
+ * guidance lives in the sibling `vendor.example.jsonc` (seeded by `infra-kit init`).
159
+ */
160
+ const buildScaffold = (targets: string[]): string => {
161
+ return `${JSON.stringify({ workspaceDir: PLACEHOLDER_WORKSPACE_DIR, targets }, null, 2)}\n`
162
+ }
@@ -0,0 +1,74 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { execMock, zxModuleMock } from 'src/lib/vendor/__tests__/zx-mock'
7
+
8
+ import { vendorDiff } from '../vendor-diff'
9
+
10
+ vi.mock('zx', () => {
11
+ return zxModuleMock()
12
+ })
13
+
14
+ let parent: string
15
+ let home: string
16
+ let sourceRoot: string
17
+ let targetRoot: string
18
+
19
+ const write = (root: string, rel: string, content: string): void => {
20
+ const full = path.join(root, rel)
21
+
22
+ fs.mkdirSync(path.dirname(full), { recursive: true })
23
+ fs.writeFileSync(full, content, 'utf8')
24
+ }
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks()
28
+
29
+ parent = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-diff-test-'))
30
+ sourceRoot = path.join(parent, 'source-repo')
31
+ targetRoot = path.join(parent, 'target-repo')
32
+
33
+ // Machine-local factory config (workspaceDir + targets) lives under ~/.infra-kit;
34
+ // point os.homedir() at a fixture so the loader finds it.
35
+ home = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-diff-home-'))
36
+ vi.spyOn(os, 'homedir').mockReturnValue(home)
37
+ write(home, '.infra-kit/vendor.json', JSON.stringify({ workspaceDir: parent, targets: ['target-repo'] }))
38
+
39
+ write(sourceRoot, 'vendor/configs/a.js', 'aaa')
40
+ write(
41
+ sourceRoot,
42
+ 'vendor.config.ts',
43
+ `export default { copy: [{ name: 'Configs', source: 'vendor/configs', target: 'vendor/configs', type: 'directory', vendored: true }] }`,
44
+ )
45
+ write(targetRoot, 'vendor/configs/a.js', 'DIFFERENT')
46
+ })
47
+
48
+ afterEach(() => {
49
+ vi.restoreAllMocks()
50
+ fs.rmSync(parent, { recursive: true, force: true })
51
+ fs.rmSync(home, { recursive: true, force: true })
52
+ })
53
+
54
+ describe('vendorDiff', () => {
55
+ it('reports drift when rsync dry-run returns changes', async () => {
56
+ execMock.mockResolvedValue({ stdout: '>f.st..... configs/a.js\n' })
57
+
58
+ const { structuredContent } = await vendorDiff({ cwd: sourceRoot })
59
+
60
+ expect(structuredContent.ok).toBe(false)
61
+ expect(structuredContent.drifted).toBe(1)
62
+ expect(structuredContent.entries[0]?.repo).toBe('target-repo')
63
+ expect(structuredContent.entries[0]?.changes).toContain('>f.st..... configs/a.js')
64
+ })
65
+
66
+ it('reports no drift when rsync dry-run is empty', async () => {
67
+ execMock.mockResolvedValue({ stdout: '' })
68
+
69
+ const { structuredContent } = await vendorDiff({ cwd: sourceRoot })
70
+
71
+ expect(structuredContent.ok).toBe(true)
72
+ expect(structuredContent.drifted).toBe(0)
73
+ })
74
+ })
@@ -0,0 +1 @@
1
+ export { vendorDiff, vendorDiffMcpTool } from './vendor-diff'
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod'
2
+
3
+ import { getProjectRoot } from 'src/lib/git-utils'
4
+ import { logger } from 'src/lib/logger'
5
+ import { loadVendorConfig } from 'src/lib/vendor/config'
6
+ import { loadFactoryConfig } from 'src/lib/vendor/factory-config'
7
+ import { runDiff, selectTargets } from 'src/lib/vendor/sync-ops'
8
+ import { defineMcpTool, textContent } from 'src/types'
9
+
10
+ /** Max itemized changes to print per drifted path before truncating. */
11
+ const MAX_REPORTED = 20
12
+
13
+ interface VendorDiffOptions {
14
+ cwd?: string
15
+ repos?: string[]
16
+ }
17
+
18
+ /**
19
+ * Source-aware drift check: compare each target repo's `vendored:true` subtree
20
+ * against the source via `rsync --dry-run --delete`. Read-only (does NOT extend
21
+ * RequiredConfirmedOptionArg).
22
+ */
23
+ export const vendorDiff = async (options: VendorDiffOptions = {}) => {
24
+ const sourceRoot = options.cwd ?? (await getProjectRoot())
25
+
26
+ const config = await loadVendorConfig(sourceRoot)
27
+ const factory = await loadFactoryConfig()
28
+ const targets = selectTargets(factory.targets, options.repos)
29
+
30
+ const entries = await runDiff(config, sourceRoot, factory.workspaceDir, targets)
31
+
32
+ if (entries.length === 0) {
33
+ logger.info('🎉 No drift detected.')
34
+ } else {
35
+ for (const entry of entries) {
36
+ logger.info(`❌ ${entry.repo} → ${entry.target} (${entry.changes.length} change(s)):`)
37
+
38
+ for (const change of entry.changes.slice(0, MAX_REPORTED)) {
39
+ logger.info(` ${change}`)
40
+ }
41
+
42
+ if (entry.changes.length > MAX_REPORTED) {
43
+ logger.info(` …and ${entry.changes.length - MAX_REPORTED} more`)
44
+ }
45
+ }
46
+ }
47
+
48
+ const structuredContent = { ok: entries.length === 0, drifted: entries.length, entries }
49
+
50
+ return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
51
+ }
52
+
53
+ const vendorDiffOutputSchema = {
54
+ ok: z.boolean().describe('Whether every target is in sync with the source'),
55
+ drifted: z.number(),
56
+ entries: z.array(
57
+ z.object({
58
+ repo: z.string(),
59
+ target: z.string(),
60
+ changes: z.array(z.string()),
61
+ }),
62
+ ),
63
+ }
64
+
65
+ // MCP Tool Registration (read-only; safe to expose).
66
+ export const vendorDiffMcpTool = defineMcpTool({
67
+ name: 'vendor-diff',
68
+ description:
69
+ 'Source-aware drift check: compare each target repo vendored subtree against the source via rsync dry-run. Read-only; run from the source repo root. ok=false when any target has drifted.',
70
+ inputSchema: {
71
+ repos: z.array(z.string()).optional().describe('Restrict to these target repo names'),
72
+ },
73
+ outputSchema: vendorDiffOutputSchema,
74
+ handler: (params) => {
75
+ return vendorDiff({ repos: params.repos })
76
+ },
77
+ })
@@ -0,0 +1 @@
1
+ export { vendorManifest, vendorManifestMcpTool } from './vendor-manifest'
@@ -0,0 +1,60 @@
1
+ import path from 'node:path'
2
+ import { z } from 'zod'
3
+
4
+ import { getProjectRoot } from 'src/lib/git-utils'
5
+ import { logger } from 'src/lib/logger'
6
+ import { loadFactoryConfig } from 'src/lib/vendor/factory-config'
7
+ import { getSourceCommit, runManifest, selectTargets } from 'src/lib/vendor/sync-ops'
8
+ import { defineMcpTool, textContent } from 'src/types'
9
+ import type { RequiredConfirmedOptionArg } from 'src/types'
10
+
11
+ interface VendorManifestOptions extends RequiredConfirmedOptionArg {
12
+ cwd?: string
13
+ repos?: string[]
14
+ }
15
+
16
+ /**
17
+ * Regenerate each target repo's `vendor/.sync-manifest.json` + README from the
18
+ * CURRENT vendor content, without copying anything. Mutating (writes manifests).
19
+ */
20
+ export const vendorManifest = async (options: VendorManifestOptions) => {
21
+ const sourceRoot = options.cwd ?? (await getProjectRoot())
22
+
23
+ const factory = await loadFactoryConfig()
24
+ const targets = selectTargets(factory.targets, options.repos)
25
+
26
+ const commit = await getSourceCommit(sourceRoot)
27
+ const source = path.basename(sourceRoot)
28
+
29
+ const repos = await runManifest(factory.workspaceDir, targets, { source, commit })
30
+
31
+ for (const repo of repos) {
32
+ logger.info(
33
+ repo.skipped ? `⚠️ ${repo.repo}: target does not exist — skipped` : `📝 ${repo.repo}: manifest written`,
34
+ )
35
+ }
36
+
37
+ const structuredContent = { source, commit, repos }
38
+
39
+ return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
40
+ }
41
+
42
+ const vendorManifestOutputSchema = {
43
+ source: z.string(),
44
+ commit: z.string(),
45
+ repos: z.array(z.object({ repo: z.string(), skipped: z.boolean() })),
46
+ }
47
+
48
+ // MCP Tool Registration (mutating).
49
+ export const vendorManifestMcpTool = defineMcpTool({
50
+ name: 'vendor-manifest',
51
+ description:
52
+ 'Regenerate each target repo vendor/.sync-manifest.json + README from current content without copying. Mutating; run from the source repo root.',
53
+ inputSchema: {
54
+ repos: z.array(z.string()).optional().describe('Restrict to these target repo names'),
55
+ },
56
+ outputSchema: vendorManifestOutputSchema,
57
+ handler: (params) => {
58
+ return vendorManifest({ repos: params.repos, confirmedCommand: params.confirmedCommand })
59
+ },
60
+ })
@@ -0,0 +1,86 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { execMock, zxModuleMock } from 'src/lib/vendor/__tests__/zx-mock'
7
+
8
+ import { vendorSync } from '../vendor-sync'
9
+
10
+ // Mock zx so the write commands run without shelling out to rsync/cp/git.
11
+ vi.mock('zx', () => {
12
+ return zxModuleMock()
13
+ })
14
+
15
+ let parent: string
16
+ let home: string
17
+ let sourceRoot: string
18
+ let targetRoot: string
19
+
20
+ const write = (root: string, rel: string, content: string): void => {
21
+ const full = path.join(root, rel)
22
+
23
+ fs.mkdirSync(path.dirname(full), { recursive: true })
24
+ fs.writeFileSync(full, content, 'utf8')
25
+ }
26
+
27
+ beforeEach(() => {
28
+ vi.clearAllMocks()
29
+ execMock.mockResolvedValue({ stdout: 'deadbeef\n' })
30
+
31
+ parent = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-write-test-'))
32
+ sourceRoot = path.join(parent, 'source-repo')
33
+ targetRoot = path.join(parent, 'target-repo')
34
+
35
+ // Machine-local factory config (workspaceDir + targets) lives under ~/.infra-kit;
36
+ // point os.homedir() at a fixture so the loader finds it.
37
+ home = fs.mkdtempSync(path.join(os.tmpdir(), 'vendor-write-home-'))
38
+ vi.spyOn(os, 'homedir').mockReturnValue(home)
39
+ write(home, '.infra-kit/vendor.json', JSON.stringify({ workspaceDir: parent, targets: ['target-repo'] }))
40
+
41
+ // Source repo: vendor content + config (copy[] only — targets is machine-local now).
42
+ write(sourceRoot, 'vendor/configs/a.js', 'aaa')
43
+ write(
44
+ sourceRoot,
45
+ 'vendor.config.ts',
46
+ `export default { copy: [{ name: 'Configs', source: 'vendor/configs', target: 'vendor/configs', type: 'directory', vendored: true }] }`,
47
+ )
48
+
49
+ // Target repo: pre-existing vendor content (rsync is mocked, so this stays).
50
+ write(targetRoot, 'vendor/configs/a.js', 'aaa')
51
+ })
52
+
53
+ afterEach(() => {
54
+ vi.restoreAllMocks()
55
+ fs.rmSync(parent, { recursive: true, force: true })
56
+ fs.rmSync(home, { recursive: true, force: true })
57
+ })
58
+
59
+ describe('vendorSync', () => {
60
+ it('writes a manifest + README into the target vendor/ tree', async () => {
61
+ const { structuredContent } = await vendorSync({ cwd: sourceRoot, confirmedCommand: true })
62
+
63
+ expect(structuredContent.source).toBe('source-repo')
64
+ expect(structuredContent.repos).toHaveLength(1)
65
+ expect(structuredContent.repos[0]?.skipped).toBe(false)
66
+
67
+ const manifestPath = path.join(targetRoot, 'vendor', '.sync-manifest.json')
68
+ const readmePath = path.join(targetRoot, 'vendor', 'README.md')
69
+
70
+ expect(fs.existsSync(manifestPath)).toBe(true)
71
+ expect(fs.existsSync(readmePath)).toBe(true)
72
+
73
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
74
+
75
+ expect(manifest.schemaVersion).toBe(1)
76
+ expect(Object.keys(manifest.files)).toContain('configs/a.js')
77
+ })
78
+
79
+ it('marks a non-existent target as skipped', async () => {
80
+ fs.rmSync(targetRoot, { recursive: true, force: true })
81
+
82
+ const { structuredContent } = await vendorSync({ cwd: sourceRoot, confirmedCommand: true })
83
+
84
+ expect(structuredContent.repos[0]?.skipped).toBe(true)
85
+ })
86
+ })
@@ -0,0 +1 @@
1
+ export { vendorSync, vendorSyncMcpTool } from './vendor-sync'
@@ -0,0 +1,90 @@
1
+ import confirm from '@inquirer/confirm'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { z } from 'zod'
5
+
6
+ import { getProjectRoot } from 'src/lib/git-utils'
7
+ import { logger } from 'src/lib/logger'
8
+ import { loadVendorConfig } from 'src/lib/vendor/config'
9
+ import { loadFactoryConfig } from 'src/lib/vendor/factory-config'
10
+ import { getSourceCommit, runSync, selectTargets } from 'src/lib/vendor/sync-ops'
11
+ import { defineMcpTool, textContent } from 'src/types'
12
+ import type { RequiredConfirmedOptionArg } from 'src/types'
13
+
14
+ interface VendorSyncOptions extends RequiredConfirmedOptionArg {
15
+ /** Source repo root. Defaults to the git toplevel. */
16
+ cwd?: string
17
+ /** Restrict the sync to these target repo names (defaults to all configured targets). */
18
+ repos?: string[]
19
+ }
20
+
21
+ /**
22
+ * Copy every item declared in the source repo's `vendor.config.ts` into each
23
+ * target repo, then (re)write each target's `vendor/README.md` +
24
+ * `.sync-manifest.json`. Mutating: extends {@link RequiredConfirmedOptionArg} and
25
+ * prompts for confirmation unless `confirmedCommand` is set (MCP injects it).
26
+ *
27
+ * The config loader and `zx`/rsync live in `config`/`sync-ops`. `vendor check`
28
+ * stays free of them because it never imports the write commands — not because
29
+ * of how they are imported here.
30
+ */
31
+ export const vendorSync = async (options: VendorSyncOptions) => {
32
+ const sourceRoot = options.cwd ?? (await getProjectRoot())
33
+
34
+ const config = await loadVendorConfig(sourceRoot)
35
+ const factory = await loadFactoryConfig()
36
+ const targets = selectTargets(factory.targets, options.repos)
37
+
38
+ if (!options.confirmedCommand) {
39
+ const proceed = await confirm(
40
+ { message: `Sync vendor files from ${path.basename(sourceRoot)} into ${targets.length} repo(s)?` },
41
+ { output: process.stderr },
42
+ )
43
+
44
+ if (!proceed) {
45
+ process.exit(0)
46
+ }
47
+ }
48
+
49
+ const commit = await getSourceCommit(sourceRoot)
50
+ const source = path.basename(sourceRoot)
51
+
52
+ const structuredContent = await runSync(config, sourceRoot, factory.workspaceDir, targets, { source, commit })
53
+
54
+ for (const repo of structuredContent.repos) {
55
+ if (repo.skipped) {
56
+ logger.info(`⚠️ ${repo.repo}: target does not exist — skipped`)
57
+ } else {
58
+ logger.info(`✅ ${repo.repo}: copied ${repo.copied}/${repo.total} items + manifest`)
59
+ }
60
+ }
61
+
62
+ return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }
63
+ }
64
+
65
+ const vendorSyncOutputSchema = {
66
+ source: z.string(),
67
+ commit: z.string(),
68
+ repos: z.array(
69
+ z.object({
70
+ repo: z.string(),
71
+ copied: z.number(),
72
+ total: z.number(),
73
+ skipped: z.boolean(),
74
+ }),
75
+ ),
76
+ }
77
+
78
+ // MCP Tool Registration (mutating — createToolHandler injects confirmedCommand:true).
79
+ export const vendorSyncMcpTool = defineMcpTool({
80
+ name: 'vendor-sync',
81
+ description:
82
+ 'Copy vendored files from the source repo (vendor.config.ts) into each target repo and regenerate their vendor/.sync-manifest.json + README. Mutating; run from the source repo root.',
83
+ inputSchema: {
84
+ repos: z.array(z.string()).optional().describe('Restrict to these target repo names'),
85
+ },
86
+ outputSchema: vendorSyncOutputSchema,
87
+ handler: (params) => {
88
+ return vendorSync({ repos: params.repos, confirmedCommand: params.confirmedCommand })
89
+ },
90
+ })
@@ -349,7 +349,7 @@ export const worktreesAddMcpTool = defineMcpTool({
349
349
  .boolean()
350
350
  .optional()
351
351
  .describe(
352
- 'Open each created worktree in a new cmux workspace with a 3-pane layout (left-top, left-bottom, full-height right), all rooted at the worktree directory. Resolution order: this flag → "worktrees.openInCmux" from infra-kit config → interactive prompt (CLI) / false (MCP, no TTY).',
352
+ 'Open each created worktree in a new cmux workspace, all rooted at the worktree directory. Pane layout follows "worktrees.cmux.layout" (default "two-columns": left | right; or "three-pane": left split top/bottom + full-height right). Resolution order: this flag → "worktrees.openInCmux" from infra-kit config → interactive prompt (CLI) / false (MCP, no TTY).',
353
353
  ),
354
354
  },
355
355
  outputSchema: {
@@ -27,8 +27,9 @@ interface WorktreesReloadResult {
27
27
  /**
28
28
  * Reload command: closes every open cmux workspace for the release worktrees on
29
29
  * disk, then reopens the full set — cmux workspaces are recreated fresh and the
30
- * configured editor workspace is reconciled and relaunched. Unlike the additive
31
- * `worktrees-open`, this is disruptive: it tears down live cmux windows first.
30
+ * configured editor workspace is reconciled and relaunched. The single
31
+ * window-restore command: also serves the cold-start case (editor + cmux closed),
32
+ * at the cost of tearing down any live cmux windows first.
32
33
  *
33
34
  * The editor has no per-window close primitive, so its "reload" is a reconcile +
34
35
  * relaunch of the configured workspace. Safe to run from any branch
@@ -49,7 +50,7 @@ export const worktreesReload = async () => {
49
50
  // Phase 2 — reopen. cmux and Cursor are independent surfaces, so reopen them
50
51
  // concurrently to shrink the window where the just-closed cmux workspaces are gone.
51
52
  const [ideOutcomes, { opened: openedCmux }] = await Promise.all([
52
- openIdeWorkspace({ projectRoot, worktreeDir, currentBranches, skipRelaunchWhenEmpty: true }),
53
+ openIdeWorkspace({ projectRoot, worktreeDir, currentBranches }),
53
54
  reopenCmux({ worktreeDir, currentBranches, repoName }),
54
55
  ])
55
56
 
@@ -144,7 +145,7 @@ interface ReopenCmuxOutcome {
144
145
  * Recreate a cmux workspace for every release worktree on disk. Force-opens each
145
146
  * branch (no dedup against the live workspace list): reload has just awaited the
146
147
  * close of these workspaces, so it favours liveness — guaranteeing every branch
147
- * ends up open — over the additive dedup that `worktrees-open` relies on.
148
+ * ends up open.
148
149
  */
149
150
  export const reopenCmux = async (args: ReopenCmuxArgs): Promise<ReopenCmuxOutcome> => {
150
151
  const { worktreeDir, currentBranches, repoName } = args
@@ -212,7 +213,7 @@ const logResults = (result: WorktreesReloadResult, context: LogResultsContext):
212
213
  export const worktreesReloadMcpTool = defineMcpTool({
213
214
  name: 'worktrees-reload',
214
215
  description:
215
- 'Close all open cmux workspaces for the current release worktrees, then reopen the full set. Disruptive closes live cmux windows (unlike the additive worktrees-open). Every configured editor (Cursor and/or Zed) is reconciled + relaunched (no per-window close). Safe to run from any branch. Use to refresh stale windows after restarting cmux/the editor.',
216
+ 'Close all open cmux workspaces for the current release worktrees, then reopen the full set — the single window-restore command. Every configured editor (Cursor and/or Zed) is reconciled + relaunched (no per-window close). Disruptive: closes live cmux windows first. Safe to run from any branch. Use to refresh stale windows or to restore everything after a cold start (editor + cmux closed).',
216
217
  inputSchema: {},
217
218
  outputSchema: {
218
219
  closedCmux: z.array(z.string()).describe('Titles of cmux workspaces a close was attempted for (best-effort)'),
@@ -118,7 +118,16 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
118
118
  pruneFolder: allSelected,
119
119
  })
120
120
 
121
- await removeIdeWorktreeFolders({ projectRoot, worktreeDir, branches: removedWorktrees })
121
+ // `!confirmedCommand` is the interactive path (a human used the picker/confirm). Only there
122
+ // do we let Zed's destructive `--reuse` relaunch fire; MCP/--yes runs (confirmedCommand=true)
123
+ // and worktrees-sync never relaunch an editor window.
124
+ await removeIdeWorktreeFolders({
125
+ projectRoot,
126
+ worktreeDir,
127
+ currentWorktrees,
128
+ removedWorktrees,
129
+ allowEditorRelaunch: !confirmedCommand,
130
+ })
122
131
 
123
132
  logResults(removedWorktrees)
124
133
 
@@ -72,7 +72,15 @@ export const worktreesSync = async (options: WorktreeSyncArgs) => {
72
72
  repoName,
73
73
  })
74
74
 
75
- await removeIdeWorktreeFolders({ projectRoot, worktreeDir, branches: removedWorktrees })
75
+ // Hard `false`: sync is background/stale-cleanup (predominantly the MCP path) it must never
76
+ // relaunch and overwrite a focused Zed window. Zed removal stays a no-op here.
77
+ await removeIdeWorktreeFolders({
78
+ projectRoot,
79
+ worktreeDir,
80
+ currentWorktrees,
81
+ removedWorktrees,
82
+ allowEditorRelaunch: false,
83
+ })
76
84
 
77
85
  logResults(removedWorktrees)
78
86