bmad-method 6.12.1 → 6.12.3-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/bmad-modules.yaml +7 -0
  3. package/body.md +17 -0
  4. package/package.json +1 -1
  5. package/src/bmm-skills/plan/bmad-architecture/scripts/lint_spine.py +4 -0
  6. package/src/bmm-skills/plan/bmad-sprint-planning/scripts/__pycache__/sprint_plan.cpython-311.pyc +0 -0
  7. package/src/bmm-skills/plan/bmad-sprint-planning/scripts/sprint_plan.py +4 -0
  8. package/src/bmm-skills/plan/bmad-sprint-planning/scripts/tests/__pycache__/test_sprint_plan.cpython-311-pytest-9.1.1.pyc +0 -0
  9. package/src/bmm-skills/ship/bmad-retrospective/scripts/__pycache__/sprint_status.cpython-311.pyc +0 -0
  10. package/src/bmm-skills/ship/bmad-retrospective/scripts/git_evidence.py +4 -0
  11. package/src/bmm-skills/ship/bmad-retrospective/scripts/sprint_status.py +4 -0
  12. package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_git_evidence.cpython-311-pytest-9.1.1.pyc +0 -0
  13. package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_sprint_status.cpython-311-pytest-9.1.1.pyc +0 -0
  14. package/src/core-skills/bmad-customize/scripts/list_customizable_skills.py +4 -0
  15. package/src/core-skills/bmad-deep-recon/scripts/recon_kit.py +4 -0
  16. package/src/scripts/__pycache__/config_utils.cpython-311.pyc +0 -0
  17. package/src/scripts/memlog.py +4 -0
  18. package/src/scripts/tests/__pycache__/test_config_utils.cpython-311.pyc +0 -0
  19. package/src/scripts/tests/__pycache__/test_resolve_config.cpython-311.pyc +0 -0
  20. package/src/scripts/tests/__pycache__/test_resolve_customization.cpython-311.pyc +0 -0
  21. package/tools/installer/core/installer.js +4 -1
  22. package/tools/installer/install-messages.yaml +13 -6
  23. package/tools/installer/modules/channel-plan.js +17 -4
  24. package/tools/installer/modules/external-manager.js +2 -0
  25. package/tools/installer/ui.js +21 -28
  26. package/tools/tests/__pycache__/test_validate_skills.cpython-311.pyc +0 -0
@@ -14,7 +14,7 @@
14
14
  "displayName": "BMad Method Analyze Plan Build",
15
15
  "source": "./",
16
16
  "description": "Full-lifecycle AI development framework — agents and workflows for product analysis, planning, architecture, and implementation.",
17
- "version": "6.12.1",
17
+ "version": "6.12.3",
18
18
  "author": {
19
19
  "name": "Brian (BMad) Madison"
20
20
  },
package/bmad-modules.yaml CHANGED
@@ -8,6 +8,11 @@
8
8
  # override with --channel/--pin/--next. Valid values: stable | next.
9
9
  # Omit to inherit the installer's hardcoded default (stable).
10
10
  #
11
+ # pin (optional) — a git tag to install instead of the channel. Use it when
12
+ # newer releases no longer work with this installer. The pin is the default for
13
+ # every module that gets no explicit choice. --pin, --next=CODE and a stable or
14
+ # next pick in the interactive picker override it; --channel and --all-* do not.
15
+ #
11
16
  # deprecated (optional, default false) — when true, the module is hidden from
12
17
  # the installer picker UNLESS it is already installed (so existing users can
13
18
  # still see/manage it, but new users are not offered it).
@@ -55,6 +60,7 @@ modules:
55
60
  type: bmad-org
56
61
  npmPackage: bmad-creative-intelligence-suite
57
62
  default_channel: stable
63
+ pin: v0.3.2
58
64
 
59
65
  bmad-method-test-architecture-enterprise:
60
66
  url: https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise
@@ -66,6 +72,7 @@ modules:
66
72
  type: bmad-org
67
73
  npmPackage: bmad-method-test-architecture-enterprise
68
74
  default_channel: stable
75
+ pin: v1.27.2
69
76
 
70
77
  bmad-loop:
71
78
  url: https://github.com/bmad-code-org/bmad-loop
package/body.md ADDED
@@ -0,0 +1,17 @@
1
+ Picking a channel in the picker is ignored when the module has a registry `pin`.
2
+ The `tea` pin (v1.27.2) wins, and the manifest records `channel: pinned` with no message.
3
+
4
+ `decideChannelForModule` checked `registryPin` before `--next=CODE`, and the picker never recorded a `stable` pick.
5
+
6
+ New order: `--pin` > `--next=CODE` or picker stable > registry pin > `--channel`/`--all-*` > registry default > `stable`.
7
+
8
+ - `channel-plan.js`: explicit per-module picks resolve before the registry pin. New `stableSet` holds a `stable` pick.
9
+ - `ui.js`: `next` is gone from the install picker and from the update flow's channel review. A stable pick fills `stableSet`. A legacy install that tracks `main` moves to stable without a prompt.
10
+ - Tests: "registry pin beats --next" splits into three cases. Picker-level tests drive the install and update flows, including the all-stable fast path that keeps the pin.
11
+
12
+ `--yes`, the all-stable fast path and `--channel` still get the pin.
13
+
14
+ `--next=CODE` stays for headless use.
15
+ On 6.12 it now installs `main` for a pinned module, which fails for TEA v2+ because that layout needs the v7 installer.
16
+
17
+ `npm run quality` passes.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.12.1",
4
+ "version": "6.12.3-next.0",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -254,4 +254,8 @@ def main(argv: list[str] | None = None) -> int:
254
254
 
255
255
 
256
256
  if __name__ == "__main__":
257
+ if sys.platform == "win32":
258
+ # Piped output on Windows defaults to a legacy code page, not UTF-8.
259
+ sys.stdout.reconfigure(encoding="utf-8")
260
+ sys.stderr.reconfigure(encoding="utf-8")
257
261
  sys.exit(main())
@@ -694,4 +694,8 @@ def main(argv=None):
694
694
 
695
695
 
696
696
  if __name__ == "__main__":
697
+ if sys.platform == "win32":
698
+ # Piped output on Windows defaults to a legacy code page, not UTF-8.
699
+ sys.stdout.reconfigure(encoding="utf-8")
700
+ sys.stderr.reconfigure(encoding="utf-8")
697
701
  main()
@@ -301,4 +301,8 @@ def main(argv=None):
301
301
 
302
302
 
303
303
  if __name__ == "__main__":
304
+ if sys.platform == "win32":
305
+ # Piped output on Windows defaults to a legacy code page, not UTF-8.
306
+ sys.stdout.reconfigure(encoding="utf-8")
307
+ sys.stderr.reconfigure(encoding="utf-8")
304
308
  main()
@@ -743,4 +743,8 @@ def main(argv=None):
743
743
 
744
744
 
745
745
  if __name__ == "__main__":
746
+ if sys.platform == "win32":
747
+ # Piped output on Windows defaults to a legacy code page, not UTF-8.
748
+ sys.stdout.reconfigure(encoding="utf-8")
749
+ sys.stderr.reconfigure(encoding="utf-8")
746
750
  main()
@@ -228,4 +228,8 @@ def main(argv: list[str]) -> int:
228
228
 
229
229
 
230
230
  if __name__ == "__main__":
231
+ if sys.platform == "win32":
232
+ # Piped output on Windows defaults to a legacy code page, not UTF-8.
233
+ sys.stdout.reconfigure(encoding="utf-8")
234
+ sys.stderr.reconfigure(encoding="utf-8")
231
235
  sys.exit(main(sys.argv[1:]))
@@ -319,4 +319,8 @@ def main(argv: list[str] | None = None) -> int:
319
319
 
320
320
 
321
321
  if __name__ == "__main__":
322
+ if sys.platform == "win32":
323
+ # Piped output on Windows defaults to a legacy code page, not UTF-8.
324
+ sys.stdout.reconfigure(encoding="utf-8")
325
+ sys.stderr.reconfigure(encoding="utf-8")
322
326
  sys.exit(main())
@@ -221,4 +221,8 @@ def main(argv: list[str] | None = None) -> int:
221
221
 
222
222
 
223
223
  if __name__ == "__main__":
224
+ if sys.platform == "win32":
225
+ # Piped output on Windows defaults to a legacy code page, not UTF-8.
226
+ sys.stdout.reconfigure(encoding="utf-8")
227
+ sys.stderr.reconfigure(encoding="utf-8")
224
228
  sys.exit(main())
@@ -1326,6 +1326,9 @@ class Installer {
1326
1326
  rounded: true,
1327
1327
  formatBorder: color.green,
1328
1328
  });
1329
+
1330
+ const { MessageLoader } = require('../message-loader');
1331
+ await new MessageLoader().displayEndMessage();
1329
1332
  }
1330
1333
 
1331
1334
  /**
@@ -1471,7 +1474,7 @@ class Installer {
1471
1474
  // 'pinned' would trigger a stable-channel tag lookup at config-collection
1472
1475
  // time, burning GitHub API quota and potentially failing.
1473
1476
  const manifestData = await this.manifest.read(bmadDir);
1474
- const channelOptions = { global: null, nextSet: new Set(), pins: new Map(), warnings: [] };
1477
+ const channelOptions = { global: null, nextSet: new Set(), stableSet: new Set(), pins: new Map(), warnings: [] };
1475
1478
  if (manifestData?.modulesDetailed) {
1476
1479
  const { fetchStableTags, classifyUpgrade, parseGitHubRepo } = require('../modules/channel-resolver');
1477
1480
  for (const entry of manifestData.modulesDetailed) {
@@ -6,16 +6,19 @@
6
6
  startMessage: |
7
7
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8
8
 
9
- Agile AI-Driven Development. Powered by BMad Core and a growing module ecosystem.
9
+ The Breakthrough Method of Agile AI-Driven Development - the original and best AI SDLC. Powered by BMad Core and a growing module ecosystem.
10
10
  Install official and community modules during setup to customize your experience.
11
11
 
12
12
  🌟 100% free. 100% open source. Always.
13
13
  No paywalls. No gated content. Knowledge shared, not sold.
14
14
 
15
+ 🚀 BMad v7 is close, and you can preview and test it now:
16
+ npx skills add bmad-code-org/BMAD-METHOD
17
+ then ask your agent for `bmad setup`. After setup, `bmad migrate`
18
+ tests the v6 to v7 migration (still in beta).
19
+
15
20
  🐍 REQUIRED: uv (https://docs.astral.sh/uv/) runs the Python scripts BMAD
16
- skills rely on (`uv run <script>`) and provisions the interpreter itself.
17
- Without it, bmad-build and bmad-build-auto halt on activation.
18
- If it's not set up yet, ask your AI agent to "install and set up uv for me".
21
+ skills rely on. If it's not set up yet, ask your AI agent to "install and set up uv for me".
19
22
 
20
23
  🌐 CONNECT:
21
24
  Website: https://bmadcode.com/
@@ -33,5 +36,9 @@ startMessage: |
33
36
 
34
37
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
35
38
 
36
- # No end message - install summary and next steps are rendered by the installer
37
- endMessage: ""
39
+ # Display at the END of installation (after the install summary)
40
+ endMessage: |
41
+ 🚀 BMad v7 is close, and you can preview and test it now:
42
+ npx skills add bmad-code-org/BMAD-METHOD
43
+ then ask your agent for `bmad setup`. After setup, `bmad migrate`
44
+ tests the v6 to v7 migration (still in beta).
@@ -10,7 +10,8 @@
10
10
  * 3. Registry defaults (default_channel from bmad-modules.yaml)
11
11
  * 4. Hardcoded fallback 'stable'
12
12
  *
13
- * Precedence: --pin > --next=CODE > --channel (global) > registry default > 'stable'.
13
+ * Precedence: --pin > explicit per-module choice (--next=CODE, picker next/stable) > registry pin
14
+ * > --channel (global) > registry default > 'stable'.
14
15
  *
15
16
  * This module is pure. No prompts, no git, no filesystem.
16
17
  */
@@ -24,6 +25,7 @@ const VALID_CHANNELS = new Set(['stable', 'next']);
24
25
  * @returns {{
25
26
  * global: 'stable'|'next'|null,
26
27
  * nextSet: Set<string>,
28
+ * stableSet: Set<string>,
27
29
  * pins: Map<string, string>,
28
30
  * warnings: string[]
29
31
  * }}
@@ -58,6 +60,9 @@ function parseChannelOptions(options = {}) {
58
60
  nextSet.add(trimmed);
59
61
  }
60
62
 
63
+ // Filled only by the interactive per-module picker (no CLI flag).
64
+ const stableSet = new Set();
65
+
61
66
  // --pin CODE=TAG (repeatable)
62
67
  const pins = new Map();
63
68
  for (const spec of options.pin || []) {
@@ -76,7 +81,7 @@ function parseChannelOptions(options = {}) {
76
81
  // headless installs with --next=/--pin for a community module don't hang.
77
82
  const acceptBypass = options.yes === true || options.acceptBypass === true;
78
83
 
79
- return { global, nextSet, pins, warnings, acceptBypass };
84
+ return { global, nextSet, stableSet, pins, warnings, acceptBypass };
80
85
  }
81
86
 
82
87
  function normalizeChannel(raw, warnings, flagName) {
@@ -104,11 +109,12 @@ function parsePinSpec(spec) {
104
109
  * @param {string} args.code
105
110
  * @param {Object} args.channelOptions - from parseChannelOptions
106
111
  * @param {string} [args.registryDefault] - module's default_channel, if any
112
+ * @param {string} [args.registryPin] - module's registry pin, if any
107
113
  * @returns {{channel: 'stable'|'next'|'pinned', pin?: string, source: string}}
108
114
  * source describes where the decision came from, for logging / debugging.
109
115
  */
110
- function decideChannelForModule({ code, channelOptions, registryDefault }) {
111
- const { global, nextSet, pins } = channelOptions || { nextSet: new Set(), pins: new Map() };
116
+ function decideChannelForModule({ code, channelOptions, registryDefault, registryPin }) {
117
+ const { global, nextSet, stableSet, pins } = channelOptions || { nextSet: new Set(), stableSet: new Set(), pins: new Map() };
112
118
 
113
119
  if (pins && pins.has(code)) {
114
120
  return { channel: 'pinned', pin: pins.get(code), source: 'flag:--pin' };
@@ -116,6 +122,12 @@ function decideChannelForModule({ code, channelOptions, registryDefault }) {
116
122
  if (nextSet && nextSet.has(code)) {
117
123
  return { channel: 'next', source: 'flag:--next' };
118
124
  }
125
+ if (stableSet && stableSet.has(code)) {
126
+ return { channel: 'stable', source: 'picker:stable' };
127
+ }
128
+ if (registryPin) {
129
+ return { channel: 'pinned', pin: registryPin, source: 'registry:pin' };
130
+ }
119
131
  if (global) {
120
132
  return { channel: global, source: 'flag:--channel' };
121
133
  }
@@ -144,6 +156,7 @@ function buildPlan({ modules, channelOptions }) {
144
156
  code: mod.code,
145
157
  channelOptions,
146
158
  registryDefault: mod.defaultChannel,
159
+ registryPin: mod.pin,
147
160
  }),
148
161
  );
149
162
  }
@@ -130,6 +130,7 @@ class ExternalModuleManager {
130
130
  npmPackage: mod.npm_package || mod.npmPackage || null,
131
131
  pluginName: mod.plugin_name || mod.pluginName || null,
132
132
  defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable',
133
+ pin: mod.pin || null,
133
134
  deprecated: mod.deprecated === true,
134
135
  deprecationMessage: mod.deprecation_message || mod['deprecation-message'] || mod.deprecationMessage || null,
135
136
  marketplacePlugin: mod.marketplace_plugin === true || mod['marketplace-plugin'] === true || mod.marketplacePlugin === true,
@@ -273,6 +274,7 @@ class ExternalModuleManager {
273
274
  code: moduleCode,
274
275
  channelOptions: options.channelOptions,
275
276
  registryDefault: moduleInfo.defaultChannel,
277
+ registryPin: moduleInfo.pin,
276
278
  });
277
279
 
278
280
  // Same-plan short-circuit: a single install calls cloneExternalModule
@@ -70,15 +70,17 @@ function buildModuleLabel(name, latestVersion, installedVersion = '') {
70
70
  * @param {Object} options
71
71
  * @param {string|null} [options.repoUrl] - Module repository URL for tag resolution
72
72
  * @param {string|null} [options.registryDefault] - Registry default channel
73
+ * @param {string|null} [options.registryPin] - Registry pinned tag
73
74
  * @param {Object|null} [options.channelOptions] - Parsed installer channel options
74
75
  * @returns {Promise<{version: string, lookupAttempted: boolean, lookupSucceeded: boolean}>}
75
76
  */
76
- async function getModuleVersion(moduleCode, { repoUrl = null, registryDefault = null, channelOptions = null } = {}) {
77
+ async function getModuleVersion(moduleCode, { repoUrl = null, registryDefault = null, registryPin = null, channelOptions = null } = {}) {
77
78
  if (repoUrl) {
78
79
  const plan = decideChannelForModule({
79
80
  code: moduleCode,
80
81
  channelOptions,
81
82
  registryDefault,
83
+ registryPin,
82
84
  });
83
85
 
84
86
  try {
@@ -291,6 +293,13 @@ class UI {
291
293
  }
292
294
  confirmedDirectory = expandedDir;
293
295
  await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`);
296
+ } else if (options.yes) {
297
+ confirmedDirectory = process.cwd();
298
+ const validation = this.validateDirectorySync(confirmedDirectory);
299
+ if (validation) {
300
+ throw new Error(`Invalid directory: ${validation}`);
301
+ }
302
+ await prompts.log.info(`Using current directory: ${confirmedDirectory}`);
294
303
  } else {
295
304
  confirmedDirectory = await this.getConfirmedDirectory();
296
305
  }
@@ -1058,10 +1067,10 @@ class UI {
1058
1067
  const allOptions = [];
1059
1068
  const initialValues = [];
1060
1069
 
1061
- const buildModuleEntry = async (code, name, description, isDefault, repoUrl = null, registryDefault = null) => {
1070
+ const buildModuleEntry = async (code, name, description, isDefault, repoUrl = null, registryDefault = null, registryPin = null) => {
1062
1071
  const isInstalled = installedModuleIds.has(code);
1063
1072
  const installedVersion = installedModuleVersions.get(code) || '';
1064
- const versionState = await getModuleVersion(code, { repoUrl, registryDefault, channelOptions });
1073
+ const versionState = await getModuleVersion(code, { repoUrl, registryDefault, registryPin, channelOptions });
1065
1074
  const label = buildModuleLabel(name, versionState.version, installedVersion);
1066
1075
  return {
1067
1076
  label,
@@ -1109,6 +1118,7 @@ class UI {
1109
1118
  mod.defaultSelected,
1110
1119
  mod.url || null,
1111
1120
  mod.defaultChannel || null,
1121
+ mod.pin || null,
1112
1122
  );
1113
1123
  if (mod.deprecated && mod.deprecationMessage) {
1114
1124
  entry.hint = entry.hint ? `${entry.hint} — ${mod.deprecationMessage}` : mod.deprecationMessage;
@@ -1970,15 +1980,12 @@ class UI {
1970
1980
  message: `${code}: choose a channel`,
1971
1981
  choices: [
1972
1982
  { name: stableLabel, value: 'stable' },
1973
- { name: 'next (main HEAD \u2014 current development)', value: 'next' },
1974
1983
  { name: 'pin (specific version)', value: 'pin' },
1975
1984
  ],
1976
1985
  default: 'stable',
1977
1986
  });
1978
1987
 
1979
- if (choice === 'next') {
1980
- channelOptions.nextSet.add(code);
1981
- } else if (choice === 'pin') {
1988
+ if (choice === 'pin') {
1982
1989
  const pinValue = await prompts.text({
1983
1990
  message: `Enter a version tag for '${code}' (e.g. v1.6.0):`,
1984
1991
  validate: (value) => {
@@ -1988,8 +1995,9 @@ class UI {
1988
1995
  },
1989
1996
  });
1990
1997
  channelOptions.pins.set(code, String(pinValue).trim());
1998
+ } else {
1999
+ channelOptions.stableSet.add(code);
1991
2000
  }
1992
- // 'stable' is the default; nothing to record.
1993
2001
  }
1994
2002
  }
1995
2003
 
@@ -2004,8 +2012,7 @@ class UI {
2004
2012
  * frozen unless the user also passes `--pin CODE=NEW_TAG`.
2005
2013
  * - On `next`: no prompt (pull HEAD).
2006
2014
  * - On `pinned`: no prompt (stays pinned).
2007
- * - No channel recorded and `version: null`: one-time migration prompt
2008
- * ("Switch to stable / Keep on next").
2015
+ * - No channel recorded and `version: null`: moves to stable, no prompt.
2009
2016
  *
2010
2017
  * Decisions that freeze the current version are applied by adding a pin to
2011
2018
  * `channelOptions.pins` so downstream clone logic honors them.
@@ -2040,7 +2047,7 @@ class UI {
2040
2047
  });
2041
2048
  if (existingWithChannel.length > 0) {
2042
2049
  reviewChannels = await prompts.confirm({
2043
- message: 'Review channel assignments (stable / next / pin) for your existing modules?',
2050
+ message: 'Review channel assignments (stable / pin) for your existing modules?',
2044
2051
  default: false,
2045
2052
  });
2046
2053
  }
@@ -2061,7 +2068,7 @@ class UI {
2061
2068
  const parsed = repoUrl ? parseGitHubRepo(repoUrl) : null;
2062
2069
 
2063
2070
  // Legacy migration: manifest carries no channel and a null/empty
2064
- // version. Offer the one-time pick between stable and next.
2071
+ // version. The next channel is gone from the picker, so move it to stable.
2065
2072
  const recordedChannel = prev.channel || null;
2066
2073
  const needsMigration = !recordedChannel && (prev.version == null || prev.version === '');
2067
2074
  if (needsMigration) {
@@ -2069,15 +2076,7 @@ class UI {
2069
2076
  // Conservative headless default: stable.
2070
2077
  continue;
2071
2078
  }
2072
- const chosen = await prompts.select({
2073
- message: `${code}: your existing install tracks the main branch. Switch to stable releases (recommended for production), or keep on main?`,
2074
- choices: [
2075
- { name: 'Switch to stable', value: 'stable' },
2076
- { name: 'Keep on main (next)', value: 'next' },
2077
- ],
2078
- default: 'stable',
2079
- });
2080
- if (chosen === 'next') channelOptions.nextSet.add(code);
2079
+ channelOptions.stableSet.add(code);
2081
2080
  continue;
2082
2081
  }
2083
2082
 
@@ -2095,9 +2094,6 @@ class UI {
2095
2094
  if (recordedChannel !== 'stable') {
2096
2095
  switchChoices.push({ name: 'Switch to stable (released version)', value: 'stable' });
2097
2096
  }
2098
- if (recordedChannel !== 'next') {
2099
- switchChoices.push({ name: 'Switch to next (main HEAD)', value: 'next' });
2100
- }
2101
2097
  switchChoices.push({ name: 'Pin to a specific version tag', value: 'pin' });
2102
2098
 
2103
2099
  const choice = await prompts.select({
@@ -2106,10 +2102,6 @@ class UI {
2106
2102
  default: 'keep',
2107
2103
  });
2108
2104
 
2109
- if (choice === 'next') {
2110
- channelOptions.nextSet.add(code);
2111
- continue;
2112
- }
2113
2105
  if (choice === 'pin') {
2114
2106
  const pinValue = await prompts.text({
2115
2107
  message: `Enter a version tag for '${code}' (e.g. v1.6.0):`,
@@ -2125,6 +2117,7 @@ class UI {
2125
2117
  if (choice === 'stable') {
2126
2118
  // Switch to stable: install at the top stable tag without an
2127
2119
  // upgrade-classification prompt (the user explicitly opted in).
2120
+ channelOptions.stableSet.add(code);
2128
2121
  // Also warm the tag cache here so the actual clone step doesn't
2129
2122
  // need a second GitHub API call (can hit rate limits).
2130
2123
  if (parsed) {