infra-kit 0.1.102 → 0.1.107

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 (147) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-0a58307d-2a37-4c69-851c-83a646502d62.jsonl +1 -0
  3. package/.omc/state/agent-replay-11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc.jsonl +16 -0
  4. package/.omc/state/agent-replay-4cf1c186-81b2-497c-b002-d7f84e7839f3.jsonl +9 -0
  5. package/.omc/state/agent-replay-5c4ab554-64f1-42ae-83e3-21e0237e955c.jsonl +11 -0
  6. package/.omc/state/agent-replay-a60ac2ec-afbd-449f-a540-6df287392fc2.jsonl +1 -0
  7. package/.omc/state/agent-replay-afc6290b-40d3-4bef-b3b6-14484c034ab9.jsonl +14 -0
  8. package/.omc/state/agent-replay-be37e426-6fc8-47f4-8178-221c8494551c.jsonl +3 -0
  9. package/.omc/state/agent-replay-c967c819-3d1c-447b-ab48-56a8448ef9f8.jsonl +2 -0
  10. package/.omc/state/agent-replay-e947a3c6-989d-4a60-91dd-6b0ddd827b2d.jsonl +3 -0
  11. package/.omc/state/idle-notif-cooldown.json +3 -0
  12. package/.omc/state/last-tool-error.json +4 -4
  13. package/.omc/state/mission-state.json +53 -0
  14. package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
  15. package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/subagent-tracking-state.json +7 -0
  16. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/last-tool-error-state.json +7 -0
  17. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/mission-state.json +117 -0
  18. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/pre-tool-advisory-throttle.json +42 -0
  19. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/subagent-tracking-state.json +53 -0
  20. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/last-tool-error-state.json +7 -0
  21. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/pre-tool-advisory-throttle.json +18 -0
  22. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/subagent-tracking-state.json +7 -0
  23. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/mission-state.json +117 -0
  24. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/pre-tool-advisory-throttle.json +18 -0
  25. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/subagent-tracking-state.json +17 -0
  26. package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +18 -0
  27. package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/subagent-tracking-state.json +7 -0
  28. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/last-tool-error-state.json +7 -0
  29. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/mission-state.json +89 -0
  30. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +34 -0
  31. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ralph-state.json +13 -0
  32. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/skill-active-state.json +15 -0
  33. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/subagent-tracking-state.json +35 -0
  34. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ultrawork-state.json +11 -0
  35. package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +10 -0
  36. package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/subagent-tracking-state.json +7 -0
  37. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/last-tool-error-state.json +7 -0
  38. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/pre-tool-advisory-throttle.json +10 -0
  39. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/subagent-tracking-state.json +26 -0
  40. package/.omc/state/subagent-tracking.json +14 -4
  41. package/.turbo/turbo-build.log +7 -0
  42. package/.turbo/turbo-check.log +14 -0
  43. package/.turbo/turbo-prettier-fix.log +2 -1
  44. package/.turbo/turbo-test.log +28 -5
  45. package/.turbo/turbo-validate.log +14 -0
  46. package/dist/cli.js +88 -74
  47. package/dist/cli.js.map +4 -4
  48. package/dist/entry/index.d.ts +2 -0
  49. package/dist/index.js +2 -0
  50. package/dist/index.js.map +7 -0
  51. package/dist/lib/package-config/package-config.d.ts +71 -0
  52. package/dist/mcp.js +43 -41
  53. package/dist/mcp.js.map +4 -4
  54. package/eslint.config.js +1 -1
  55. package/infra-kit.config.ts +5 -0
  56. package/package.json +20 -13
  57. package/scripts/build.js +32 -3
  58. package/src/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
  59. package/src/commands/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +14 -0
  60. package/src/commands/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +18 -0
  61. package/src/commands/audit/__tests__/audit.test.ts +59 -0
  62. package/src/commands/audit/audit.ts +177 -0
  63. package/src/commands/audit/index.ts +1 -0
  64. package/src/commands/config/config.ts +49 -7
  65. package/src/commands/doctor/__tests__/agent-files.test.ts +110 -0
  66. package/src/commands/doctor/doctor.ts +69 -4
  67. package/src/commands/env-clear/env-clear.ts +1 -1
  68. package/src/commands/env-list/env-list.ts +3 -3
  69. package/src/commands/env-load/env-load.ts +1 -1
  70. package/src/commands/env-status/env-status.ts +1 -1
  71. package/src/commands/gh-merge-dev/gh-merge-dev.ts +3 -8
  72. package/src/commands/gh-release-deliver/gh-release-deliver.ts +47 -21
  73. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +13 -7
  74. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +12 -6
  75. package/src/commands/gh-release-list/gh-release-list.ts +19 -8
  76. package/src/commands/init/__tests__/agent-files.test.ts +147 -0
  77. package/src/commands/init/__tests__/migrate-config.test.ts +160 -0
  78. package/src/commands/init/agent-files.ts +199 -0
  79. package/src/commands/init/index.ts +7 -0
  80. package/src/commands/init/init.ts +82 -60
  81. package/src/commands/init/migrate-config.ts +146 -0
  82. package/src/commands/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  83. package/src/commands/release-create/__tests__/release-create.test.ts +55 -0
  84. package/src/commands/release-create/release-create.ts +142 -38
  85. package/src/commands/release-desc-edit/release-desc-edit.ts +28 -8
  86. package/src/commands/version/version.ts +1 -1
  87. package/src/commands/worktrees-add/worktrees-add.ts +7 -12
  88. package/src/commands/worktrees-list/worktrees-list.ts +13 -5
  89. package/src/commands/worktrees-open/worktrees-open.ts +1 -1
  90. package/src/commands/worktrees-remove/worktrees-remove.ts +6 -10
  91. package/src/commands/worktrees-sync/worktrees-sync.ts +3 -5
  92. package/src/entry/cli.ts +50 -7
  93. package/src/entry/index.ts +5 -0
  94. package/src/integrations/cmux/open-workspace-with-layout.ts +4 -4
  95. package/src/integrations/cmux/workspace-title.ts +10 -4
  96. package/src/integrations/doppler/doppler-project.ts +1 -1
  97. package/src/integrations/gh/gh-release-prs/__tests__/gh-release-prs.test.ts +115 -0
  98. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +49 -32
  99. package/src/lib/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +14 -0
  100. package/src/lib/constants/index.ts +15 -0
  101. package/src/lib/git-utils/__tests__/git-utils.test.ts +49 -0
  102. package/src/lib/git-utils/git-utils.ts +3 -1
  103. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +270 -0
  104. package/src/lib/infra-kit-config/index.ts +7 -1
  105. package/src/lib/infra-kit-config/infra-kit-config.ts +46 -28
  106. package/src/lib/managed-block/__tests__/managed-block.test.ts +121 -0
  107. package/src/lib/managed-block/index.ts +8 -0
  108. package/src/lib/managed-block/managed-block.ts +145 -0
  109. package/src/lib/package-config/__tests__/package-config.test.ts +95 -0
  110. package/src/lib/package-config/index.ts +3 -0
  111. package/src/lib/package-config/package-config-schema.ts +19 -0
  112. package/src/lib/package-config/package-config.ts +99 -0
  113. package/src/lib/package-validator/__tests__/package-validator.test.ts +263 -0
  114. package/src/lib/package-validator/checks/__tests__/checks.test.ts +130 -0
  115. package/src/lib/package-validator/checks/config-check.ts +30 -0
  116. package/src/lib/package-validator/checks/files-check.ts +29 -0
  117. package/src/lib/package-validator/checks/index.ts +4 -0
  118. package/src/lib/package-validator/checks/scripts-check.ts +23 -0
  119. package/src/lib/package-validator/checks/turbo-check.ts +47 -0
  120. package/src/lib/package-validator/fs-utils.ts +18 -0
  121. package/src/lib/package-validator/index.ts +3 -0
  122. package/src/lib/package-validator/loader/config-loader.ts +77 -0
  123. package/src/lib/package-validator/loader/index.ts +2 -0
  124. package/src/lib/package-validator/loader/package-discovery.ts +98 -0
  125. package/src/lib/package-validator/package-validator.ts +48 -0
  126. package/src/lib/package-validator/types.ts +15 -0
  127. package/src/lib/release-id/__tests__/release-id.test.ts +351 -0
  128. package/src/lib/release-id/__tests__/versioned-regression.test.ts +69 -0
  129. package/src/lib/release-id/index.ts +15 -0
  130. package/src/lib/release-id/release-id.ts +257 -0
  131. package/src/lib/release-utils/__tests__/release-utils.test.ts +122 -0
  132. package/src/lib/release-utils/index.ts +4 -0
  133. package/src/lib/release-utils/release-utils.ts +85 -17
  134. package/src/lib/version-utils/__tests__/load-existing-versions.test.ts +37 -0
  135. package/src/lib/version-utils/__tests__/next-version.test.ts +119 -13
  136. package/src/lib/version-utils/index.ts +3 -0
  137. package/src/lib/version-utils/load-existing-versions.ts +29 -10
  138. package/src/lib/version-utils/next-version.ts +67 -12
  139. package/src/lib/version-utils/version-utils.ts +13 -4
  140. package/src/mcp/tools/index.ts +2 -0
  141. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  142. package/src/types.ts +1 -1
  143. package/tsconfig.tsbuildinfo +1 -1
  144. package/src/lib/__tests__/infra-kit-config.test.ts +0 -231
  145. /package/src/integrations/{clickup → linear}/.gitkeep +0 -0
  146. /package/src/lib/{__tests__ → constants/__tests__}/constants.test.ts +0 -0
  147. /package/src/lib/{constants.ts → constants/constants.ts} +0 -0
@@ -1,13 +1,14 @@
1
1
  import confirm from '@inquirer/confirm'
2
2
  import select from '@inquirer/select'
3
3
  import process from 'node:process'
4
- import { z } from 'zod/v4'
4
+ import { z } from 'zod'
5
5
  import { question } from 'zx'
6
6
 
7
7
  import { loadJiraConfig } from 'src/integrations/jira'
8
8
  import { commandEcho } from 'src/lib/command-echo'
9
9
  import { OperationError } from 'src/lib/errors/operation-error'
10
10
  import { logger } from 'src/lib/logger'
11
+ import { InvalidReleaseNameError, displayLabel, validateName } from 'src/lib/release-id'
11
12
  import { createSingleRelease, prepareGitForRelease } from 'src/lib/release-utils'
12
13
  import type { ReleaseCreationResult, ReleaseType } from 'src/lib/release-utils'
13
14
  import {
@@ -18,12 +19,12 @@ import {
18
19
  parseVersion,
19
20
  resolveReleaseEntries,
20
21
  } from 'src/lib/version-utils'
21
- import type { ReleaseEntry, SemVer } from 'src/lib/version-utils'
22
+ import type { ReleaseEntry, ReleaseInput, SemVer } from 'src/lib/version-utils'
22
23
  import { defineMcpTool, textContent } from 'src/types'
23
24
  import type { RequiredConfirmedOptionArg } from 'src/types'
24
25
 
25
26
  interface ReleaseCreateArgs extends RequiredConfirmedOptionArg {
26
- releases?: ReleaseEntry[]
27
+ releases?: ReleaseInput[]
27
28
  }
28
29
 
29
30
  const VERSION_PROMPT_HINT = '"1.2.5" or "next"'
@@ -38,7 +39,7 @@ const trySuggestNext = (known: SemVer[], type: ReleaseType): string | null => {
38
39
  }
39
40
  }
40
41
 
41
- const resolveOrExit = (entries: ReleaseEntry[], known: SemVer[]): ReleaseEntry[] => {
42
+ const resolveOrExit = (entries: ReleaseInput[], known: SemVer[]): ReleaseEntry[] => {
42
43
  try {
43
44
  return resolveReleaseEntries(entries, known)
44
45
  } catch (err) {
@@ -49,20 +50,80 @@ const resolveOrExit = (entries: ReleaseEntry[], known: SemVer[]): ReleaseEntry[]
49
50
  })
50
51
  }
51
52
 
53
+ if (err instanceof InvalidReleaseNameError) {
54
+ throw new OperationError(err, {
55
+ operation: 'validate release name',
56
+ remediation:
57
+ 'use a kebab-case name like "checkout-redesign" (lowercase, digits, single hyphens, not a reserved word)',
58
+ })
59
+ }
60
+
52
61
  throw err
53
62
  }
54
63
  }
55
64
 
65
+ const promptForVersionInput = async (running: SemVer[], type: ReleaseType): Promise<string> => {
66
+ const suggestion = trySuggestNext(running, type)
67
+ const defaultHint = suggestion ? ` [${suggestion}]` : ''
68
+ const versionAnswer = (await question(` Version (e.g. ${VERSION_PROMPT_HINT})${defaultHint}: `)).trim()
69
+ const versionInput = versionAnswer === '' ? (suggestion ?? '') : versionAnswer
70
+
71
+ if (versionInput === '') {
72
+ logger.error('No version provided. Exiting...')
73
+ process.exit(1)
74
+ }
75
+
76
+ return versionInput
77
+ }
78
+
79
+ const promptForNameInput = async (): Promise<string> => {
80
+ const name = (await question(' Name (kebab-case, e.g. "checkout-redesign"): ')).trim()
81
+
82
+ if (name === '') {
83
+ logger.error('No name provided. Exiting...')
84
+ process.exit(1)
85
+ }
86
+
87
+ try {
88
+ validateName(name)
89
+ } catch (err) {
90
+ const reason = err instanceof Error ? err.message : String(err)
91
+
92
+ logger.error(`${reason} Exiting...`)
93
+ process.exit(1)
94
+ }
95
+
96
+ return name
97
+ }
98
+
56
99
  const promptForReleasesInteractive = async (ensureKnown: () => Promise<SemVer[]>): Promise<ReleaseEntry[]> => {
57
100
  commandEcho.setInteractive()
58
101
 
59
- const baseKnown = await ensureKnown()
60
- const running: SemVer[] = [...baseKnown]
102
+ let baseKnown: SemVer[] | null = null
103
+ const running: SemVer[] = []
104
+ const ensureRunning = async (): Promise<SemVer[]> => {
105
+ if (baseKnown === null) {
106
+ baseKnown = await ensureKnown()
107
+ running.push(...baseKnown)
108
+ }
109
+
110
+ return running
111
+ }
112
+
61
113
  const entries: ReleaseEntry[] = []
62
114
  let addAnother = true
63
115
 
64
116
  while (addAnother) {
65
117
  const ordinal = entries.length + 1
118
+ const kind = await select<'version' | 'name'>({
119
+ message: `Release #${ordinal} — version or name?`,
120
+ choices: [
121
+ { name: 'version (semver / next)', value: 'version' },
122
+ { name: 'name (free-form)', value: 'name' },
123
+ ],
124
+ default: 'version',
125
+ })
126
+
66
127
  const type = await select<ReleaseType>({
67
128
  message: `Release #${ordinal} — select type:`,
68
129
  choices: [
@@ -72,19 +133,22 @@ const promptForReleasesInteractive = async (ensureKnown: () => Promise<SemVer[]>
72
133
  default: 'regular',
73
134
  })
74
135
 
75
- const suggestion = trySuggestNext(running, type)
76
- const defaultHint = suggestion ? ` [${suggestion}]` : ''
77
- const versionAnswer = (await question(` Version (e.g. ${VERSION_PROMPT_HINT})${defaultHint}: `)).trim()
78
- const versionInput = versionAnswer === '' ? (suggestion ?? '') : versionAnswer
136
+ let resolved: ReleaseEntry
79
137
 
80
- if (versionInput === '') {
81
- logger.error('No version provided. Exiting...')
82
- process.exit(1)
83
- }
138
+ if (kind === 'name') {
139
+ const name = await promptForNameInput()
140
+
141
+ resolved = resolveOrExit([{ name, type }], [])[0] as ReleaseEntry
142
+ } else {
143
+ // Versions may need prior versions for "next"; load lazily.
144
+ const versionInput = await promptForVersionInput(await ensureRunning(), type)
84
145
 
85
- const resolved = resolveOrExit([{ version: versionInput, type }], running)[0] as ReleaseEntry
146
+ resolved = resolveOrExit([{ version: versionInput, type }], running)[0] as ReleaseEntry
86
147
 
87
- running.push(parseVersion(`v${resolved.version}`))
148
+ if (resolved.id.kind === 'version') {
149
+ running.push(parseVersion(`v${resolved.id.raw}`))
150
+ }
151
+ }
88
152
 
89
153
  const description = (await question(' Description (optional, press Enter to skip): ')).trim()
90
154
 
@@ -97,7 +161,8 @@ const promptForReleasesInteractive = async (ensureKnown: () => Promise<SemVer[]>
97
161
  }
98
162
 
99
163
  const formatReleaseSummary = (entry: ReleaseEntry): string => {
100
- const parts = [`v${entry.version}`, entry.type]
164
+ const label = entry.id.kind === 'version' ? `v${entry.id.raw}` : entry.id.name
165
+ const parts = [label, entry.type]
101
166
 
102
167
  if (entry.description) parts.push(entry.description)
103
168
 
@@ -106,9 +171,17 @@ const formatReleaseSummary = (entry: ReleaseEntry): string => {
106
171
 
107
172
  const echoReleases = (entries: ReleaseEntry[]): void => {
108
173
  for (const entry of entries) {
174
+ if (entry.id.kind === 'name') {
175
+ // Named releases echo the --name form; description is not part of the
176
+ // flag and is set later via release-desc-edit / interactively.
177
+ commandEcho.addOption('--name', entry.id.name)
178
+
179
+ continue
180
+ }
181
+
109
182
  const spec = entry.description
110
- ? `${entry.version}:${entry.type}:${entry.description}`
111
- : `${entry.version}:${entry.type}`
183
+ ? `${entry.id.raw}:${entry.type}:${entry.description}`
184
+ : `${entry.id.raw}:${entry.type}`
112
185
 
113
186
  commandEcho.addOption('--release', spec)
114
187
  }
@@ -120,7 +193,7 @@ interface FailedRelease {
120
193
  }
121
194
 
122
195
  const collectEntries = async (
123
- inputReleases: ReleaseEntry[] | undefined,
196
+ inputReleases: ReleaseInput[] | undefined,
124
197
  ensureKnown: () => Promise<SemVer[]>,
125
198
  ): Promise<ReleaseEntry[]> => {
126
199
  if (inputReleases && inputReleases.length > 0) {
@@ -168,31 +241,33 @@ const executeOne = async (
168
241
  args: ExecuteOneArgs,
169
242
  ): Promise<{ result?: ReleaseCreationResult; failure?: FailedRelease }> => {
170
243
  const { entry, jiraConfig } = args
244
+ const label = displayLabel(entry.id)
245
+ const prTitleLabel = entry.id.kind === 'version' ? `v${entry.id.raw}` : entry.id.name
171
246
 
172
247
  try {
173
248
  await prepareGitForRelease(entry.type)
174
249
 
175
250
  const result = await createSingleRelease({
176
- version: entry.version,
251
+ id: entry.id,
177
252
  jiraConfig,
178
253
  description: entry.description,
179
254
  type: entry.type,
180
255
  })
181
256
 
182
- logger.info(`✅ Successfully created release: v${entry.version} (${entry.type})`)
257
+ logger.info(`✅ Successfully created release: ${prTitleLabel} (${entry.type})`)
183
258
  logger.info(`🔗 GitHub PR: ${result.prUrl}`)
184
259
  logger.info(`🔗 Jira Version: ${result.jiraVersionUrl}\n`)
185
260
 
186
261
  return { result }
187
262
  } catch (error) {
188
263
  const err = new OperationError(error, {
189
- operation: `create release v${entry.version} (${entry.type})`,
190
- remediation: 'verify the version is unique and the base branch is clean',
264
+ operation: `create release ${prTitleLabel} (${entry.type})`,
265
+ remediation: 'verify the version or name is unique and the base branch is clean',
191
266
  })
192
267
 
193
268
  logger.error(`❌ ${err.message}\n`)
194
269
 
195
- return { failure: { version: entry.version, error: err.message } }
270
+ return { failure: { version: label, error: err.message } }
196
271
  }
197
272
  }
198
273
 
@@ -272,24 +347,53 @@ export const releaseCreate = async (args: ReleaseCreateArgs) => {
272
347
  export const releaseCreateMcpTool = defineMcpTool({
273
348
  name: 'release-create',
274
349
  description:
275
- 'Create one or more releases in a single call. Each entry in "releases" carries its own version, type (regular|hotfix, default regular), and optional description, so regular and hotfix releases can be mixed in the same invocation. For each release this tool switches to the appropriate base branch (dev for regular, main for hotfix), cuts the release branch, opens a GitHub release PR, and creates the matching Jira fix version. The literal token "next" auto-increments from the union of remote release branches and Jira fix versions (regular bumps minor + resets patch; hotfix bumps patch on the highest minor); multiple "next" tokens advance sequentially across mixed types. Confirmation is auto-skipped for MCP calls, so the caller is responsible for gating. Continues on per-release failure and reports successes/failures.',
350
+ 'Create one or more releases in a single call. Each entry in "releases" carries EITHER a "version" (semver or the literal token "next") OR a "name" (free-form kebab-case identifier) — exactly one is required and they are mutually exclusive. Each entry also has its own type (regular|hotfix, default regular) and optional description, so regular and hotfix releases can be mixed in the same invocation. For each release this tool switches to the appropriate base branch (dev for regular, main for hotfix), cuts the release branch (release/v<semver> for versions, release/n/<name> for names), opens a GitHub release PR, and creates the matching Jira fix version (v<semver> for versions, <name> for names). The literal token "next" auto-increments from the union of remote release branches and Jira fix versions (regular bumps minor + resets patch; hotfix bumps patch on the highest minor); multiple "next" tokens advance sequentially across mixed types. Named releases never auto-bump and "next" is version-only. Confirmation is auto-skipped for MCP calls, so the caller is responsible for gating. Continues on per-release failure and reports successes/failures.',
276
351
  inputSchema: {
277
352
  releases: z
278
353
  .array(
279
- z.object({
280
- version: z
281
- .string()
282
- .describe('Version to create (e.g., "1.2.5") or the literal token "next" for auto-increment.'),
283
- type: z
284
- .enum(['regular', 'hotfix'])
285
- .optional()
286
- .default('regular')
287
- .describe('Release type: "regular" (branches off dev) or "hotfix" (branches off main).'),
288
- description: z.string().optional().describe('Optional description for the Jira version.'),
289
- }),
354
+ z
355
+ .object({
356
+ version: z
357
+ .string()
358
+ .optional()
359
+ .describe(
360
+ 'Version to create (e.g., "1.2.5") or the literal token "next" for auto-increment. Mutually exclusive with "name".',
361
+ ),
362
+ name: z
363
+ .string()
364
+ .optional()
365
+ .describe(
366
+ 'Free-form kebab-case release name (e.g., "checkout-redesign"). Mutually exclusive with "version". Named releases never auto-bump.',
367
+ ),
368
+ type: z
369
+ .enum(['regular', 'hotfix'])
370
+ .optional()
371
+ .default('regular')
372
+ .describe('Release type: "regular" (branches off dev) or "hotfix" (branches off main).'),
373
+ description: z.string().optional().describe('Optional description for the Jira version.'),
374
+ })
375
+ .refine(
376
+ (entry) => {
377
+ return (entry.version === undefined) !== (entry.name === undefined)
378
+ },
379
+ {
380
+ message: 'Each release entry must have exactly one of "version" or "name" (they are mutually exclusive).',
381
+ },
382
+ )
383
+ .transform((entry): ReleaseInput => {
384
+ return entry.name !== undefined
385
+ ? { name: entry.name, type: entry.type, ...(entry.description ? { description: entry.description } : {}) }
386
+ : {
387
+ version: entry.version as string,
388
+ type: entry.type,
389
+ ...(entry.description ? { description: entry.description } : {}),
390
+ }
391
+ }),
290
392
  )
291
393
  .min(1)
292
- .describe('One or more releases to create. Each entry has its own version, type, and optional description.'),
394
+ .describe(
395
+ 'One or more releases to create. Each entry has exactly one of "version" or "name", plus its own type and optional description.',
396
+ ),
293
397
  },
294
398
  outputSchema: {
295
399
  createdBranches: z.array(z.string()).describe('List of created release branch names'),
@@ -1,7 +1,7 @@
1
1
  import confirm from '@inquirer/confirm'
2
2
  import select from '@inquirer/select'
3
3
  import process from 'node:process'
4
- import { z } from 'zod/v4'
4
+ import { z } from 'zod'
5
5
  import { question } from 'zx'
6
6
 
7
7
  import { getReleasePRsWithInfo, updateReleasePRBody } from 'src/integrations/gh'
@@ -10,7 +10,13 @@ import type { JiraConfig, JiraVersion } from 'src/integrations/jira'
10
10
  import { commandEcho } from 'src/lib/command-echo'
11
11
  import { OperationError } from 'src/lib/errors/operation-error'
12
12
  import { logger } from 'src/lib/logger'
13
- import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
13
+ import { displayLabel, formatJiraName, parseBranchName } from 'src/lib/release-id'
14
+ import {
15
+ detectReleaseType,
16
+ formatBranchChoices,
17
+ getJiraDescriptions,
18
+ resolveReleaseBranch,
19
+ } from 'src/lib/release-utils'
14
20
  import type { ReleaseType } from 'src/lib/release-utils'
15
21
  import { defineMcpTool, textContent } from 'src/types'
16
22
  import type { RequiredConfirmedOptionArg } from 'src/types'
@@ -87,7 +93,7 @@ export const releaseDescEdit = async (args: ReleaseDescEditArgs) => {
87
93
  let selectedBranch: string
88
94
 
89
95
  if (versionArg) {
90
- selectedBranch = `release/v${versionArg}`
96
+ selectedBranch = resolveReleaseBranch(versionArg)
91
97
  await verifyReleasePRExists(selectedBranch)
92
98
  } else {
93
99
  commandEcho.setInteractive()
@@ -96,11 +102,23 @@ export const releaseDescEdit = async (args: ReleaseDescEditArgs) => {
96
102
  selectedBranch = picked.branch
97
103
  }
98
104
 
99
- const selectedVersion = selectedBranch.replace('release/v', '')
105
+ // selectedBranch is always a release branch here (operator ref strictly parsed,
106
+ // or picked from discovery-filtered choices), so parseBranchName cannot be null.
107
+ const releaseId = parseBranchName(selectedBranch)
108
+
109
+ if (!releaseId) {
110
+ throw new OperationError(undefined, {
111
+ operation: `edit description for ${selectedBranch}`,
112
+ remediation: 'pass a version (e.g. "1.2.5") or a release name (e.g. "checkout-redesign")',
113
+ })
114
+ }
115
+
116
+ const selectedVersion = displayLabel(releaseId)
100
117
 
101
118
  commandEcho.addOption('--version', selectedVersion)
102
119
 
103
- const versionName = `v${selectedVersion}`
120
+ // Jira fix version is named by the Jira convention: `v1.2.3` | `<name>`.
121
+ const versionName = formatJiraName(releaseId)
104
122
  const jiraVersion = await findVersionByName(versionName, jiraConfig)
105
123
 
106
124
  if (!jiraVersion) {
@@ -190,14 +208,16 @@ export const releaseDescEdit = async (args: ReleaseDescEditArgs) => {
190
208
  export const releaseDescEditMcpTool = defineMcpTool({
191
209
  name: 'release-desc-edit',
192
210
  description:
193
- "Edit a release's description in Jira and in the matching GitHub release PR body. Targets the Jira fix version named `v<version>` and the open PR on branch `release/v<version>`. The PR body is rewritten canonically to `<jiraVersionUrl>\\n\\n<description>` — any prior manual edits to the body are overwritten. Both `version` and `description` are required for MCP calls (the picker/prompt are unreachable without a TTY). Empty `description` clears the description on both sides. Confirmation is auto-skipped for MCP, so the caller is responsible for gating.",
211
+ "Edit a release's description in Jira and in the matching GitHub release PR body. Accepts a release version or a release name: targets the Jira fix version named `v<version>` (versioned) or `<name>` (named) and the open PR on branch `release/v<version>` or `release/n/<name>`. The PR body is rewritten canonically to `<jiraVersionUrl>\\n\\n<description>` — any prior manual edits to the body are overwritten. Both `version` and `description` are required for MCP calls (the picker/prompt are unreachable without a TTY). Empty `description` clears the description on both sides. Confirmation is auto-skipped for MCP, so the caller is responsible for gating.",
194
212
  inputSchema: {
195
- version: z.string().describe('Release version, e.g. "1.2.5".'),
213
+ version: z
214
+ .string()
215
+ .describe('Accepts a release version (e.g. "1.2.5") OR a release name (e.g. "checkout-redesign").'),
196
216
  description: z.string().describe('New description. Empty string clears the description.'),
197
217
  },
198
218
  outputSchema: {
199
219
  version: z.string().describe('Release version'),
200
- branch: z.string().describe('Release branch name (e.g. "release/v1.2.5")'),
220
+ branch: z.string().describe('Release branch name (e.g. "release/v1.2.5" or "release/n/checkout-redesign")'),
201
221
  jiraVersionUrl: z.string().describe('Jira fix version URL'),
202
222
  previousDescription: z.string().describe('The description before the update'),
203
223
  newDescription: z.string().describe('The description after the update'),
@@ -1,4 +1,4 @@
1
- import { z } from 'zod/v4'
1
+ import { z } from 'zod'
2
2
 
3
3
  import { logger } from 'src/lib/logger'
4
4
  import { defineMcpTool, textContent } from 'src/types'
@@ -3,7 +3,7 @@ import checkbox from '@inquirer/checkbox'
3
3
  import confirm from '@inquirer/confirm'
4
4
  import select from '@inquirer/select'
5
5
  import process from 'node:process'
6
- import { z } from 'zod/v4'
6
+ import { z } from 'zod'
7
7
  import { $ } from 'zx'
8
8
 
9
9
  import { buildCmuxWorkspaceTitle, openCmuxWorkspaceWithLayout } from 'src/integrations/cmux'
@@ -15,7 +15,8 @@ import { OperationError } from 'src/lib/errors/operation-error'
15
15
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
16
16
  import { getInfraKitConfig } from 'src/lib/infra-kit-config'
17
17
  import { logger } from 'src/lib/logger'
18
- import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
18
+ import { formatBranchName, isReleaseBranch, parseReleaseRef } from 'src/lib/release-id'
19
+ import { detectReleaseType, formatBranchChoices, getJiraDescriptions, releaseBranchLabels } from 'src/lib/release-utils'
19
20
  import type { ReleaseType } from 'src/lib/release-utils'
20
21
  import { defineMcpTool, textContent } from 'src/types'
21
22
  import type { RequiredConfirmedOptionArg } from 'src/types'
@@ -23,7 +24,6 @@ import type { RequiredConfirmedOptionArg } from 'src/types'
23
24
  // Constants
24
25
  const FEATURE_DIR = 'feature'
25
26
  const RELEASE_DIR = 'release'
26
- const RELEASE_BRANCH_PREFIX = 'release/v'
27
27
 
28
28
  export const CURSOR_MODES = ['workspace', 'windows', 'none'] as const
29
29
  export type CursorMode = (typeof CURSOR_MODES)[number]
@@ -58,7 +58,7 @@ export const worktreesAdd = async (options: WorktreeManagementArgs) => {
58
58
 
59
59
  if (versions) {
60
60
  selectedReleaseBranches = versions.split(',').map((v) => {
61
- return `release/v${v.trim()}`
61
+ return formatBranchName(parseReleaseRef(v.trim()))
62
62
  })
63
63
  } else {
64
64
  const releasePRsInfo = await getReleasePRsWithInfo()
@@ -103,12 +103,7 @@ export const worktreesAdd = async (options: WorktreeManagementArgs) => {
103
103
  if (all) {
104
104
  commandEcho.addOption('--all', true)
105
105
  } else {
106
- commandEcho.addOption(
107
- '--versions',
108
- selectedReleaseBranches.map((branch) => {
109
- return branch.replace('release/v', '')
110
- }),
111
- )
106
+ commandEcho.addOption('--versions', releaseBranchLabels(selectedReleaseBranches))
112
107
  }
113
108
 
114
109
  // Ask for confirmation
@@ -282,7 +277,7 @@ const categorizeWorktrees = (args: CategorizeWorktreesArgs): { branchesToCreate:
282
277
  const { selectedReleaseBranches, currentWorktrees } = args
283
278
 
284
279
  const currentBranchNames = currentWorktrees.filter((branch) => {
285
- return branch.startsWith(RELEASE_BRANCH_PREFIX)
280
+ return isReleaseBranch(branch)
286
281
  })
287
282
 
288
283
  const branchesToCreate = selectedReleaseBranches.filter((branch) => {
@@ -357,7 +352,7 @@ export const worktreesAddMcpTool = defineMcpTool({
357
352
  .string()
358
353
  .optional()
359
354
  .describe(
360
- 'Comma-separated release versions to target (e.g. "1.2.5, 1.2.6"). Either "versions" or all=true must be provided for MCP calls. Overrides "all" when set.',
355
+ 'Comma-separated release versions or names to target (e.g. "1.2.5, 1.2.6" or "checkout-redesign, 1.2.5"). Either "versions" or all=true must be provided for MCP calls. Overrides "all" when set.',
361
356
  ),
362
357
  cursor: z
363
358
  .enum(CURSOR_MODES)
@@ -1,8 +1,9 @@
1
- import { z } from 'zod/v4'
1
+ import { z } from 'zod'
2
2
 
3
3
  import { getReleasePRsWithInfo } from 'src/integrations/gh'
4
4
  import { getCurrentWorktrees } from 'src/lib/git-utils'
5
5
  import { logger } from 'src/lib/logger'
6
+ import { displayLabel, formatJiraName, parseBranchName } from 'src/lib/release-id'
6
7
  import { detectReleaseType, formatVersionLabel, getJiraDescriptions } from 'src/lib/release-utils'
7
8
  import type { ReleaseType } from 'src/lib/release-utils'
8
9
  import { defineMcpTool, textContent } from 'src/types'
@@ -36,12 +37,19 @@ export const worktreesList = async () => {
36
37
  }),
37
38
  )
38
39
 
39
- const worktrees: WorktreeInfo[] = currentWorktrees.map((branch) => {
40
- const version = branch.replace('release/', '')
40
+ // Skip worktrees whose branch does not parse as a release id (lenient source).
41
+ const worktrees: WorktreeInfo[] = currentWorktrees.flatMap((branch) => {
42
+ const id = parseBranchName(branch)
43
+
44
+ if (!id) return []
45
+
46
+ // Human label `1.2.3` | `<name>`; Jira-descriptions map is keyed by the
47
+ // Jira version NAME (`v1.2.3` | `<name>`) — same split as formatBranchChoices.
48
+ const version = displayLabel(id)
41
49
  const type = releaseTypes.get(branch) || 'regular'
42
- const description = jiraDescriptions.get(version) || null
50
+ const description = jiraDescriptions.get(formatJiraName(id)) || null
43
51
 
44
- return { version, type, description }
52
+ return [{ version, type, description }]
45
53
  })
46
54
 
47
55
  // Log formatted output
@@ -1,4 +1,4 @@
1
- import { z } from 'zod/v4'
1
+ import { z } from 'zod'
2
2
  import { $ } from 'zx'
3
3
 
4
4
  import { buildCmuxWorkspaceTitle, listCmuxWorkspaceTitles, openCmuxWorkspaceWithLayout } from 'src/integrations/cmux'
@@ -1,7 +1,7 @@
1
1
  import checkbox from '@inquirer/checkbox'
2
2
  import confirm from '@inquirer/confirm'
3
3
  import process from 'node:process'
4
- import { z } from 'zod/v4'
4
+ import { z } from 'zod'
5
5
 
6
6
  import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
7
7
  import { getReleasePRsWithInfo } from 'src/integrations/gh'
@@ -11,7 +11,8 @@ import { OperationError } from 'src/lib/errors/operation-error'
11
11
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
12
12
  import { getInfraKitConfig } from 'src/lib/infra-kit-config'
13
13
  import { logger } from 'src/lib/logger'
14
- import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
14
+ import { formatBranchName, parseReleaseRef } from 'src/lib/release-id'
15
+ import { detectReleaseType, formatBranchChoices, getJiraDescriptions, releaseBranchLabels } from 'src/lib/release-utils'
15
16
  import type { ReleaseType } from 'src/lib/release-utils'
16
17
  import { removeWorktrees } from 'src/lib/worktrees'
17
18
  import { defineMcpTool, textContent } from 'src/types'
@@ -56,7 +57,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
56
57
  selectedReleaseBranches = currentWorktrees
57
58
  } else if (versions) {
58
59
  selectedReleaseBranches = versions.split(',').map((v) => {
59
- return `release/v${v.trim()}`
60
+ return formatBranchName(parseReleaseRef(v.trim()))
60
61
  })
61
62
  } else {
62
63
  commandEcho.setInteractive()
@@ -82,12 +83,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
82
83
  if (allSelected) {
83
84
  commandEcho.addOption('--all', true)
84
85
  } else {
85
- commandEcho.addOption(
86
- '--versions',
87
- selectedReleaseBranches.map((branch) => {
88
- return branch.replace('release/v', '')
89
- }),
90
- )
86
+ commandEcho.addOption('--versions', releaseBranchLabels(selectedReleaseBranches))
91
87
  }
92
88
 
93
89
  // Ask for confirmation
@@ -216,7 +212,7 @@ export const worktreesRemoveMcpTool = defineMcpTool({
216
212
  .string()
217
213
  .optional()
218
214
  .describe(
219
- 'Comma-separated release versions to target (e.g. "1.2.5, 1.2.6"). Either "versions" or all=true must be provided for MCP calls. Overrides "all" when set.',
215
+ 'Comma-separated release versions or names to target (e.g. "1.2.5, 1.2.6" or "checkout-redesign, 1.2.5"). Either "versions" or all=true must be provided for MCP calls. Overrides "all" when set.',
220
216
  ),
221
217
  },
222
218
  outputSchema: {
@@ -1,6 +1,6 @@
1
1
  import confirm from '@inquirer/confirm'
2
2
  import process from 'node:process'
3
- import { z } from 'zod/v4'
3
+ import { z } from 'zod'
4
4
  import { $ } from 'zx'
5
5
 
6
6
  import { buildCmuxWorkspaceTitle, closeCmuxWorkspaceByTitle } from 'src/integrations/cmux'
@@ -12,12 +12,10 @@ import { OperationError } from 'src/lib/errors/operation-error'
12
12
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
13
13
  import { getInfraKitConfig } from 'src/lib/infra-kit-config'
14
14
  import { logger } from 'src/lib/logger'
15
+ import { isReleaseBranch } from 'src/lib/release-id'
15
16
  import { defineMcpTool, textContent } from 'src/types'
16
17
  import type { RequiredConfirmedOptionArg } from 'src/types'
17
18
 
18
- // Constants
19
- const RELEASE_BRANCH_PREFIX = 'release/v'
20
-
21
19
  interface WorktreeSyncArgs extends RequiredConfirmedOptionArg {}
22
20
 
23
21
  /**
@@ -108,7 +106,7 @@ const categorizeWorktrees = (args: CategorizeWorktreesArgs): { branchesToRemove:
108
106
  const { releasePRsList, currentWorktrees } = args
109
107
 
110
108
  const currentBranchNames = currentWorktrees.filter((branch) => {
111
- return branch.startsWith(RELEASE_BRANCH_PREFIX)
109
+ return isReleaseBranch(branch)
112
110
  })
113
111
 
114
112
  const branchesToRemove = currentBranchNames.filter((branch) => {