infra-kit 0.1.111 → 0.1.114

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 (99) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-0d1515ce-4ebe-4ce8-a865-2897f68de168.jsonl +1 -0
  3. package/.omc/state/agent-replay-127837d3-a000-4656-9453-2324ab353723.jsonl +5 -0
  4. package/.omc/state/agent-replay-15c409af-24fa-4026-88d3-a82dd1e68b87.jsonl +11 -0
  5. package/.omc/state/agent-replay-74afa912-1443-4011-b4f2-db9f055cdd9f.jsonl +3 -0
  6. package/.omc/state/agent-replay-8dd0ad2f-3a90-4158-b9de-ec8e19a7c983.jsonl +3 -0
  7. package/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
  8. package/.omc/state/idle-notif-cooldown.json +1 -1
  9. package/.omc/state/sessions/0d1515ce-4ebe-4ce8-a865-2897f68de168/pre-tool-advisory-throttle.json +10 -0
  10. package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/mission-state.json +61 -0
  11. package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/pre-tool-advisory-throttle.json +22 -0
  12. package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/skill-active-state.json +15 -0
  13. package/.omc/state/sessions/127837d3-a000-4656-9453-2324ab353723/subagent-tracking-state.json +26 -0
  14. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/last-tool-error-state.json +7 -0
  15. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/mission-state.json +89 -0
  16. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/pre-tool-advisory-throttle.json +34 -0
  17. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/ralph-state.json +13 -0
  18. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/skill-active-state.json +15 -0
  19. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/subagent-tracking-state.json +44 -0
  20. package/.omc/state/sessions/15c409af-24fa-4026-88d3-a82dd1e68b87/ultrawork-state.json +11 -0
  21. package/.omc/state/sessions/560ea519-574a-4713-b134-5bf6cfaf3c33/pre-tool-advisory-throttle.json +18 -0
  22. package/.omc/state/sessions/74afa912-1443-4011-b4f2-db9f055cdd9f/pre-tool-advisory-throttle.json +18 -0
  23. package/.omc/state/sessions/74afa912-1443-4011-b4f2-db9f055cdd9f/skill-active-state.json +15 -0
  24. package/.omc/state/sessions/74afa912-1443-4011-b4f2-db9f055cdd9f/subagent-tracking-state.json +17 -0
  25. package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +14 -0
  26. package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ralph-state.json +13 -0
  27. package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/subagent-tracking-state.json +17 -0
  28. package/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ultrawork-state.json +11 -0
  29. package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
  30. package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ralph-state.json +13 -0
  31. package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ultrawork-state.json +11 -0
  32. package/.turbo/turbo-build.log +3 -4
  33. package/.turbo/turbo-eslint-check.log +1 -2
  34. package/.turbo/turbo-prettier-check.log +1 -2
  35. package/.turbo/turbo-prettier-fix.log +1 -2
  36. package/.turbo/turbo-test.log +25 -18
  37. package/.turbo/turbo-ts-check.log +1 -2
  38. package/dist/cli.js +62 -54
  39. package/dist/cli.js.map +4 -4
  40. package/dist/mcp.js +36 -36
  41. package/dist/mcp.js.map +4 -4
  42. package/package.json +1 -1
  43. package/src/.omc/state/agent-replay-8dd0ad2f-3a90-4158-b9de-ec8e19a7c983.jsonl +11 -0
  44. package/src/.omc/state/idle-notif-cooldown.json +3 -0
  45. package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/mission-state.json +79 -0
  46. package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +22 -0
  47. package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ralph-state.json +13 -0
  48. package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/subagent-tracking-state.json +26 -0
  49. package/src/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/ultrawork-state.json +11 -0
  50. package/src/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
  51. package/src/commands/doctor/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +14 -0
  52. package/src/commands/doctor/__tests__/check-ide-installed.test.ts +142 -0
  53. package/src/commands/doctor/doctor.ts +104 -1
  54. package/src/commands/gh-release-deliver/gh-release-deliver.ts +32 -1
  55. package/src/commands/init/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +18 -0
  56. package/src/commands/init/__tests__/migrate-config.test.ts +132 -1
  57. package/src/commands/init/init.ts +19 -6
  58. package/src/commands/init/migrate-config.ts +104 -0
  59. package/src/commands/worktrees-add/index.ts +1 -2
  60. package/src/commands/worktrees-add/worktrees-add.ts +24 -60
  61. package/src/commands/worktrees-open/worktrees-open.ts +55 -68
  62. package/src/commands/worktrees-reload/__tests__/reload-cmux.test.ts +119 -0
  63. package/src/commands/worktrees-reload/index.ts +1 -0
  64. package/src/commands/worktrees-reload/worktrees-reload.ts +229 -0
  65. package/src/commands/worktrees-remove/worktrees-remove.ts +2 -44
  66. package/src/commands/worktrees-sync/worktrees-sync.ts +2 -44
  67. package/src/entry/cli.ts +32 -11
  68. package/src/integrations/cursor/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
  69. package/src/integrations/cursor/.omc/state/idle-notif-cooldown.json +3 -0
  70. package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +18 -0
  71. package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/subagent-tracking-state.json +17 -0
  72. package/src/integrations/cursor/index.ts +2 -0
  73. package/src/integrations/cursor/launch-cursor.ts +12 -0
  74. package/src/integrations/cursor/open-cursor-workspace.ts +69 -0
  75. package/src/integrations/ide/__tests__/ide-facade.test.ts +250 -0
  76. package/src/integrations/ide/add-ide-worktree-folders.ts +77 -0
  77. package/src/integrations/ide/index.ts +6 -0
  78. package/src/integrations/ide/open-ide-workspace.ts +51 -0
  79. package/src/integrations/ide/provider-label.ts +24 -0
  80. package/src/integrations/ide/remove-ide-worktree-folders.ts +80 -0
  81. package/src/integrations/ide/types.ts +32 -0
  82. package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +85 -0
  83. package/src/integrations/zed/add-folders-to-zed-workspace.ts +30 -0
  84. package/src/integrations/zed/index.ts +2 -0
  85. package/src/integrations/zed/open-zed-workspace.ts +57 -0
  86. package/src/lib/assert-never/assert-never.ts +15 -0
  87. package/src/lib/assert-never/index.ts +1 -0
  88. package/src/lib/git-utils/__tests__/git-primitives.test.ts +122 -1
  89. package/src/lib/git-utils/git-utils.ts +35 -0
  90. package/src/lib/git-utils/index.ts +2 -0
  91. package/src/lib/infra-kit-config/.omc/state/sessions/8dd0ad2f-3a90-4158-b9de-ec8e19a7c983/pre-tool-advisory-throttle.json +18 -0
  92. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +172 -9
  93. package/src/lib/infra-kit-config/index.ts +2 -1
  94. package/src/lib/infra-kit-config/infra-kit-config.ts +80 -22
  95. package/src/lib/package-config/.omc/state/sessions/dde22a92-a0e9-4948-905d-25c9a812ab35/last-tool-error-state.json +7 -0
  96. package/src/lib/package-config/.omc/state/sessions/dde22a92-a0e9-4948-905d-25c9a812ab35/pre-tool-advisory-throttle.json +10 -0
  97. package/src/lib/worktrees/remove-worktrees.ts +15 -0
  98. package/src/mcp/tools/index.ts +2 -0
  99. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,119 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { closeCmuxWorkspaceByTitle, listCmuxWorkspaceTitles, openCmuxWorkspaceWithLayout } from 'src/integrations/cmux'
4
+ import { getRepoName } from 'src/lib/git-utils'
5
+
6
+ import { closeCmux, reopenCmux } from '../worktrees-reload'
7
+
8
+ vi.mock('src/integrations/cmux', async (importActual) => {
9
+ const actual = await importActual<typeof import('src/integrations/cmux')>()
10
+
11
+ return {
12
+ ...actual,
13
+ listCmuxWorkspaceTitles: vi.fn(),
14
+ closeCmuxWorkspaceByTitle: vi.fn(),
15
+ openCmuxWorkspaceWithLayout: vi.fn(),
16
+ }
17
+ })
18
+
19
+ vi.mock('src/lib/git-utils', () => {
20
+ return {
21
+ getRepoName: vi.fn(),
22
+ getProjectRoot: vi.fn(),
23
+ getCurrentWorktrees: vi.fn(),
24
+ }
25
+ })
26
+
27
+ const REPO = 'hulyo-monorepo'
28
+ const WORKTREE_DIR = '/repos/project-worktrees'
29
+ const BRANCHES = ['release/v1.48.0', 'release/checkout-redesign']
30
+
31
+ const titleFor = {
32
+ versioned: `${REPO} 1.48.0`,
33
+ named: `${REPO} checkout-redesign`,
34
+ }
35
+
36
+ describe('closeCmux', () => {
37
+ beforeEach(() => {
38
+ vi.clearAllMocks()
39
+ vi.mocked(getRepoName).mockResolvedValue(REPO)
40
+ vi.mocked(closeCmuxWorkspaceByTitle).mockResolvedValue(undefined)
41
+ })
42
+
43
+ it('closes only the branches whose workspace is open in the snapshot', async () => {
44
+ vi.mocked(listCmuxWorkspaceTitles).mockResolvedValue(new Set([titleFor.versioned]))
45
+
46
+ const closed = await closeCmux({ currentBranches: BRANCHES, repoName: REPO })
47
+
48
+ expect(closed).toEqual([titleFor.versioned])
49
+ expect(closeCmuxWorkspaceByTitle).toHaveBeenCalledTimes(1)
50
+ expect(closeCmuxWorkspaceByTitle).toHaveBeenCalledWith(titleFor.versioned)
51
+ })
52
+
53
+ it('closes every open branch when all are present in the snapshot', async () => {
54
+ vi.mocked(listCmuxWorkspaceTitles).mockResolvedValue(new Set([titleFor.versioned, titleFor.named]))
55
+
56
+ const closed = await closeCmux({ currentBranches: BRANCHES, repoName: REPO })
57
+
58
+ expect(closed).toEqual([titleFor.versioned, titleFor.named])
59
+ expect(closeCmuxWorkspaceByTitle).toHaveBeenCalledTimes(2)
60
+ })
61
+
62
+ it('closes nothing when none are open', async () => {
63
+ vi.mocked(listCmuxWorkspaceTitles).mockResolvedValue(new Set())
64
+
65
+ const closed = await closeCmux({ currentBranches: BRANCHES, repoName: REPO })
66
+
67
+ expect(closed).toEqual([])
68
+ expect(closeCmuxWorkspaceByTitle).not.toHaveBeenCalled()
69
+ })
70
+
71
+ it('returns [] and skips the snapshot when there are no worktrees', async () => {
72
+ const closed = await closeCmux({ currentBranches: [], repoName: REPO })
73
+
74
+ expect(closed).toEqual([])
75
+ expect(listCmuxWorkspaceTitles).not.toHaveBeenCalled()
76
+ expect(closeCmuxWorkspaceByTitle).not.toHaveBeenCalled()
77
+ })
78
+ })
79
+
80
+ describe('reopenCmux', () => {
81
+ beforeEach(() => {
82
+ vi.clearAllMocks()
83
+ vi.mocked(getRepoName).mockResolvedValue(REPO)
84
+ vi.mocked(openCmuxWorkspaceWithLayout).mockResolvedValue(undefined)
85
+ })
86
+
87
+ it('force-opens a workspace for every branch (no dedup)', async () => {
88
+ const result = await reopenCmux({ worktreeDir: WORKTREE_DIR, currentBranches: BRANCHES, repoName: REPO })
89
+
90
+ expect(result.opened).toEqual([titleFor.versioned, titleFor.named])
91
+ expect(openCmuxWorkspaceWithLayout).toHaveBeenCalledTimes(2)
92
+ expect(openCmuxWorkspaceWithLayout).toHaveBeenCalledWith({
93
+ cwd: `${WORKTREE_DIR}/release/v1.48.0`,
94
+ title: titleFor.versioned,
95
+ })
96
+ expect(openCmuxWorkspaceWithLayout).toHaveBeenCalledWith({
97
+ cwd: `${WORKTREE_DIR}/release/checkout-redesign`,
98
+ title: titleFor.named,
99
+ })
100
+ })
101
+
102
+ it('continues past a per-branch failure and reopens the rest', async () => {
103
+ vi.mocked(openCmuxWorkspaceWithLayout)
104
+ .mockRejectedValueOnce(new Error('cmux boom'))
105
+ .mockResolvedValueOnce(undefined)
106
+
107
+ const result = await reopenCmux({ worktreeDir: WORKTREE_DIR, currentBranches: BRANCHES, repoName: REPO })
108
+
109
+ expect(result.opened).toEqual([titleFor.named])
110
+ expect(openCmuxWorkspaceWithLayout).toHaveBeenCalledTimes(2)
111
+ })
112
+
113
+ it('opens nothing when there are no worktrees', async () => {
114
+ const result = await reopenCmux({ worktreeDir: WORKTREE_DIR, currentBranches: [], repoName: REPO })
115
+
116
+ expect(result.opened).toEqual([])
117
+ expect(openCmuxWorkspaceWithLayout).not.toHaveBeenCalled()
118
+ })
119
+ })
@@ -0,0 +1 @@
1
+ export { worktreesReload, worktreesReloadMcpTool } from './worktrees-reload'
@@ -0,0 +1,229 @@
1
+ import { z } from 'zod'
2
+
3
+ import {
4
+ buildCmuxWorkspaceTitle,
5
+ canonicalizeCmuxTitle,
6
+ closeCmuxWorkspaceByTitle,
7
+ listCmuxWorkspaceTitles,
8
+ openCmuxWorkspaceWithLayout,
9
+ } from 'src/integrations/cmux'
10
+ import { ideProviderLabel, openIdeWorkspace } from 'src/integrations/ide'
11
+ import type { IdeProvider } from 'src/integrations/ide'
12
+ import { commandEcho } from 'src/lib/command-echo'
13
+ import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
14
+ import { OperationError } from 'src/lib/errors/operation-error'
15
+ import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
16
+ import { logger } from 'src/lib/logger'
17
+ import { defineMcpTool, textContent } from 'src/types'
18
+
19
+ interface WorktreesReloadResult {
20
+ closedCmux: string[]
21
+ openedCmux: string[]
22
+ ideProviders: IdeProvider[]
23
+ ideFoldersAdded: number
24
+ ideFoldersRemoved: number
25
+ }
26
+
27
+ /**
28
+ * Reload command: closes every open cmux workspace for the release worktrees on
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.
32
+ *
33
+ * The editor has no per-window close primitive, so its "reload" is a reconcile +
34
+ * relaunch of the configured workspace. Safe to run from any branch
35
+ * (non-destructive to git — only cmux/editor view state is touched).
36
+ */
37
+ export const worktreesReload = async () => {
38
+ commandEcho.start('worktrees-reload')
39
+
40
+ try {
41
+ const projectRoot = await getProjectRoot()
42
+ const worktreeDir = `${projectRoot}${WORKTREES_DIR_SUFFIX}`
43
+ const currentBranches = await getCurrentWorktrees('release')
44
+ const repoName = await getRepoName()
45
+
46
+ // Phase 1 — close (cmux only; Cursor has no per-window close).
47
+ const closedCmux = await closeCmux({ currentBranches, repoName })
48
+
49
+ // Phase 2 — reopen. cmux and Cursor are independent surfaces, so reopen them
50
+ // concurrently to shrink the window where the just-closed cmux workspaces are gone.
51
+ const [ideOutcomes, { opened: openedCmux }] = await Promise.all([
52
+ openIdeWorkspace({ projectRoot, worktreeDir, currentBranches, skipRelaunchWhenEmpty: true }),
53
+ reopenCmux({ worktreeDir, currentBranches, repoName }),
54
+ ])
55
+
56
+ const result: WorktreesReloadResult = {
57
+ closedCmux,
58
+ openedCmux,
59
+ ideProviders: ideOutcomes
60
+ .filter((outcome) => {
61
+ return outcome.ran
62
+ })
63
+ .map((outcome) => {
64
+ return outcome.provider
65
+ }),
66
+ ideFoldersAdded: ideOutcomes.reduce((sum, outcome) => {
67
+ return sum + outcome.added
68
+ }, 0),
69
+ ideFoldersRemoved: ideOutcomes.reduce((sum, outcome) => {
70
+ return sum + outcome.removed
71
+ }, 0),
72
+ }
73
+
74
+ logResults(result, {
75
+ ideRan: ideOutcomes.some((outcome) => {
76
+ return outcome.ran
77
+ }),
78
+ })
79
+
80
+ commandEcho.print()
81
+
82
+ return {
83
+ content: textContent(JSON.stringify(result, null, 2)),
84
+ structuredContent: { ...result },
85
+ }
86
+ } catch (error) {
87
+ logger.error({ error }, '❌ Error reloading worktrees')
88
+ throw new OperationError(error, {
89
+ operation: 'reload worktrees',
90
+ remediation: "run 'worktrees-list' to confirm the branches exist",
91
+ })
92
+ }
93
+ }
94
+
95
+ interface CloseCmuxArgs {
96
+ currentBranches: string[]
97
+ repoName: string
98
+ }
99
+
100
+ /**
101
+ * Close the cmux workspace for each release worktree that is currently open.
102
+ * Snapshots `listCmuxWorkspaceTitles()` up front purely so the returned list
103
+ * reflects which workspaces were actually open (the close itself is best-effort
104
+ * and per-title). Returns the titles a close was attempted for.
105
+ */
106
+ export const closeCmux = async (args: CloseCmuxArgs): Promise<string[]> => {
107
+ const { currentBranches, repoName } = args
108
+
109
+ if (currentBranches.length === 0) {
110
+ return []
111
+ }
112
+
113
+ const openBefore = await listCmuxWorkspaceTitles()
114
+
115
+ const closed: string[] = []
116
+
117
+ for (const branch of currentBranches) {
118
+ const title = buildCmuxWorkspaceTitle({ repoName, branch })
119
+
120
+ // openBefore holds canonical keys; match the built title the same way so a
121
+ // workspace stored under a drifted title (whitespace / old v-prefix) resolves.
122
+ if (!openBefore.has(canonicalizeCmuxTitle(title))) {
123
+ continue
124
+ }
125
+
126
+ await closeCmuxWorkspaceByTitle(title)
127
+ closed.push(title)
128
+ }
129
+
130
+ return closed
131
+ }
132
+
133
+ interface ReopenCmuxArgs {
134
+ worktreeDir: string
135
+ currentBranches: string[]
136
+ repoName: string
137
+ }
138
+
139
+ interface ReopenCmuxOutcome {
140
+ opened: string[]
141
+ }
142
+
143
+ /**
144
+ * Recreate a cmux workspace for every release worktree on disk. Force-opens each
145
+ * branch (no dedup against the live workspace list): reload has just awaited the
146
+ * 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
+ */
149
+ export const reopenCmux = async (args: ReopenCmuxArgs): Promise<ReopenCmuxOutcome> => {
150
+ const { worktreeDir, currentBranches, repoName } = args
151
+
152
+ const opened: string[] = []
153
+
154
+ for (const branch of currentBranches) {
155
+ const title = buildCmuxWorkspaceTitle({ repoName, branch })
156
+
157
+ try {
158
+ await openCmuxWorkspaceWithLayout({ cwd: `${worktreeDir}/${branch}`, title })
159
+ opened.push(title)
160
+ } catch (error) {
161
+ logger.warn({ error, title }, `⚠️ Failed to reopen cmux workspace for ${branch}`)
162
+ }
163
+ }
164
+
165
+ return { opened }
166
+ }
167
+
168
+ interface LogResultsContext {
169
+ ideRan: boolean
170
+ }
171
+
172
+ const logResults = (result: WorktreesReloadResult, context: LogResultsContext): void => {
173
+ if (result.closedCmux.length > 0) {
174
+ logger.info(`🧹 Closed ${result.closedCmux.length} cmux workspace(s)`)
175
+ }
176
+
177
+ if (context.ideRan) {
178
+ const ideLabels = result.ideProviders
179
+ .map((provider) => {
180
+ return ideProviderLabel(provider)
181
+ })
182
+ .join(', ')
183
+
184
+ if (result.ideFoldersAdded > 0) {
185
+ logger.info(`✅ Added ${result.ideFoldersAdded} folder(s) to ${ideLabels} workspace(s)`)
186
+ }
187
+
188
+ if (result.ideFoldersRemoved > 0) {
189
+ logger.info(`🧹 Removed ${result.ideFoldersRemoved} dangling folder(s) from ${ideLabels} workspace(s)`)
190
+ }
191
+ }
192
+
193
+ if (result.openedCmux.length > 0) {
194
+ logger.info('✅ Reopened cmux workspaces:')
195
+ for (const title of result.openedCmux) {
196
+ logger.info(title)
197
+ }
198
+ }
199
+
200
+ if (
201
+ !context.ideRan &&
202
+ result.closedCmux.length === 0 &&
203
+ result.openedCmux.length === 0 &&
204
+ result.ideFoldersAdded === 0 &&
205
+ result.ideFoldersRemoved === 0
206
+ ) {
207
+ logger.info('ℹ️ Nothing to reload')
208
+ }
209
+ }
210
+
211
+ // MCP Tool Registration
212
+ export const worktreesReloadMcpTool = defineMcpTool({
213
+ name: 'worktrees-reload',
214
+ 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
+ inputSchema: {},
217
+ outputSchema: {
218
+ closedCmux: z.array(z.string()).describe('Titles of cmux workspaces a close was attempted for (best-effort)'),
219
+ openedCmux: z.array(z.string()).describe('Titles of cmux workspaces reopened during this run'),
220
+ ideProviders: z
221
+ .array(z.string())
222
+ .describe('Configured IDE providers that were reloaded (cursor | zed); empty if none configured'),
223
+ ideFoldersAdded: z.number().describe('Total worktree folders added across all configured editor workspaces'),
224
+ ideFoldersRemoved: z
225
+ .number()
226
+ .describe('Total dangling worktree folders removed across all configured editor workspaces'),
227
+ },
228
+ handler: worktreesReload,
229
+ })
@@ -3,14 +3,13 @@ import confirm from '@inquirer/confirm'
3
3
  import process from 'node:process'
4
4
  import { z } from 'zod'
5
5
 
6
- import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
7
6
  import { getReleasePRsWithInfo } from 'src/integrations/gh'
7
+ import { removeIdeWorktreeFolders } from 'src/integrations/ide'
8
8
  import { commandEcho } from 'src/lib/command-echo'
9
9
  import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
10
10
  import { OperationError } from 'src/lib/errors/operation-error'
11
11
  import { assertManagementContext } from 'src/lib/git-guard'
12
12
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
13
- import { getInfraKitConfig } from 'src/lib/infra-kit-config'
14
13
  import { logger } from 'src/lib/logger'
15
14
  import { formatBranchName, parseReleaseRef } from 'src/lib/release-id'
16
15
  import { detectReleaseType, formatBranchChoices, getJiraDescriptions, releaseBranchLabels } from 'src/lib/release-utils'
@@ -119,7 +118,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
119
118
  pruneFolder: allSelected,
120
119
  })
121
120
 
122
- await syncCursorWorkspaceOnRemove({ removedWorktrees, worktreeDir, projectRoot })
121
+ await removeIdeWorktreeFolders({ projectRoot, worktreeDir, branches: removedWorktrees })
123
122
 
124
123
  logResults(removedWorktrees)
125
124
 
@@ -143,47 +142,6 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
143
142
  }
144
143
  }
145
144
 
146
- interface SyncCursorWorkspaceOnRemoveArgs {
147
- removedWorktrees: string[]
148
- worktreeDir: string
149
- projectRoot: string
150
- }
151
-
152
- /**
153
- * Strip removed worktrees from the configured Cursor workspace's `folders` array.
154
- * No-op if Cursor isn't configured, mode isn't "workspace", or no worktrees were removed.
155
- */
156
- const syncCursorWorkspaceOnRemove = async (args: SyncCursorWorkspaceOnRemoveArgs): Promise<void> => {
157
- const { removedWorktrees, worktreeDir, projectRoot } = args
158
-
159
- if (removedWorktrees.length === 0) {
160
- return
161
- }
162
-
163
- const config = await getInfraKitConfig()
164
- const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
165
-
166
- if (!cursorConfig || cursorConfig.mode !== 'workspace' || !cursorConfig.workspaceConfigPath) {
167
- return
168
- }
169
-
170
- const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
171
-
172
- const folderPaths = removedWorktrees.map((branch) => {
173
- return `${worktreeDir}/${branch}`
174
- })
175
-
176
- try {
177
- const { removed } = await removeFoldersFromCursorWorkspace({ workspacePath, folderPaths })
178
-
179
- if (removed.length > 0) {
180
- logger.info(`✅ Removed ${removed.length} folder(s) from ${workspacePath}`)
181
- }
182
- } catch (error) {
183
- logger.warn({ error }, `⚠️ Failed to update Cursor workspace at ${workspacePath}`)
184
- }
185
- }
186
-
187
145
  /**
188
146
  * Log the results of worktree management
189
147
  */
@@ -4,14 +4,13 @@ import { z } from 'zod'
4
4
  import { $ } from 'zx'
5
5
 
6
6
  import { buildCmuxWorkspaceTitle, closeCmuxWorkspaceByTitle } from 'src/integrations/cmux'
7
- import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
8
7
  import { getReleasePRs } from 'src/integrations/gh'
8
+ import { removeIdeWorktreeFolders } from 'src/integrations/ide'
9
9
  import { commandEcho } from 'src/lib/command-echo'
10
10
  import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
11
11
  import { OperationError } from 'src/lib/errors/operation-error'
12
12
  import { assertManagementContext } from 'src/lib/git-guard'
13
13
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
14
- import { getInfraKitConfig } from 'src/lib/infra-kit-config'
15
14
  import { logger } from 'src/lib/logger'
16
15
  import { isReleaseBranch } from 'src/lib/release-id'
17
16
  import { defineMcpTool, textContent } from 'src/types'
@@ -73,7 +72,7 @@ export const worktreesSync = async (options: WorktreeSyncArgs) => {
73
72
  repoName,
74
73
  })
75
74
 
76
- await syncCursorWorkspaceOnRemove({ removedWorktrees, worktreeDir, projectRoot })
75
+ await removeIdeWorktreeFolders({ projectRoot, worktreeDir, branches: removedWorktrees })
77
76
 
78
77
  logResults(removedWorktrees)
79
78
 
@@ -156,47 +155,6 @@ const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<string[]> =>
156
155
  return removed
157
156
  }
158
157
 
159
- interface SyncCursorWorkspaceOnRemoveArgs {
160
- removedWorktrees: string[]
161
- worktreeDir: string
162
- projectRoot: string
163
- }
164
-
165
- /**
166
- * Strip removed worktrees from the configured Cursor workspace's `folders` array.
167
- * No-op if Cursor isn't configured, mode isn't "workspace", or no worktrees were removed.
168
- */
169
- const syncCursorWorkspaceOnRemove = async (args: SyncCursorWorkspaceOnRemoveArgs): Promise<void> => {
170
- const { removedWorktrees, worktreeDir, projectRoot } = args
171
-
172
- if (removedWorktrees.length === 0) {
173
- return
174
- }
175
-
176
- const config = await getInfraKitConfig()
177
- const cursorConfig = config.ide?.provider === 'cursor' ? config.ide.config : undefined
178
-
179
- if (!cursorConfig || cursorConfig.mode !== 'workspace' || !cursorConfig.workspaceConfigPath) {
180
- return
181
- }
182
-
183
- const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)
184
-
185
- const folderPaths = removedWorktrees.map((branch) => {
186
- return `${worktreeDir}/${branch}`
187
- })
188
-
189
- try {
190
- const { removed: removedEntries } = await removeFoldersFromCursorWorkspace({ workspacePath, folderPaths })
191
-
192
- if (removedEntries.length > 0) {
193
- logger.info(`✅ Removed ${removedEntries.length} folder(s) from ${workspacePath}`)
194
- }
195
- } catch (error) {
196
- logger.warn({ error }, `⚠️ Failed to update Cursor workspace at ${workspacePath}`)
197
- }
198
- }
199
-
200
158
  /**
201
159
  * Log the results of worktree management
202
160
  */
package/src/entry/cli.ts CHANGED
@@ -18,12 +18,14 @@ import { init } from 'src/commands/init'
18
18
  import { releaseCreate } from 'src/commands/release-create'
19
19
  import { releaseDescEdit } from 'src/commands/release-desc-edit'
20
20
  import { version } from 'src/commands/version'
21
- import { CURSOR_MODES, worktreesAdd } from 'src/commands/worktrees-add'
22
- import type { CursorMode } from 'src/commands/worktrees-add'
21
+ import { worktreesAdd } from 'src/commands/worktrees-add'
23
22
  import { worktreesList } from 'src/commands/worktrees-list'
24
23
  import { worktreesOpen } from 'src/commands/worktrees-open'
24
+ import { worktreesReload } from 'src/commands/worktrees-reload'
25
25
  import { worktreesRemove } from 'src/commands/worktrees-remove'
26
26
  import { worktreesSync } from 'src/commands/worktrees-sync'
27
+ import { IDE_MODES } from 'src/integrations/ide'
28
+ import type { IdeMode } from 'src/integrations/ide'
27
29
  import { logger } from 'src/lib/logger'
28
30
  import { parseReleaseSpec } from 'src/lib/version-utils'
29
31
  import type { ReleaseInput } from 'src/lib/version-utils'
@@ -34,7 +36,7 @@ const collectReleaseSpec = (value: string, prev: string[]): string[] => {
34
36
  return [...prev, value]
35
37
  }
36
38
 
37
- const normalizeCursorMode = (value: unknown): CursorMode | undefined => {
39
+ const normalizeIdeMode = (value: unknown, flagName: '--ide' | '--cursor'): IdeMode | undefined => {
38
40
  if (typeof value === 'undefined') {
39
41
  return undefined
40
42
  }
@@ -47,11 +49,11 @@ const normalizeCursorMode = (value: unknown): CursorMode | undefined => {
47
49
  return 'none'
48
50
  }
49
51
 
50
- if (typeof value === 'string' && (CURSOR_MODES as readonly string[]).includes(value)) {
51
- return value as CursorMode
52
+ if (typeof value === 'string' && (IDE_MODES as readonly string[]).includes(value)) {
53
+ return value as IdeMode
52
54
  }
53
55
 
54
- throw new Error(`Invalid --cursor value "${String(value)}". Expected one of: ${CURSOR_MODES.join(', ')}.`)
56
+ throw new Error(`Invalid ${flagName} value "${String(value)}". Expected one of: ${IDE_MODES.join(', ')}.`)
55
57
  }
56
58
 
57
59
  const runProgram = async (argv?: string[]): Promise<void> => {
@@ -175,18 +177,23 @@ program
175
177
  .option('-y, --yes', 'Skip confirmation prompt')
176
178
  .option('-a, --all', 'Select all active release branches')
177
179
  .option('-v, --versions <versions>', 'Specify versions by comma, e.g. 1.2.5, 1.2.6')
178
- .option('-c, --cursor [mode]', 'Cursor mode for created worktrees: workspace (default) | windows | none')
179
- .option('--no-cursor', 'Skip Cursor (alias for --cursor none)')
180
+ .option('-i, --ide [mode]', 'Editor mode for created worktrees: workspace (default) | none')
181
+ .option('--no-ide', 'Skip the editor (alias for --ide none)')
182
+ .option('-c, --cursor [mode]', 'Deprecated alias for --ide')
183
+ .option('--no-cursor', 'Deprecated alias for --no-ide')
180
184
  .option('-g, --github-desktop', 'Open created worktrees in GitHub Desktop')
181
185
  .option('--no-github-desktop', 'Skip GitHub Desktop prompt')
182
186
  .option('-m, --cmux', 'Open created worktrees in cmux (3-pane layout)')
183
187
  .option('--no-cmux', 'Skip cmux prompt')
184
188
  .action(async (options) => {
189
+ // `--ide` wins over the deprecated `--cursor` alias when both are provided.
190
+ const ide = normalizeIdeMode(options.ide, '--ide') ?? normalizeIdeMode(options.cursor, '--cursor')
191
+
185
192
  await worktreesAdd({
186
193
  confirmedCommand: options.yes,
187
194
  all: options.all,
188
195
  versions: options.versions,
189
- cursor: normalizeCursorMode(options.cursor),
196
+ ide,
190
197
  githubDesktop: options.githubDesktop,
191
198
  cmux: options.cmux,
192
199
  })
@@ -211,11 +218,18 @@ program
211
218
 
212
219
  program
213
220
  .command('worktrees-open')
214
- .description('Open Cursor + cmux for existing release worktrees (cold-start restore)')
221
+ .description('Open the configured editor (Cursor/Zed) + cmux for existing release worktrees (cold-start restore)')
215
222
  .action(async () => {
216
223
  await worktreesOpen()
217
224
  })
218
225
 
226
+ program
227
+ .command('worktrees-reload')
228
+ .description('Close all cmux/editor worktree windows, then reopen the current release worktrees')
229
+ .action(async () => {
230
+ await worktreesReload()
231
+ })
232
+
219
233
  const configCmd = program.command('config').description('Manage infra-kit configuration files')
220
234
 
221
235
  configCmd
@@ -305,7 +319,14 @@ if (process.argv.length <= 2) {
305
319
  'release-deploy-selected',
306
320
  'release-deliver',
307
321
  ]
308
- const worktreeCommands = ['worktrees-add', 'worktrees-list', 'worktrees-open', 'worktrees-remove', 'worktrees-sync']
322
+ const worktreeCommands = [
323
+ 'worktrees-add',
324
+ 'worktrees-list',
325
+ 'worktrees-open',
326
+ 'worktrees-reload',
327
+ 'worktrees-remove',
328
+ 'worktrees-sync',
329
+ ]
309
330
  const envCommands = [
310
331
  'audit',
311
332
  'doctor',
@@ -0,0 +1,2 @@
1
+ {"t":0,"agent":"system","event":"skill_invoked","skill_name":"oh-my-claudecode:cancel"}
2
+ {"t":0,"agent":"a56b8e3","agent_type":"unknown","event":"agent_stop","success":true,"duration_ms":0}
@@ -0,0 +1,3 @@
1
+ {
2
+ "lastSentAt": "2026-06-22T07:10:20.378Z"
3
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": 1,
3
+ "entries": {
4
+ "79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
5
+ "last_emitted_at_ms": 1782112155382,
6
+ "message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
7
+ },
8
+ "466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
9
+ "last_emitted_at_ms": 1782112102061,
10
+ "message": "Read multiple files in parallel when possible for faster analysis."
11
+ },
12
+ "445ed27a3872b681d98190bae61ccb84954e1bc4e140df5370be958dee776b3a": {
13
+ "last_emitted_at_ms": 1782112093035,
14
+ "message": "Verify changes work after editing. Test functionality before marking complete."
15
+ }
16
+ },
17
+ "updated_at": "2026-06-22T07:09:15.382Z"
18
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "agents": [
3
+ {
4
+ "agent_id": "a56b8e33689099584",
5
+ "agent_type": "unknown",
6
+ "started_at": "2026-06-22T07:13:23.151Z",
7
+ "parent_mode": "none",
8
+ "status": "completed",
9
+ "completed_at": "2026-06-22T07:13:23.151Z",
10
+ "duration_ms": 0
11
+ }
12
+ ],
13
+ "total_spawned": 0,
14
+ "total_completed": 1,
15
+ "total_failed": 0,
16
+ "last_updated": "2026-06-22T07:13:23.253Z"
17
+ }
@@ -1,4 +1,6 @@
1
1
  export { addFoldersToCursorWorkspace } from './add-folders-to-workspace'
2
+ export { launchCursor } from './launch-cursor'
3
+ export { openCursorWorkspace } from './open-cursor-workspace'
2
4
  export { reconcileCursorWorkspaceFolders } from './reconcile-workspace-folders'
3
5
  export { removeFoldersFromCursorWorkspace } from './remove-folders-from-workspace'
4
6
  export { resolveCursorWorkspacePath } from './resolve-workspace-path'
@@ -0,0 +1,12 @@
1
+ import { $ } from 'zx'
2
+
3
+ /**
4
+ * Spawns Cursor against a `.code-workspace` file (or any path). Centralised here
5
+ * — rather than inlined at each call site — so the editor spawn lives inside the
6
+ * cursor provider module and is covered by a single `vi.mock('src/integrations/cursor')`.
7
+ * Keeping it out of the IDE-orchestration layer is what stops tests that mock the
8
+ * provider (but not `zx`) from shelling out to a real editor.
9
+ */
10
+ export const launchCursor = async (workspacePath: string): Promise<void> => {
11
+ await $`cursor ${workspacePath}`
12
+ }