bmad-method 6.12.2 → 6.12.3-next.1

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.
@@ -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.2",
17
+ "version": "6.12.4",
18
18
  "author": {
19
19
  "name": "Brian (BMad) Madison"
20
20
  },
package/bmad-modules.yaml CHANGED
@@ -9,7 +9,9 @@
9
9
  # Omit to inherit the installer's hardcoded default (stable).
10
10
  #
11
11
  # pin (optional) — a git tag to install instead of the channel. Use it when
12
- # newer releases no longer work with this installer. Only --pin overrides it.
12
+ # newer releases no longer work with this installer. The installer skips the
13
+ # channel picker for a pinned module and prints a notice. --pin and --next=CODE
14
+ # override the pin; --channel and --all-* do not.
13
15
  #
14
16
  # deprecated (optional, default false) — when true, the module is hidden from
15
17
  # the installer picker UNLESS it is already installed (so existing users can
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.2",
4
+ "version": "6.12.3-next.1",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
package/pr2.md ADDED
@@ -0,0 +1,11 @@
1
+ On 6.12.3 the TEA channel picker offers stable, which resolves to v3.0.2.
2
+ The install then fails with `Expected 'src/module.yaml' to exist in version v3.0.2`, because TEA v2+ uses the v7 layout.
3
+
4
+ A module with a registry `pin` now skips the picker, in both the install and update flows.
5
+ The installer prints `tea is pinned to v1.27.2. Newer versions need the v7 installer: npx skills add bmad-code-org/bmad-method-test-architecture-enterprise`.
6
+ Unpinned modules keep the stable and pin choices.
7
+ `--pin CODE=TAG` still overrides a registry pin.
8
+
9
+ Tests drive the install and update flows with a pinned and an unpinned module.
10
+ They fail without the change and pass with it.
11
+ `npm run quality` passes.
@@ -1474,7 +1474,7 @@ class Installer {
1474
1474
  // 'pinned' would trigger a stable-channel tag lookup at config-collection
1475
1475
  // time, burning GitHub API quota and potentially failing.
1476
1476
  const manifestData = await this.manifest.read(bmadDir);
1477
- 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: [] };
1478
1478
  if (manifestData?.modulesDetailed) {
1479
1479
  const { fetchStableTags, classifyUpgrade, parseGitHubRepo } = require('../modules/channel-resolver');
1480
1480
  for (const entry of manifestData.modulesDetailed) {
@@ -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) {
@@ -109,17 +114,20 @@ function parsePinSpec(spec) {
109
114
  * source describes where the decision came from, for logging / debugging.
110
115
  */
111
116
  function decideChannelForModule({ code, channelOptions, registryDefault, registryPin }) {
112
- const { global, nextSet, pins } = channelOptions || { nextSet: new Set(), pins: new Map() };
117
+ const { global, nextSet, stableSet, pins } = channelOptions || { nextSet: new Set(), stableSet: new Set(), pins: new Map() };
113
118
 
114
119
  if (pins && pins.has(code)) {
115
120
  return { channel: 'pinned', pin: pins.get(code), source: 'flag:--pin' };
116
121
  }
117
- if (registryPin) {
118
- return { channel: 'pinned', pin: registryPin, source: 'registry:pin' };
119
- }
120
122
  if (nextSet && nextSet.has(code)) {
121
123
  return { channel: 'next', source: 'flag:--next' };
122
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
+ }
123
131
  if (global) {
124
132
  return { channel: global, source: 'flag:--channel' };
125
133
  }
@@ -1931,12 +1931,6 @@ class UI {
1931
1931
  * Mutates channelOptions.pins and channelOptions.nextSet to reflect picker choices.
1932
1932
  */
1933
1933
  async _interactiveChannelGate({ options, channelOptions, selectedModules }) {
1934
- if (options.yes) return;
1935
- // If the user already declared their channel intent via flags, trust them
1936
- // and skip the gate.
1937
- const haveFlagIntent = channelOptions.global || channelOptions.nextSet.size > 0 || channelOptions.pins.size > 0;
1938
- if (haveFlagIntent) return;
1939
-
1940
1934
  // Figure out which selected modules actually get a channel (externals only).
1941
1935
  // Bundled core/bmm and custom modules skip the picker.
1942
1936
  const externalManager = new ExternalModuleManager();
@@ -1949,6 +1943,20 @@ class UI {
1949
1943
  });
1950
1944
  if (channelSelectable.length === 0) return;
1951
1945
 
1946
+ // A registry pin marks the newest version this installer can install.
1947
+ // Offering stable or next there leads to a failed install, so say what is
1948
+ // pinned (headless installs included) and skip the picker for those modules.
1949
+ await this._announcePinnedModules(channelSelectable, externalByCode, channelOptions);
1950
+
1951
+ if (options.yes) return;
1952
+ // If the user already declared their channel intent via flags, trust them
1953
+ // and skip the gate.
1954
+ const haveFlagIntent = channelOptions.global || channelOptions.nextSet.size > 0 || channelOptions.pins.size > 0;
1955
+ if (haveFlagIntent) return;
1956
+
1957
+ const choosable = channelSelectable.filter((code) => !externalByCode.get(code).pin);
1958
+ if (choosable.length === 0) return;
1959
+
1952
1960
  const fastPath = await prompts.confirm({
1953
1961
  message: `Ready to install (all stable)? Pick "n" to customize channels or pin versions.`,
1954
1962
  default: true,
@@ -1958,7 +1966,7 @@ class UI {
1958
1966
  // Customize path: per-module picker.
1959
1967
  const { fetchStableTags, parseGitHubRepo } = require('./modules/channel-resolver');
1960
1968
 
1961
- for (const code of channelSelectable) {
1969
+ for (const code of choosable) {
1962
1970
  const info = externalByCode.get(code);
1963
1971
  const repoUrl = info.url;
1964
1972
 
@@ -1980,15 +1988,12 @@ class UI {
1980
1988
  message: `${code}: choose a channel`,
1981
1989
  choices: [
1982
1990
  { name: stableLabel, value: 'stable' },
1983
- { name: 'next (main HEAD \u2014 current development)', value: 'next' },
1984
1991
  { name: 'pin (specific version)', value: 'pin' },
1985
1992
  ],
1986
1993
  default: 'stable',
1987
1994
  });
1988
1995
 
1989
- if (choice === 'next') {
1990
- channelOptions.nextSet.add(code);
1991
- } else if (choice === 'pin') {
1996
+ if (choice === 'pin') {
1992
1997
  const pinValue = await prompts.text({
1993
1998
  message: `Enter a version tag for '${code}' (e.g. v1.6.0):`,
1994
1999
  validate: (value) => {
@@ -1998,8 +2003,25 @@ class UI {
1998
2003
  },
1999
2004
  });
2000
2005
  channelOptions.pins.set(code, String(pinValue).trim());
2006
+ } else {
2007
+ channelOptions.stableSet.add(code);
2001
2008
  }
2002
- // 'stable' is the default; nothing to record.
2009
+ }
2010
+ }
2011
+
2012
+ /**
2013
+ * Tell the user which selected modules sit on a registry pin and how to get a
2014
+ * newer version. Skips a module whose version the user set with --pin/--next.
2015
+ */
2016
+ async _announcePinnedModules(codes, externalByCode, channelOptions) {
2017
+ const { parseGitHubRepo } = require('./modules/channel-resolver');
2018
+ for (const code of codes) {
2019
+ const info = externalByCode.get(code);
2020
+ if (!info || !info.pin || info.builtIn) continue;
2021
+ if (channelOptions.pins.has(code) || channelOptions.nextSet.has(code)) continue;
2022
+ const repo = info.url ? parseGitHubRepo(info.url) : null;
2023
+ const where = repo ? ` Newer versions need the v7 installer: npx skills add ${repo.owner}/${repo.repo}` : '';
2024
+ await prompts.log.info(`${code} is pinned to ${info.pin}.${where}`);
2003
2025
  }
2004
2026
  }
2005
2027
 
@@ -2014,8 +2036,7 @@ class UI {
2014
2036
  * frozen unless the user also passes `--pin CODE=NEW_TAG`.
2015
2037
  * - On `next`: no prompt (pull HEAD).
2016
2038
  * - On `pinned`: no prompt (stays pinned).
2017
- * - No channel recorded and `version: null`: one-time migration prompt
2018
- * ("Switch to stable / Keep on next").
2039
+ * - No channel recorded and `version: null`: moves to stable, no prompt.
2019
2040
  *
2020
2041
  * Decisions that freeze the current version are applied by adding a pin to
2021
2042
  * `channelOptions.pins` so downstream clone logic honors them.
@@ -2046,11 +2067,11 @@ class UI {
2046
2067
  const prev = existingByName.get(code);
2047
2068
  if (!prev) return false;
2048
2069
  const info = externalByCode.get(code);
2049
- return info && !info.builtIn;
2070
+ return info && !info.builtIn && !info.pin;
2050
2071
  });
2051
2072
  if (existingWithChannel.length > 0) {
2052
2073
  reviewChannels = await prompts.confirm({
2053
- message: 'Review channel assignments (stable / next / pin) for your existing modules?',
2074
+ message: 'Review channel assignments (stable / pin) for your existing modules?',
2054
2075
  default: false,
2055
2076
  });
2056
2077
  }
@@ -2066,12 +2087,25 @@ class UI {
2066
2087
  // their version is stapled to the CLI version, not a git tag. Skip
2067
2088
  // tag-API lookups for them; the "upgrade" mechanism is `npx bmad@X install`.
2068
2089
  if (info.builtIn) continue;
2090
+ // A registry-pinned module stays on its pin; the channel logic below would
2091
+ // move it to a version this installer cannot install. A version the user
2092
+ // pinned earlier is kept.
2093
+ if (info.pin) {
2094
+ const decided = channelOptions.global || channelOptions.nextSet.has(code) || channelOptions.pins.has(code);
2095
+ const keepsOwnPin = !decided && prev.channel === 'pinned' && prev.version && prev.version !== info.pin;
2096
+ if (keepsOwnPin) {
2097
+ channelOptions.pins.set(code, prev.version);
2098
+ } else {
2099
+ await this._announcePinnedModules([code], externalByCode, channelOptions);
2100
+ }
2101
+ continue;
2102
+ }
2069
2103
 
2070
2104
  const repoUrl = info.url;
2071
2105
  const parsed = repoUrl ? parseGitHubRepo(repoUrl) : null;
2072
2106
 
2073
2107
  // Legacy migration: manifest carries no channel and a null/empty
2074
- // version. Offer the one-time pick between stable and next.
2108
+ // version. The next channel is gone from the picker, so move it to stable.
2075
2109
  const recordedChannel = prev.channel || null;
2076
2110
  const needsMigration = !recordedChannel && (prev.version == null || prev.version === '');
2077
2111
  if (needsMigration) {
@@ -2079,15 +2113,7 @@ class UI {
2079
2113
  // Conservative headless default: stable.
2080
2114
  continue;
2081
2115
  }
2082
- const chosen = await prompts.select({
2083
- message: `${code}: your existing install tracks the main branch. Switch to stable releases (recommended for production), or keep on main?`,
2084
- choices: [
2085
- { name: 'Switch to stable', value: 'stable' },
2086
- { name: 'Keep on main (next)', value: 'next' },
2087
- ],
2088
- default: 'stable',
2089
- });
2090
- if (chosen === 'next') channelOptions.nextSet.add(code);
2116
+ channelOptions.stableSet.add(code);
2091
2117
  continue;
2092
2118
  }
2093
2119
 
@@ -2105,9 +2131,6 @@ class UI {
2105
2131
  if (recordedChannel !== 'stable') {
2106
2132
  switchChoices.push({ name: 'Switch to stable (released version)', value: 'stable' });
2107
2133
  }
2108
- if (recordedChannel !== 'next') {
2109
- switchChoices.push({ name: 'Switch to next (main HEAD)', value: 'next' });
2110
- }
2111
2134
  switchChoices.push({ name: 'Pin to a specific version tag', value: 'pin' });
2112
2135
 
2113
2136
  const choice = await prompts.select({
@@ -2116,10 +2139,6 @@ class UI {
2116
2139
  default: 'keep',
2117
2140
  });
2118
2141
 
2119
- if (choice === 'next') {
2120
- channelOptions.nextSet.add(code);
2121
- continue;
2122
- }
2123
2142
  if (choice === 'pin') {
2124
2143
  const pinValue = await prompts.text({
2125
2144
  message: `Enter a version tag for '${code}' (e.g. v1.6.0):`,
@@ -2135,6 +2154,7 @@ class UI {
2135
2154
  if (choice === 'stable') {
2136
2155
  // Switch to stable: install at the top stable tag without an
2137
2156
  // upgrade-classification prompt (the user explicitly opted in).
2157
+ channelOptions.stableSet.add(code);
2138
2158
  // Also warm the tag cache here so the actual clone step doesn't
2139
2159
  // need a second GitHub API call (can hit rate limits).
2140
2160
  if (parsed) {