bmad-method 6.3.1-next.2 → 6.3.1-next.21

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 (160) hide show
  1. package/package.json +3 -3
  2. package/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md +51 -36
  3. package/src/bmm-skills/1-analysis/bmad-agent-analyst/customize.toml +90 -0
  4. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md +50 -33
  5. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/customize.toml +81 -0
  6. package/src/bmm-skills/1-analysis/bmad-document-project/SKILL.md +57 -1
  7. package/src/bmm-skills/1-analysis/bmad-document-project/customize.toml +41 -0
  8. package/src/bmm-skills/1-analysis/bmad-document-project/workflows/deep-dive-instructions.md +1 -0
  9. package/src/bmm-skills/1-analysis/bmad-document-project/workflows/full-scan-instructions.md +1 -0
  10. package/src/bmm-skills/1-analysis/bmad-prfaq/SKILL.md +48 -9
  11. package/src/bmm-skills/1-analysis/bmad-prfaq/customize.toml +41 -0
  12. package/src/bmm-skills/1-analysis/bmad-prfaq/references/verdict.md +4 -0
  13. package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +44 -9
  14. package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +47 -0
  15. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/contextual-discovery.md +8 -7
  16. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/draft-and-review.md +6 -5
  17. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/finalize.md +4 -1
  18. package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/guided-elicitation.md +3 -2
  19. package/src/bmm-skills/1-analysis/research/bmad-domain-research/SKILL.md +91 -1
  20. package/src/bmm-skills/1-analysis/research/bmad-domain-research/customize.toml +41 -0
  21. package/src/bmm-skills/1-analysis/research/bmad-domain-research/domain-steps/step-06-research-synthesis.md +6 -0
  22. package/src/bmm-skills/1-analysis/research/bmad-market-research/SKILL.md +91 -1
  23. package/src/bmm-skills/1-analysis/research/bmad-market-research/customize.toml +41 -0
  24. package/src/bmm-skills/1-analysis/research/bmad-market-research/steps/step-06-research-completion.md +6 -0
  25. package/src/bmm-skills/1-analysis/research/bmad-technical-research/SKILL.md +91 -1
  26. package/src/bmm-skills/1-analysis/research/bmad-technical-research/customize.toml +41 -0
  27. package/src/bmm-skills/1-analysis/research/bmad-technical-research/technical-steps/step-06-research-synthesis.md +6 -0
  28. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md +50 -35
  29. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/customize.toml +85 -0
  30. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md +50 -31
  31. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/customize.toml +60 -0
  32. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/SKILL.md +99 -1
  33. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/customize.toml +41 -0
  34. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-08-scoping.md +70 -23
  35. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-11-polish.md +1 -1
  36. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/steps-c/step-12-complete.md +6 -0
  37. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/SKILL.md +70 -1
  38. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/customize.toml +41 -0
  39. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-14-complete.md +6 -0
  40. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/SKILL.md +97 -1
  41. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/customize.toml +42 -0
  42. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +2 -0
  43. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/SKILL.md +99 -1
  44. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/customize.toml +42 -0
  45. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-13-report-complete.md +1 -0
  46. package/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md +50 -30
  47. package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +65 -0
  48. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/SKILL.md +86 -1
  49. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/customize.toml +41 -0
  50. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-06-final-assessment.md +6 -0
  51. package/src/bmm-skills/3-solutioning/bmad-create-architecture/SKILL.md +69 -1
  52. package/src/bmm-skills/3-solutioning/bmad-create-architecture/customize.toml +41 -0
  53. package/src/bmm-skills/3-solutioning/bmad-create-architecture/steps/step-08-complete.md +6 -0
  54. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/SKILL.md +88 -1
  55. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/customize.toml +41 -0
  56. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/steps/step-04-final-validation.md +6 -0
  57. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/SKILL.md +76 -1
  58. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/customize.toml +41 -0
  59. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/steps/step-03-complete.md +6 -0
  60. package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +48 -43
  61. package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.toml +90 -0
  62. package/src/bmm-skills/4-implementation/bmad-correct-course/SKILL.md +296 -1
  63. package/src/bmm-skills/4-implementation/bmad-correct-course/customize.toml +41 -0
  64. package/src/bmm-skills/4-implementation/bmad-create-story/SKILL.md +412 -1
  65. package/src/bmm-skills/4-implementation/bmad-create-story/customize.toml +41 -0
  66. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/SKILL.md +171 -1
  67. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/customize.toml +41 -0
  68. package/src/bmm-skills/4-implementation/bmad-retrospective/SKILL.md +1507 -1
  69. package/src/bmm-skills/4-implementation/bmad-retrospective/customize.toml +41 -0
  70. package/src/bmm-skills/module.yaml +49 -0
  71. package/src/core-skills/bmad-advanced-elicitation/SKILL.md +7 -1
  72. package/src/core-skills/bmad-customize/SKILL.md +111 -0
  73. package/src/core-skills/bmad-customize/scripts/list_customizable_skills.py +231 -0
  74. package/src/core-skills/bmad-customize/scripts/tests/test_list_customizable_skills.py +249 -0
  75. package/src/core-skills/bmad-distillator/resources/distillate-format-reference.md +1 -1
  76. package/src/core-skills/bmad-party-mode/SKILL.md +13 -10
  77. package/src/core-skills/module-help.csv +1 -0
  78. package/src/core-skills/module.yaml +3 -0
  79. package/src/scripts/resolve_config.py +176 -0
  80. package/src/scripts/resolve_customization.py +230 -0
  81. package/tools/installer/cli-utils.js +0 -137
  82. package/tools/installer/commands/install.js +13 -0
  83. package/tools/installer/commands/status.js +1 -1
  84. package/tools/installer/commands/uninstall.js +1 -1
  85. package/tools/installer/core/config.js +4 -1
  86. package/tools/installer/core/existing-install.js +1 -1
  87. package/tools/installer/core/install-paths.js +12 -6
  88. package/tools/installer/core/installer.js +182 -95
  89. package/tools/installer/core/manifest-generator.js +347 -190
  90. package/tools/installer/core/manifest.js +49 -642
  91. package/tools/installer/file-ops.js +1 -1
  92. package/tools/installer/fs-native.js +116 -0
  93. package/tools/installer/ide/_config-driven.js +1 -1
  94. package/tools/installer/ide/platform-codes.js +1 -1
  95. package/tools/installer/ide/shared/path-utils.js +0 -145
  96. package/tools/installer/ide/shared/skill-manifest.js +1 -1
  97. package/tools/installer/message-loader.js +1 -1
  98. package/tools/installer/modules/channel-plan.js +203 -0
  99. package/tools/installer/modules/channel-resolver.js +241 -0
  100. package/tools/installer/modules/community-manager.js +131 -24
  101. package/tools/installer/modules/custom-module-manager.js +161 -47
  102. package/tools/installer/modules/external-manager.js +236 -73
  103. package/tools/installer/modules/official-modules.js +61 -63
  104. package/tools/installer/modules/plugin-resolver.js +1 -1
  105. package/tools/installer/modules/registry-client.js +133 -12
  106. package/tools/installer/modules/registry-fallback.yaml +8 -0
  107. package/tools/installer/modules/version-resolver.js +336 -0
  108. package/tools/installer/project-root.js +55 -1
  109. package/tools/installer/prompts.js +0 -106
  110. package/tools/installer/ui.js +457 -51
  111. package/tools/migrate-custom-module-paths.js +1 -1
  112. package/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml +0 -11
  113. package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml +0 -11
  114. package/src/bmm-skills/1-analysis/bmad-document-project/workflow.md +0 -25
  115. package/src/bmm-skills/1-analysis/research/bmad-domain-research/workflow.md +0 -51
  116. package/src/bmm-skills/1-analysis/research/bmad-market-research/workflow.md +0 -51
  117. package/src/bmm-skills/1-analysis/research/bmad-technical-research/workflow.md +0 -52
  118. package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml +0 -11
  119. package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml +0 -11
  120. package/src/bmm-skills/2-plan-workflows/bmad-create-prd/workflow.md +0 -61
  121. package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/workflow.md +0 -35
  122. package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/workflow.md +0 -62
  123. package/src/bmm-skills/2-plan-workflows/bmad-validate-prd/workflow.md +0 -61
  124. package/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml +0 -11
  125. package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +0 -47
  126. package/src/bmm-skills/3-solutioning/bmad-create-architecture/workflow.md +0 -32
  127. package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +0 -51
  128. package/src/bmm-skills/3-solutioning/bmad-generate-project-context/workflow.md +0 -39
  129. package/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml +0 -11
  130. package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +0 -267
  131. package/src/bmm-skills/4-implementation/bmad-create-story/workflow.md +0 -380
  132. package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/workflow.md +0 -136
  133. package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +0 -1479
  134. package/tools/installer/ide/shared/agent-command-generator.js +0 -180
  135. package/tools/installer/ide/shared/bmad-artifacts.js +0 -208
  136. package/tools/installer/ide/shared/module-injections.js +0 -136
  137. package/tools/installer/ide/templates/agent-command-template.md +0 -14
  138. package/tools/installer/ide/templates/combined/antigravity.md +0 -8
  139. package/tools/installer/ide/templates/combined/default-agent.md +0 -15
  140. package/tools/installer/ide/templates/combined/default-task.md +0 -10
  141. package/tools/installer/ide/templates/combined/default-tool.md +0 -10
  142. package/tools/installer/ide/templates/combined/default-workflow.md +0 -6
  143. package/tools/installer/ide/templates/combined/gemini-agent.toml +0 -14
  144. package/tools/installer/ide/templates/combined/gemini-task.toml +0 -11
  145. package/tools/installer/ide/templates/combined/gemini-tool.toml +0 -11
  146. package/tools/installer/ide/templates/combined/gemini-workflow-yaml.toml +0 -16
  147. package/tools/installer/ide/templates/combined/gemini-workflow.toml +0 -14
  148. package/tools/installer/ide/templates/combined/kiro-agent.md +0 -16
  149. package/tools/installer/ide/templates/combined/kiro-task.md +0 -9
  150. package/tools/installer/ide/templates/combined/kiro-tool.md +0 -9
  151. package/tools/installer/ide/templates/combined/kiro-workflow.md +0 -7
  152. package/tools/installer/ide/templates/combined/opencode-agent.md +0 -15
  153. package/tools/installer/ide/templates/combined/opencode-task.md +0 -13
  154. package/tools/installer/ide/templates/combined/opencode-tool.md +0 -13
  155. package/tools/installer/ide/templates/combined/opencode-workflow-yaml.md +0 -16
  156. package/tools/installer/ide/templates/combined/opencode-workflow.md +0 -16
  157. package/tools/installer/ide/templates/combined/rovodev.md +0 -9
  158. package/tools/installer/ide/templates/combined/trae.md +0 -9
  159. package/tools/installer/ide/templates/combined/windsurf-workflow.md +0 -10
  160. package/tools/installer/ide/templates/split/.gitkeep +0 -0
@@ -1,9 +1,16 @@
1
- const fs = require('fs-extra');
1
+ const fs = require('../fs-native');
2
2
  const os = require('node:os');
3
3
  const path = require('node:path');
4
4
  const { execSync } = require('node:child_process');
5
5
  const prompts = require('../prompts');
6
6
 
7
+ function quoteCustomRef(ref) {
8
+ if (typeof ref !== 'string' || !/^[\w.\-+/]+$/.test(ref)) {
9
+ throw new Error(`Unsafe ref name: ${JSON.stringify(ref)}`);
10
+ }
11
+ return `"${ref}"`;
12
+ }
13
+
7
14
  /**
8
15
  * Manages custom modules installed from user-provided sources.
9
16
  * Supports any Git host (GitHub, GitLab, Bitbucket, self-hosted) and local file paths.
@@ -38,8 +45,8 @@ class CustomModuleManager {
38
45
  };
39
46
  }
40
47
 
41
- const trimmed = input.trim();
42
- if (!trimmed) {
48
+ const trimmedRaw = input.trim();
49
+ if (!trimmedRaw) {
43
50
  return {
44
51
  type: null,
45
52
  cloneUrl: null,
@@ -52,8 +59,53 @@ class CustomModuleManager {
52
59
  };
53
60
  }
54
61
 
62
+ // Extract optional @<tag-or-branch> suffix from the end of the input.
63
+ // Semver-valid characters: letters, digits, dot, hyphen, underscore, plus, slash.
64
+ // Raw commit SHAs are NOT supported here — `git clone --branch` can't take
65
+ // them; use --pin at the module level or check out the SHA manually.
66
+ // Only strip when the tail looks like a ref, so we don't disturb
67
+ // URLs without a version spec or the SSH protocol's `git@host:...` prefix.
68
+ let trimmed = trimmedRaw;
69
+ let versionSuffix = null;
70
+ const lastAt = trimmedRaw.lastIndexOf('@');
71
+ // Skip if @ is part of git@github.com:... (first char cannot be stripped as version)
72
+ // and skip if @ appears before the path rather than after a ref-shaped tail.
73
+ if (lastAt > 0) {
74
+ const candidate = trimmedRaw.slice(lastAt + 1);
75
+ const before = trimmedRaw.slice(0, lastAt);
76
+ // candidate must be ref-shaped and must not itself look like a URL / SSH host
77
+ if (/^[\w.\-+/]+$/.test(candidate) && !candidate.includes(':')) {
78
+ // Avoid consuming the @ in `git@host:owner/repo` — `before` wouldn't end with a path separator
79
+ // in that case. Require that the @ comes after the host/path, not inside the auth segment.
80
+ // Rule: the @ is a version suffix only if `before` looks like a complete URL or local path.
81
+ const beforeLooksLikeRepo =
82
+ before.startsWith('/') ||
83
+ before.startsWith('./') ||
84
+ before.startsWith('../') ||
85
+ before.startsWith('~') ||
86
+ /^https?:\/\//i.test(before) ||
87
+ /^git@[^:]+:.+/.test(before);
88
+ if (beforeLooksLikeRepo) {
89
+ versionSuffix = candidate;
90
+ trimmed = before;
91
+ }
92
+ }
93
+ }
94
+
55
95
  // Local path detection: starts with /, ./, ../, or ~
56
96
  if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../') || trimmed.startsWith('~')) {
97
+ if (versionSuffix) {
98
+ return {
99
+ type: 'local',
100
+ cloneUrl: null,
101
+ subdir: null,
102
+ localPath: null,
103
+ cacheKey: null,
104
+ displayName: null,
105
+ isValid: false,
106
+ error: 'Local paths do not support @version suffixes',
107
+ };
108
+ }
57
109
  return this._parseLocalPath(trimmed);
58
110
  }
59
111
 
@@ -66,6 +118,8 @@ class CustomModuleManager {
66
118
  cloneUrl: trimmed,
67
119
  subdir: null,
68
120
  localPath: null,
121
+ version: versionSuffix || null,
122
+ rawInput: trimmedRaw,
69
123
  cacheKey: `${host}/${owner}/${repo}`,
70
124
  displayName: `${owner}/${repo}`,
71
125
  isValid: true,
@@ -79,29 +133,47 @@ class CustomModuleManager {
79
133
  const [, host, owner, repo, remainder] = httpsMatch;
80
134
  const cloneUrl = `https://${host}/${owner}/${repo}`;
81
135
  let subdir = null;
136
+ let urlRef = null; // branch/tag extracted from /tree/<ref>/subdir
82
137
 
83
138
  if (remainder) {
84
139
  // Extract subdir from deep path patterns used by various Git hosts
85
140
  const deepPathPatterns = [
86
- /^\/(?:-\/)?tree\/[^/]+\/(.+)$/, // GitHub /tree/branch/path, GitLab /-/tree/branch/path
87
- /^\/(?:-\/)?blob\/[^/]+\/(.+)$/, // /blob/branch/path (treat same as tree)
88
- /^\/src\/[^/]+\/(.+)$/, // Gitea/Forgejo /src/branch/path
141
+ { regex: /^\/(?:-\/)?tree\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 }, // GitHub, GitLab
142
+ { regex: /^\/(?:-\/)?blob\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 },
143
+ { regex: /^\/src\/([^/]+)\/(.+)$/, refIdx: 1, pathIdx: 2 }, // Gitea/Forgejo
89
144
  ];
145
+ // Also match `/tree/<ref>` with no subdir
146
+ const refOnlyPatterns = [/^\/(?:-\/)?tree\/([^/]+?)\/?$/, /^\/(?:-\/)?blob\/([^/]+?)\/?$/, /^\/src\/([^/]+?)\/?$/];
90
147
 
91
- for (const pattern of deepPathPatterns) {
92
- const match = remainder.match(pattern);
148
+ for (const p of deepPathPatterns) {
149
+ const match = remainder.match(p.regex);
93
150
  if (match) {
94
- subdir = match[1].replace(/\/$/, ''); // strip trailing slash
151
+ urlRef = match[p.refIdx];
152
+ subdir = match[p.pathIdx].replace(/\/$/, '');
95
153
  break;
96
154
  }
97
155
  }
156
+ if (!subdir) {
157
+ for (const r of refOnlyPatterns) {
158
+ const match = remainder.match(r);
159
+ if (match) {
160
+ urlRef = match[1];
161
+ break;
162
+ }
163
+ }
164
+ }
98
165
  }
99
166
 
167
+ // Precedence: explicit @version suffix > URL /tree/<ref> path segment.
168
+ const version = versionSuffix || urlRef || null;
169
+
100
170
  return {
101
171
  type: 'url',
102
172
  cloneUrl,
103
173
  subdir,
104
174
  localPath: null,
175
+ version,
176
+ rawInput: trimmedRaw,
105
177
  cacheKey: `${host}/${owner}/${repo}`,
106
178
  displayName: `${owner}/${repo}`,
107
179
  isValid: true,
@@ -155,33 +227,6 @@ class CustomModuleManager {
155
227
  };
156
228
  }
157
229
 
158
- /**
159
- * @deprecated Use parseSource() instead. Kept for backward compatibility.
160
- * Parse and validate a GitHub repository URL.
161
- * @param {string} url - GitHub URL to validate
162
- * @returns {Object} { owner, repo, isValid, error }
163
- */
164
- validateGitHubUrl(url) {
165
- if (!url || typeof url !== 'string') {
166
- return { owner: null, repo: null, isValid: false, error: 'URL is required' };
167
- }
168
- const trimmed = url.trim();
169
-
170
- // HTTPS format: https://github.com/owner/repo[.git] (strict, no trailing path)
171
- const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
172
- if (httpsMatch) {
173
- return { owner: httpsMatch[1], repo: httpsMatch[2], isValid: true, error: null };
174
- }
175
-
176
- // SSH format: git@github.com:owner/repo[.git]
177
- const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
178
- if (sshMatch) {
179
- return { owner: sshMatch[1], repo: sshMatch[2], isValid: true, error: null };
180
- }
181
-
182
- return { owner: null, repo: null, isValid: false, error: 'Not a valid GitHub URL (expected https://github.com/owner/repo)' };
183
- }
184
-
185
230
  // ─── Marketplace JSON ─────────────────────────────────────────────────────
186
231
 
187
232
  /**
@@ -282,6 +327,10 @@ class CustomModuleManager {
282
327
  const silent = options.silent || false;
283
328
  const displayName = parsed.displayName;
284
329
 
330
+ // Pin override: --pin CODE=TAG resolved at module-selection time overrides
331
+ // any @version suffix present in the URL.
332
+ const effectiveVersion = options.pinOverride || parsed.version || null;
333
+
285
334
  await fs.ensureDir(path.dirname(repoCacheDir));
286
335
 
287
336
  const createSpinner = async () => {
@@ -291,8 +340,23 @@ class CustomModuleManager {
291
340
  return await prompts.spinner();
292
341
  };
293
342
 
343
+ // If an existing cache exists but was cloned at a different version, re-clone.
344
+ // Tracked via .bmad-source.json's recorded version.
294
345
  if (await fs.pathExists(repoCacheDir)) {
295
- // Update existing clone
346
+ let cachedVersion = null;
347
+ try {
348
+ const existing = await fs.readJson(path.join(repoCacheDir, '.bmad-source.json'));
349
+ cachedVersion = existing?.version || null;
350
+ } catch {
351
+ // no metadata; treat as mismatched to be safe if a version was requested
352
+ }
353
+ if ((effectiveVersion || null) !== (cachedVersion || null)) {
354
+ await fs.remove(repoCacheDir);
355
+ }
356
+ }
357
+
358
+ if (await fs.pathExists(repoCacheDir)) {
359
+ // Update existing clone (same version as before)
296
360
  const fetchSpinner = await createSpinner();
297
361
  fetchSpinner.start(`Updating ${displayName}...`);
298
362
  try {
@@ -301,10 +365,25 @@ class CustomModuleManager {
301
365
  stdio: ['ignore', 'pipe', 'pipe'],
302
366
  env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
303
367
  });
304
- execSync('git reset --hard origin/HEAD', {
305
- cwd: repoCacheDir,
306
- stdio: ['ignore', 'pipe', 'pipe'],
307
- });
368
+ if (effectiveVersion) {
369
+ // Fetch the ref as either a tag or a branch — `origin <ref>` works
370
+ // for both, whereas `origin tag <ref>` fails for branch refs parsed
371
+ // out of /tree/<branch>/... URLs.
372
+ execSync(`git fetch --depth 1 origin ${quoteCustomRef(effectiveVersion)} --no-tags`, {
373
+ cwd: repoCacheDir,
374
+ stdio: ['ignore', 'pipe', 'pipe'],
375
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
376
+ });
377
+ execSync(`git checkout --quiet FETCH_HEAD`, {
378
+ cwd: repoCacheDir,
379
+ stdio: ['ignore', 'pipe', 'pipe'],
380
+ });
381
+ } else {
382
+ execSync('git reset --hard origin/HEAD', {
383
+ cwd: repoCacheDir,
384
+ stdio: ['ignore', 'pipe', 'pipe'],
385
+ });
386
+ }
308
387
  fetchSpinner.stop(`Updated ${displayName}`);
309
388
  } catch {
310
389
  fetchSpinner.error(`Update failed, re-downloading ${displayName}`);
@@ -314,25 +393,44 @@ class CustomModuleManager {
314
393
 
315
394
  if (!(await fs.pathExists(repoCacheDir))) {
316
395
  const fetchSpinner = await createSpinner();
317
- fetchSpinner.start(`Cloning ${displayName}...`);
396
+ fetchSpinner.start(`Cloning ${displayName}${effectiveVersion ? ` @ ${effectiveVersion}` : ''}...`);
318
397
  try {
319
- execSync(`git clone --depth 1 "${parsed.cloneUrl}" "${repoCacheDir}"`, {
320
- stdio: ['ignore', 'pipe', 'pipe'],
321
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
322
- });
398
+ if (effectiveVersion) {
399
+ execSync(`git clone --depth 1 --branch ${quoteCustomRef(effectiveVersion)} "${parsed.cloneUrl}" "${repoCacheDir}"`, {
400
+ stdio: ['ignore', 'pipe', 'pipe'],
401
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
402
+ });
403
+ } else {
404
+ execSync(`git clone --depth 1 "${parsed.cloneUrl}" "${repoCacheDir}"`, {
405
+ stdio: ['ignore', 'pipe', 'pipe'],
406
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
407
+ });
408
+ }
323
409
  fetchSpinner.stop(`Cloned ${displayName}`);
324
410
  } catch (error_) {
325
411
  fetchSpinner.error(`Failed to clone ${displayName}`);
326
- throw new Error(`Failed to clone ${parsed.cloneUrl}: ${error_.message}`);
412
+ const refSuffix = effectiveVersion ? `@${effectiveVersion}` : '';
413
+ throw new Error(`Failed to clone ${parsed.cloneUrl}${refSuffix}: ${error_.message}`);
327
414
  }
328
415
  }
329
416
 
417
+ // Record the resolved SHA for the manifest writer.
418
+ let resolvedSha = null;
419
+ try {
420
+ resolvedSha = execSync('git rev-parse HEAD', { cwd: repoCacheDir, stdio: 'pipe' }).toString().trim();
421
+ } catch {
422
+ // swallow — a non-git repo (local path) wouldn't reach here anyway
423
+ }
424
+
330
425
  // Write source metadata for later URL reconstruction
331
426
  const metadataPath = path.join(repoCacheDir, '.bmad-source.json');
332
427
  await fs.writeJson(metadataPath, {
333
428
  cloneUrl: parsed.cloneUrl,
334
429
  cacheKey: parsed.cacheKey,
335
430
  displayName: parsed.displayName,
431
+ version: effectiveVersion || null,
432
+ rawInput: parsed.rawInput || sourceInput,
433
+ sha: resolvedSha,
336
434
  clonedAt: new Date().toISOString(),
337
435
  });
338
436
 
@@ -373,10 +471,26 @@ class CustomModuleManager {
373
471
  const resolver = new PluginResolver();
374
472
  const resolved = await resolver.resolve(repoPath, plugin);
375
473
 
474
+ // Read clone metadata (written by cloneRepo) so we can pick up the
475
+ // resolved git ref + SHA for manifest recording.
476
+ let cloneMetadata = null;
477
+ if (sourceUrl) {
478
+ try {
479
+ cloneMetadata = await fs.readJson(path.join(repoPath, '.bmad-source.json'));
480
+ } catch {
481
+ // no metadata — local-source or legacy cache
482
+ }
483
+ }
484
+
376
485
  // Stamp source info onto each resolved module for manifest tracking
377
486
  for (const mod of resolved) {
378
487
  if (sourceUrl) mod.repoUrl = sourceUrl;
379
488
  if (localPath) mod.localPath = localPath;
489
+ if (cloneMetadata) {
490
+ mod.cloneRef = cloneMetadata.version || null;
491
+ mod.cloneSha = cloneMetadata.sha || null;
492
+ mod.rawInput = cloneMetadata.rawInput || null;
493
+ }
380
494
  CustomModuleManager._resolutionCache.set(mod.code, mod);
381
495
  }
382
496