ma-agents 3.5.4 → 3.5.6

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.
@@ -0,0 +1,57 @@
1
+ ---
2
+ type: bug
3
+ status: ready-for-dev
4
+ severity: medium
5
+ bug_type: other
6
+ version_found: 3.5.4
7
+ title: ExperimentalWarning about CommonJS loading ES Module during install
8
+ ---
9
+
10
+ # Bug: ExperimentalWarning about CommonJS loading ES Module during install
11
+
12
+ **Severity:** medium
13
+ **Affected Component:** installer CLI (bin/cli.js + bmad-method child spawn)
14
+
15
+ ## Reproduction Steps
16
+
17
+ 1. Use a Node version that emits the experimental `require()` of ESM warning (Node 20.17+ through early 22.x; see Node release notes on `--experimental-require-module`).
18
+ 2. From a clean working directory, run `node bin/cli.js install --yes --agent claude-code` (or equivalently `npx ma-agents install`).
19
+ 3. Observe startup stderr before the BMAD banner appears.
20
+
21
+ ## Expected Behavior
22
+
23
+ Installer starts cleanly with only intentional informational output. No `ExperimentalWarning` noise appears on any Node version supported by `engines` (`>=16.0.0`).
24
+
25
+ ## Actual Behavior
26
+
27
+ Stderr shows a message of the form:
28
+
29
+ ```
30
+ (node:XXXXX) ExperimentalWarning: CommonJS module <path> is loading ES Module <path> using require().
31
+ Support for loading ES Module in require() is an experimental feature and might change at any time.
32
+ ```
33
+
34
+ The installer still completes successfully, but the warning is noisy, alarms first-time users, and appears in CI logs.
35
+
36
+ ## Root Cause Hypothesis
37
+
38
+ The ma-agents CLI (`bin/cli.js`, CommonJS) spawns the bmad-method CLI (`node_modules/bmad-method/tools/bmad-npx-wrapper.js`, also CommonJS). bmad-method interacts with `@clack/prompts` and `@clack/core`, both of which declare `"type": "module"` in their `package.json`. On Node runtimes that emit `ExperimentalWarning` for `require()` of an ESM package (Node 20.17+ with `--experimental-require-module` default-on), the warning surfaces during install startup. While bmad-method already uses dynamic `import()` for the `@clack/*` packages in newer paths, the warning can still be produced by a transitive CJS → ESM require chain under certain Node versions.
39
+
40
+ ## Affected Files
41
+
42
+ - `bin/cli.js`
43
+ - `lib/bmad.js`
44
+
45
+ ## Suggested Fix
46
+
47
+ Install a process-level warning filter at the top of `bin/cli.js` that suppresses only `ExperimentalWarning` whose message matches the `require()`-of-ESM pattern, while re-emitting every other warning (DeprecationWarning, unhandled rejections, custom warnings) via `process.emitWarning` so they remain visible. Additionally, pass a sanitized `env` to the bmad-method child spawn in `lib/bmad.js` — set `NODE_OPTIONS` to include `--no-warnings=ExperimentalWarning` (Node 22+) or use the equivalent `NODE_NO_WARNINGS=1` as a narrow fallback — so the spawned child is equally quiet. Add `test/experimental-warning.test.js` to assert:
48
+
49
+ 1. The filter swallows the specific ExperimentalWarning.
50
+ 2. Unrelated warnings (e.g., a synthetic `DeprecationWarning`) are still emitted.
51
+ 3. `lib/bmad.js` spawn envs include the expected suppression flag.
52
+
53
+ ## Notes
54
+
55
+ - Created via `create-bug-story` workflow
56
+ - Discoverable by sprint workflows via glob: `_bmad-output/implementation-artifacts/bug-*.md`
57
+ - To add to a sprint, run `/add-to-sprint`
@@ -27,6 +27,10 @@ tracking_system: file-system
27
27
  story_location: _bmad-output/implementation-artifacts
28
28
 
29
29
  development_status:
30
+ # ─── BUG FIXES (ACTIVE) ───────────────────────────────────────────────────────
31
+ # Bug A (2026-04-14): ExperimentalWarning on installer startup. Severity: medium.
32
+ bug-experimentalwarning-about-commonjs-loading-es-module-during-install: ready-for-dev
33
+
30
34
  # ─── IN PROGRESS ──────────────────────────────────────────────────────────────
31
35
 
32
36
  # Epic 5: Bundled BMAD Installation — stories 5.1-5.4 done (archived), 5.5-5.6 in review
package/bin/cli.js CHANGED
@@ -1,5 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Bug A fix — suppress the cosmetic Node.js ExperimentalWarning about
4
+ // CommonJS require() of an ES Module. Must be installed BEFORE any
5
+ // other require() so the filter catches warnings fired by the very
6
+ // first dependency-loading call below. See lib/warning-filter.js.
7
+ //
8
+ // Re-exec gating:
9
+ // - Only when this file is launched as the CLI (require.main === module).
10
+ // When imported by the test suite or another script, skip the re-exec
11
+ // path — otherwise the importing process would silently fork itself.
12
+ // - Only for commands that can trigger the CJS-require-ESM warning,
13
+ // i.e. those that spawn bmad-method or otherwise load the heavy
14
+ // transitive dependency graph. Informational commands (--help,
15
+ // --version, status, list, agents) stay single-process for fast
16
+ // cold start. The listener-only fallback is still attached so
17
+ // in-process warnings still route through our filter.
18
+ const { installExperimentalWarningFilter } = require('../lib/warning-filter');
19
+ const _reexecCommands = new Set([
20
+ 'install', 'uninstall', 'remove',
21
+ 'create-skill', 'validate-skill', 'set-mandatory',
22
+ 'customize-agent', 'create-agent',
23
+ 'config',
24
+ undefined, // bare `ma-agents` → interactive wizard
25
+ ]);
26
+ const _firstArg = process.argv[2];
27
+ installExperimentalWarningFilter({
28
+ reexec: require.main === module && _reexecCommands.has(_firstArg),
29
+ });
30
+
3
31
  const prompts = require('prompts');
4
32
  const chalk = require('chalk');
5
33
  const path = require('path');
package/lib/bmad.js CHANGED
@@ -4,6 +4,27 @@ const os = require('os');
4
4
  const { execSync } = require('child_process');
5
5
  const chalk = require('chalk');
6
6
  const yaml = require('yaml');
7
+ const { withExperimentalWarningSuppressed } = require('./warning-filter');
8
+
9
+ /**
10
+ * Build the env object passed to the bmad-method child spawn.
11
+ * Extends parent env with GIT_TERMINAL_PROMPT=0 and a NODE_OPTIONS
12
+ * value that silences the ExperimentalWarning about CJS require() of
13
+ * an ES Module (Bug A). Callers that need a different NODE_OPTIONS
14
+ * can merge after this call.
15
+ *
16
+ * @param {NodeJS.ProcessEnv} [parentEnv=process.env]
17
+ * @returns {NodeJS.ProcessEnv}
18
+ */
19
+ function buildChildSpawnEnv(parentEnv = process.env) {
20
+ return {
21
+ ...parentEnv,
22
+ GIT_TERMINAL_PROMPT: '0',
23
+ // Bug A — keep the bmad-method child as quiet about CJS-require-ESM
24
+ // as the parent. Preserves any NODE_OPTIONS the user already set.
25
+ NODE_OPTIONS: withExperimentalWarningSuppressed(parentEnv.NODE_OPTIONS),
26
+ };
27
+ }
7
28
 
8
29
  const BMAD_DIR = '_bmad';
9
30
  const CONFIG_DIR = path.join(BMAD_DIR, '_config', 'agents');
@@ -122,7 +143,7 @@ async function installBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = p
122
143
 
123
144
  console.log(chalk.gray(` Running: ${command}`));
124
145
  try {
125
- runCommand(command, { cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
146
+ runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
126
147
  await deployMethodology(projectRoot, force);
127
148
  return true;
128
149
  } catch (error) {
@@ -157,7 +178,7 @@ async function runMigration(modules, tools, projectRoot, force, { userName, comm
157
178
 
158
179
  console.log(chalk.gray(` Running: ${command}`));
159
180
  try {
160
- runCommand(command, { cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
181
+ runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
161
182
  } catch (error) {
162
183
  // Rollback on failure
163
184
  console.error(chalk.red(` BMAD update failed: ${error.message}`));
@@ -284,7 +305,7 @@ async function updateBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = pr
284
305
 
285
306
  console.log(chalk.gray(` Running: ${command}`));
286
307
  try {
287
- runCommand(command, { cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
308
+ runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
288
309
  await deployMethodology(projectRoot, force);
289
310
  return true;
290
311
  } catch (error) {
@@ -361,7 +382,7 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
361
382
 
362
383
  console.log(chalk.gray(` Running: ${command}`));
363
384
  try {
364
- runCommand(command, { cwd: projectRoot, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
385
+ runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
365
386
  } catch (error) {
366
387
  console.error(chalk.red(` BMAD recompile failed: ${error.message}`));
367
388
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0",
2
+ "version": "1.1.0",
3
3
  "description": "BMAD-METHOD AI Development Training Presentation",
4
4
  "files": [
5
5
  "BMAD_AI_Development_Training.pptx"
@@ -0,0 +1,245 @@
1
+ /**
2
+ * lib/warning-filter.js
3
+ *
4
+ * Suppresses the Node.js `ExperimentalWarning` that fires when a CommonJS
5
+ * module loads an ES Module via `require()`.
6
+ *
7
+ * Why this exists
8
+ * ---------------
9
+ * The ma-agents CLI (`bin/cli.js`) is CommonJS. It transitively loads
10
+ * code paths from `bmad-method`, which itself interacts with
11
+ * `@clack/prompts` / `@clack/core` (both `"type": "module"`).
12
+ * On Node runtimes where `require()` of ESM is permitted but still
13
+ * emits `ExperimentalWarning` (notably Node 20.17+ through early 22.x),
14
+ * every installer run prints a noisy warning before the banner.
15
+ *
16
+ * The warning is cosmetic — the installer completes successfully — but
17
+ * it frightens first-time users and pollutes CI logs.
18
+ *
19
+ * Why a `process.on('warning', ...)` listener alone is NOT enough
20
+ * ----------------------------------------------------------------
21
+ * In Node.js 20+, `ExperimentalWarning` is printed through an internal
22
+ * path that does not honor user-land `'warning'` listeners for
23
+ * suppression purposes — the listener is invoked, but Node ALSO prints
24
+ * the default formatted message to stderr. The only reliable way to
25
+ * silence it is a CLI flag at process start:
26
+ * NODE_OPTIONS="--no-warnings=ExperimentalWarning" (Node 16+)
27
+ *
28
+ * Strategy
29
+ * --------
30
+ * On first entry, we check whether the current process was started with
31
+ * the suppression flag. If not, we re-exec ourselves in a child
32
+ * process with `NODE_OPTIONS` augmented to include the flag, inherit
33
+ * its stdio, and exit with the child's status. A sentinel env var
34
+ * `MA_AGENTS_WARNINGS_FILTERED=1` prevents infinite re-spawn loops.
35
+ *
36
+ * We additionally attach a `process.on('warning')` listener that
37
+ * silently swallows the specific CJS-require-ESM ExperimentalWarning
38
+ * (if it somehow reaches user-land despite the flag) while re-emitting
39
+ * every other warning. This is belt-and-braces for Node versions where
40
+ * listener semantics differ.
41
+ *
42
+ * For the bmad-method child process spawn (separate Node invocation),
43
+ * `lib/bmad.js` extends `NODE_OPTIONS` with the same flag via
44
+ * `withExperimentalWarningSuppressed()` so the child is equally quiet.
45
+ *
46
+ * This module MUST stay CommonJS and MUST NOT import any ESM package.
47
+ *
48
+ * @module lib/warning-filter
49
+ */
50
+
51
+ 'use strict';
52
+
53
+ const SENTINEL_ENV = 'MA_AGENTS_WARNINGS_FILTERED';
54
+ const SUPPRESS_FLAG = '--no-warnings=ExperimentalWarning';
55
+
56
+ /**
57
+ * Default Node.js warning formatter (mirrors the built-in output
58
+ * used when no `warning` listener is registered). Kept in sync with
59
+ * Node's internal `process/warning.js` formatting circa Node 20–22.
60
+ *
61
+ * @param {Error} warning
62
+ * @returns {string}
63
+ */
64
+ function formatWarning(warning) {
65
+ const name = warning && warning.name ? warning.name : 'Warning';
66
+ const code = warning && warning.code ? ` [${warning.code}]` : '';
67
+ const message = warning && warning.message ? warning.message : String(warning);
68
+ const detail = warning && warning.detail ? `\n${warning.detail}` : '';
69
+ const stack = warning && warning.stack ? warning.stack.split('\n').slice(1).join('\n') : '';
70
+ const header = `(node:${process.pid}) ${name}${code}: ${message}`;
71
+ const body = stack ? `${header}\n${stack}` : header;
72
+ return detail ? `${body}${detail}\n` : `${body}\n`;
73
+ }
74
+
75
+ /**
76
+ * Classify a warning as the specific "CommonJS require() loading an ES Module"
77
+ * ExperimentalWarning we want to suppress.
78
+ *
79
+ * Matches two Node phrasings observed in v20.17+ / v22.x:
80
+ * 1. "CommonJS module <X> is loading ES Module <Y> using require()"
81
+ * 2. "Support for loading ES Module in require() is an experimental feature"
82
+ *
83
+ * Only matches when `warning.name === 'ExperimentalWarning'` — never
84
+ * silences a DeprecationWarning, custom warning, or anything else.
85
+ *
86
+ * @param {Error|{name?: string, message?: string}} warning
87
+ * @returns {boolean}
88
+ */
89
+ function isRequireEsmWarning(warning) {
90
+ if (!warning || warning.name !== 'ExperimentalWarning') {
91
+ return false;
92
+ }
93
+ const msg = String(warning.message || '');
94
+ if (/CommonJS module\b.*\bloading ES Module\b.*\brequire\(\)/i.test(msg)) {
95
+ return true;
96
+ }
97
+ if (/Support for loading ES Module in require\(\)/i.test(msg)) {
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+
103
+ /**
104
+ * Build the NODE_OPTIONS value that should be passed to a child Node
105
+ * process (such as bmad-method) to silence the ExperimentalWarning.
106
+ * Preserves any existing NODE_OPTIONS the caller already set.
107
+ *
108
+ * @param {string|undefined} currentNodeOptions - value of NODE_OPTIONS in the parent env
109
+ * @returns {string} new NODE_OPTIONS value (may equal input if flag already present)
110
+ */
111
+ function withExperimentalWarningSuppressed(currentNodeOptions) {
112
+ const current = (currentNodeOptions || '').trim();
113
+ if (!current) {
114
+ return SUPPRESS_FLAG;
115
+ }
116
+ // If any form of --no-warnings is already present, keep as-is.
117
+ if (/(^|\s)--no-warnings(\s|=|$)/.test(current)) {
118
+ return current;
119
+ }
120
+ return `${current} ${SUPPRESS_FLAG}`;
121
+ }
122
+
123
+ /**
124
+ * Returns true if the current Node process was started with a command-line
125
+ * option that already suppresses the target warning. Inspects
126
+ * `process.execArgv` (flags passed directly to node) and the environment's
127
+ * `NODE_OPTIONS` (flags Node consumed from env at startup).
128
+ *
129
+ * @returns {boolean}
130
+ */
131
+ function isWarningAlreadySuppressed() {
132
+ const execArgv = process.execArgv || [];
133
+ for (const a of execArgv) {
134
+ if (a === '--no-warnings' || a.startsWith('--no-warnings=')) return true;
135
+ if (a.startsWith('--disable-warning=')) return true;
136
+ }
137
+ const opts = (process.env.NODE_OPTIONS || '').trim();
138
+ if (/(^|\s)--no-warnings(\s|=|$)/.test(opts)) return true;
139
+ if (/(^|\s)--disable-warning=/.test(opts)) return true;
140
+ return false;
141
+ }
142
+
143
+ /**
144
+ * Attach the user-land warning listener (belt-and-braces). Idempotent —
145
+ * tags its listener with `__maAgentsFilter` so repeated calls only
146
+ * register one.
147
+ *
148
+ * @returns {boolean} true if newly attached, false if already attached.
149
+ */
150
+ function attachListener() {
151
+ const existing = process.listeners('warning').find(fn => fn.__maAgentsFilter === true);
152
+ if (existing) return false;
153
+
154
+ function filter(warning) {
155
+ if (isRequireEsmWarning(warning)) {
156
+ return; // swallow — cosmetic warning we fix
157
+ }
158
+ try {
159
+ process.stderr.write(formatWarning(warning));
160
+ } catch {
161
+ // never throw from a warning listener
162
+ }
163
+ }
164
+ filter.__maAgentsFilter = true;
165
+ process.on('warning', filter);
166
+ return true;
167
+ }
168
+
169
+ /**
170
+ * Install the warning filter. On first call from a non-suppressed
171
+ * process, re-execs the current script with
172
+ * `NODE_OPTIONS="--no-warnings=ExperimentalWarning"` and a sentinel env
173
+ * var to prevent re-spawn loops, then exits with the child's status.
174
+ *
175
+ * On subsequent calls — or when the process is already suppressed —
176
+ * it simply attaches the user-land listener and returns.
177
+ *
178
+ * Safe to call multiple times (idempotent).
179
+ *
180
+ * @param {Object} [opts]
181
+ * @param {boolean} [opts.reexec=true] - set to false to skip re-exec
182
+ * (used by tests that want to verify listener behaviour only).
183
+ * @returns {boolean} true if the filter was newly installed, false if
184
+ * a previous install was already active.
185
+ */
186
+ function installExperimentalWarningFilter(opts = {}) {
187
+ const reexec = opts.reexec !== false;
188
+
189
+ // If already re-execed, or already suppressed via flags, just attach
190
+ // the listener and move on.
191
+ if (process.env[SENTINEL_ENV] === '1' || isWarningAlreadySuppressed()) {
192
+ return attachListener();
193
+ }
194
+
195
+ if (!reexec) {
196
+ return attachListener();
197
+ }
198
+
199
+ // Re-exec self with the suppression flag in NODE_OPTIONS.
200
+ // We use spawnSync to wait for the child synchronously and then
201
+ // propagate its exit code.
202
+ const { spawnSync } = require('child_process');
203
+ const newEnv = {
204
+ ...process.env,
205
+ [SENTINEL_ENV]: '1',
206
+ NODE_OPTIONS: withExperimentalWarningSuppressed(process.env.NODE_OPTIONS),
207
+ };
208
+ // Forward most execArgv flags to the child, but drop any --inspect*
209
+ // variants — the parent has already bound the debugger port, so the
210
+ // child would fail with EADDRINUSE. The debugger is attached to the
211
+ // parent only; since the parent exits immediately after the child,
212
+ // this is acceptable.
213
+ const forwardedExecArgv = (process.execArgv || []).filter(a =>
214
+ !/^--inspect(-brk|-port)?(=|$)/.test(a)
215
+ );
216
+ const args = [...forwardedExecArgv, ...(process.argv.slice(1) || [])];
217
+ const child = spawnSync(process.execPath, args, {
218
+ stdio: 'inherit',
219
+ env: newEnv,
220
+ });
221
+ // If spawn failed (e.g., EACCES), fall back to listener-only mode.
222
+ if (child.error) {
223
+ return attachListener();
224
+ }
225
+ // Propagate signal or exit code. Node's process.exit is the last
226
+ // thing we do on this path.
227
+ if (child.signal) {
228
+ process.kill(process.pid, child.signal);
229
+ // In case the signal was ignored:
230
+ process.exit(1);
231
+ }
232
+ process.exit(typeof child.status === 'number' ? child.status : 0);
233
+ }
234
+
235
+ module.exports = {
236
+ installExperimentalWarningFilter,
237
+ isRequireEsmWarning,
238
+ withExperimentalWarningSuppressed,
239
+ isWarningAlreadySuppressed,
240
+ // exposed for tests only
241
+ _formatWarning: formatWarning,
242
+ _attachListener: attachListener,
243
+ _SENTINEL_ENV: SENTINEL_ENV,
244
+ _SUPPRESS_FLAG: SUPPRESS_FLAG,
245
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ma-agents",
3
- "version": "3.5.4",
3
+ "version": "3.5.6",
4
4
  "description": "NPX tool to install skills for AI coding agents (Claude Code, Gemini, Copilot, Kilocode, Cline, Cursor, Roo Code)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/cli.js",
11
- "test": "node test/yes-flag.test.js && node test/skill-authoring.test.js && node test/skill-validation.test.js && node test/skill-mandatory.test.js && node test/skill-customize-agent.test.js && node test/create-agent.test.js && node test/generate-project-context.test.js && node test/build-bmad-args.test.js && node test/bmad-version-bump.test.js && node test/extension-module-restructure.test.js && node test/convert-agents-to-skills.test.js && node test/migration.test.js && node test/migration-validation.test.js && node test/story-15-5-workflow-skills.test.js && node test/repo-layout.test.js && node test/config-storage.test.js && node test/cross-repo-validation.test.js && node test/config-lost-on-update.test.js && node test/portable-paths.test.js && node test/cicd-remote-mode.test.js && node test/config-layout.test.js && node test/roo-code-agent.test.js && node test/roo-code-injection.test.js && node test/profile.test.js && node test/onprem-injection.test.js",
11
+ "test": "node test/yes-flag.test.js && node test/skill-authoring.test.js && node test/skill-validation.test.js && node test/skill-mandatory.test.js && node test/skill-customize-agent.test.js && node test/create-agent.test.js && node test/generate-project-context.test.js && node test/build-bmad-args.test.js && node test/bmad-version-bump.test.js && node test/extension-module-restructure.test.js && node test/convert-agents-to-skills.test.js && node test/migration.test.js && node test/migration-validation.test.js && node test/story-15-5-workflow-skills.test.js && node test/repo-layout.test.js && node test/config-storage.test.js && node test/cross-repo-validation.test.js && node test/config-lost-on-update.test.js && node test/portable-paths.test.js && node test/cicd-remote-mode.test.js && node test/config-layout.test.js && node test/roo-code-agent.test.js && node test/roo-code-injection.test.js && node test/profile.test.js && node test/onprem-injection.test.js && node test/experimental-warning.test.js",
12
12
  "build:bmad-cache": "node scripts/build-bmad-cache.js"
13
13
  },
14
14
  "keywords": [
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for Bug A: ExperimentalWarning about CommonJS loading ES Module
4
+ * during installer startup.
5
+ *
6
+ * Contract:
7
+ * lib/warning-filter.js exports installExperimentalWarningFilter().
8
+ * When installed, the process-level 'warning' listener swallows
9
+ * ExperimentalWarning messages that describe CJS require() of an
10
+ * ES Module, but lets every other warning through unchanged.
11
+ *
12
+ * Additionally, bin/cli.js must install the filter at startup, and
13
+ * lib/bmad.js must pass NODE_OPTIONS="--no-warnings=ExperimentalWarning"
14
+ * (or equivalent) to the bmad-method child spawn env.
15
+ */
16
+ 'use strict';
17
+
18
+ const assert = require('assert');
19
+ const { spawnSync } = require('child_process');
20
+ const path = require('path');
21
+ const fs = require('fs');
22
+
23
+ const REPO_ROOT = path.join(__dirname, '..');
24
+ const FILTER_PATH = path.join(REPO_ROOT, 'lib', 'warning-filter.js');
25
+ const CLI_PATH = path.join(REPO_ROOT, 'bin', 'cli.js');
26
+ const BMAD_PATH = path.join(REPO_ROOT, 'lib', 'bmad.js');
27
+
28
+ let passed = 0;
29
+ let failed = 0;
30
+
31
+ function test(name, fn) {
32
+ try {
33
+ fn();
34
+ console.log(` ✓ ${name}`);
35
+ passed++;
36
+ } catch (err) {
37
+ console.error(` ✗ ${name}: ${err.message}`);
38
+ if (err.stack) console.error(err.stack.split('\n').slice(1, 4).join('\n'));
39
+ failed++;
40
+ }
41
+ }
42
+
43
+ console.log('\n── Bug A: ExperimentalWarning filter tests ──────────────────────\n');
44
+
45
+ // ─── Test 1: filter module exists and exports the expected API ────────────────
46
+ test('lib/warning-filter.js exists', () => {
47
+ assert.ok(fs.existsSync(FILTER_PATH), 'expected lib/warning-filter.js to exist');
48
+ });
49
+
50
+ test('filter module exports installExperimentalWarningFilter()', () => {
51
+ const mod = require(FILTER_PATH);
52
+ assert.strictEqual(typeof mod.installExperimentalWarningFilter, 'function');
53
+ });
54
+
55
+ test('filter module exports isRequireEsmWarning() predicate', () => {
56
+ const mod = require(FILTER_PATH);
57
+ assert.strictEqual(typeof mod.isRequireEsmWarning, 'function');
58
+ });
59
+
60
+ // ─── Test 2: isRequireEsmWarning() correctly classifies warnings ──────────────
61
+ test('isRequireEsmWarning matches CJS-loading-ESM ExperimentalWarning', () => {
62
+ const { isRequireEsmWarning } = require(FILTER_PATH);
63
+ const w = new Error(
64
+ 'CommonJS module /a/b.js is loading ES Module /c/d.js using require(). ' +
65
+ 'Support for loading ES Module in require() is an experimental feature ' +
66
+ 'and might change at any time.'
67
+ );
68
+ w.name = 'ExperimentalWarning';
69
+ assert.strictEqual(isRequireEsmWarning(w), true);
70
+ });
71
+
72
+ test('isRequireEsmWarning matches require(ESM) variant phrasing', () => {
73
+ const { isRequireEsmWarning } = require(FILTER_PATH);
74
+ const w = new Error('Support for loading ES Module in require() is an experimental feature.');
75
+ w.name = 'ExperimentalWarning';
76
+ assert.strictEqual(isRequireEsmWarning(w), true);
77
+ });
78
+
79
+ test('isRequireEsmWarning ignores unrelated ExperimentalWarning', () => {
80
+ const { isRequireEsmWarning } = require(FILTER_PATH);
81
+ const w = new Error('Fetch API is an experimental feature.');
82
+ w.name = 'ExperimentalWarning';
83
+ assert.strictEqual(isRequireEsmWarning(w), false);
84
+ });
85
+
86
+ test('isRequireEsmWarning ignores DeprecationWarning with require() text', () => {
87
+ const { isRequireEsmWarning } = require(FILTER_PATH);
88
+ const w = new Error('Some deprecated require() usage in ES Module.');
89
+ w.name = 'DeprecationWarning';
90
+ assert.strictEqual(isRequireEsmWarning(w), false);
91
+ });
92
+
93
+ // ─── Test 3: end-to-end — re-exec flow silences ExperimentalWarning ──────────
94
+ test('installed filter (with re-exec) silences CJS-require-ESM ExperimentalWarning', () => {
95
+ // Drive the full re-exec path: parent re-execs with NODE_OPTIONS set,
96
+ // child emits the warning, expects ZERO bytes of ExperimentalWarning.
97
+ const child = spawnSync(process.execPath, [
98
+ '-e',
99
+ `const { installExperimentalWarningFilter } = require(${JSON.stringify(FILTER_PATH)});
100
+ installExperimentalWarningFilter();
101
+ // After re-exec we land here in the child:
102
+ process.emitWarning('CommonJS module /a.js is loading ES Module /b.mjs using require().', 'ExperimentalWarning');`,
103
+ ], { encoding: 'utf8' });
104
+ assert.strictEqual(child.status, 0, `child exited non-zero: ${child.stderr}`);
105
+ assert.ok(
106
+ !/ExperimentalWarning/.test(child.stderr),
107
+ `expected no ExperimentalWarning in stderr, got:\n${child.stderr}`
108
+ );
109
+ });
110
+
111
+ // ─── Test 3b: NODE_OPTIONS-only test bypasses re-exec ─────────────────────────
112
+ test('listener-only mode swallows the matched warning at user-land', () => {
113
+ // reexec:false means we only attach the listener; target warning still
114
+ // hits Node's internal printer in some Node versions, but our listener
115
+ // returns silently so the listener path itself never re-emits it.
116
+ const child = spawnSync(process.execPath, [
117
+ '-e',
118
+ `const { installExperimentalWarningFilter, _attachListener } = require(${JSON.stringify(FILTER_PATH)});
119
+ installExperimentalWarningFilter({ reexec: false });
120
+ // Emit a custom-named warning identical in shape so we don't depend
121
+ // on Node's internal experimental-warning path:
122
+ const w = new Error('CommonJS module /a.js is loading ES Module /b.mjs using require().');
123
+ w.name = 'ExperimentalWarning';
124
+ // Drive the listener directly to assert it returns without writing.
125
+ const listener = process.listeners('warning').find(fn => fn.__maAgentsFilter);
126
+ if (!listener) { console.error('no filter listener'); process.exit(2); }
127
+ // capture stderr writes
128
+ let captured = '';
129
+ const orig = process.stderr.write.bind(process.stderr);
130
+ process.stderr.write = (c) => { captured += String(c); return true; };
131
+ listener(w);
132
+ process.stderr.write = orig;
133
+ if (captured.length !== 0) { console.error('LEAKED:', captured); process.exit(3); }
134
+ process.exit(0);`,
135
+ ], { encoding: 'utf8', env: { ...process.env, [require(FILTER_PATH)._SENTINEL_ENV]: '1' } });
136
+ assert.strictEqual(child.status, 0, `child exited ${child.status}: ${child.stderr}`);
137
+ });
138
+
139
+ // ─── Test 4: filter preserves unrelated warnings ──────────────────────────────
140
+ test('installed filter preserves DeprecationWarning', () => {
141
+ // Drive the listener directly to verify it re-emits non-matching warnings.
142
+ const child = spawnSync(process.execPath, [
143
+ '-e',
144
+ `const { installExperimentalWarningFilter } = require(${JSON.stringify(FILTER_PATH)});
145
+ installExperimentalWarningFilter({ reexec: false });
146
+ const listener = process.listeners('warning').find(fn => fn.__maAgentsFilter);
147
+ const w = new Error('legacy api — do not use');
148
+ w.name = 'DeprecationWarning';
149
+ let captured = '';
150
+ const orig = process.stderr.write.bind(process.stderr);
151
+ process.stderr.write = (c) => { captured += String(c); return true; };
152
+ listener(w);
153
+ process.stderr.write = orig;
154
+ process.stdout.write(captured);`,
155
+ ], { encoding: 'utf8' });
156
+ assert.strictEqual(child.status, 0);
157
+ assert.ok(
158
+ /DeprecationWarning/.test(child.stdout),
159
+ `expected DeprecationWarning to be preserved, stdout=\n${child.stdout}\nstderr=\n${child.stderr}`
160
+ );
161
+ });
162
+
163
+ test('installed filter preserves unrelated ExperimentalWarning', () => {
164
+ const child = spawnSync(process.execPath, [
165
+ '-e',
166
+ `const { installExperimentalWarningFilter } = require(${JSON.stringify(FILTER_PATH)});
167
+ installExperimentalWarningFilter({ reexec: false });
168
+ const listener = process.listeners('warning').find(fn => fn.__maAgentsFilter);
169
+ const w = new Error('Fetch API is an experimental feature.');
170
+ w.name = 'ExperimentalWarning';
171
+ let captured = '';
172
+ const orig = process.stderr.write.bind(process.stderr);
173
+ process.stderr.write = (c) => { captured += String(c); return true; };
174
+ listener(w);
175
+ process.stderr.write = orig;
176
+ process.stdout.write(captured);`,
177
+ ], { encoding: 'utf8' });
178
+ assert.strictEqual(child.status, 0);
179
+ assert.ok(
180
+ /Fetch API/.test(child.stdout),
181
+ `expected unrelated ExperimentalWarning preserved, stdout=\n${child.stdout}`
182
+ );
183
+ });
184
+
185
+ // ─── Test 5: idempotent — installing twice doesn't double-suppress or leak ────
186
+ test('installExperimentalWarningFilter is idempotent', () => {
187
+ const child = spawnSync(process.execPath, [
188
+ '-e',
189
+ `const { installExperimentalWarningFilter } = require(${JSON.stringify(FILTER_PATH)});
190
+ installExperimentalWarningFilter({ reexec: false });
191
+ installExperimentalWarningFilter({ reexec: false });
192
+ const listeners = process.listeners('warning').filter(fn => fn.__maAgentsFilter === true);
193
+ if (listeners.length !== 1) {
194
+ console.error('EXPECTED 1 filter listener, got', listeners.length);
195
+ process.exit(2);
196
+ }
197
+ process.exit(0);`,
198
+ ], { encoding: 'utf8' });
199
+ assert.strictEqual(child.status, 0, `child exited ${child.status}: ${child.stderr}`);
200
+ });
201
+
202
+ // ─── Test 6: bin/cli.js wires the filter at startup ───────────────────────────
203
+ test('bin/cli.js requires warning-filter and installs it', () => {
204
+ const src = fs.readFileSync(CLI_PATH, 'utf8');
205
+ assert.ok(
206
+ /require\(['"]\.\.\/lib\/warning-filter['"]\)/.test(src) ||
207
+ /require\(['"]\.\.\/lib\/warning-filter\.js['"]\)/.test(src),
208
+ 'expected bin/cli.js to require ../lib/warning-filter'
209
+ );
210
+ assert.ok(
211
+ /installExperimentalWarningFilter\s*\(/.test(src),
212
+ 'expected bin/cli.js to call installExperimentalWarningFilter(...)'
213
+ );
214
+ });
215
+
216
+ // ─── Test 7: lib/bmad.js propagates suppression to child spawn via NODE_OPTIONS
217
+ test('lib/bmad.js builds child env that suppresses ExperimentalWarning', () => {
218
+ const src = fs.readFileSync(BMAD_PATH, 'utf8');
219
+ // Either calls the helper directly or inlines the flag.
220
+ const usesHelper = /withExperimentalWarningSuppressed/.test(src) &&
221
+ /require\(['"]\.\/warning-filter['"]\)/.test(src);
222
+ const inlinesFlag = /--no-warnings=ExperimentalWarning/.test(src);
223
+ assert.ok(
224
+ usesHelper || inlinesFlag,
225
+ 'expected lib/bmad.js to either import withExperimentalWarningSuppressed ' +
226
+ 'from ./warning-filter, or inline --no-warnings=ExperimentalWarning'
227
+ );
228
+ assert.ok(
229
+ /NODE_OPTIONS/.test(src),
230
+ 'expected lib/bmad.js to set NODE_OPTIONS for the child spawn'
231
+ );
232
+ });
233
+
234
+ // ─── Test 8: integration — running bin/cli.js --help is silent on stderr ──────
235
+ test('bin/cli.js --help produces no ExperimentalWarning on stderr', () => {
236
+ const child = spawnSync(process.execPath, [CLI_PATH, '--help'], {
237
+ encoding: 'utf8',
238
+ env: { ...process.env, [require(FILTER_PATH)._SENTINEL_ENV]: '' },
239
+ });
240
+ assert.strictEqual(child.status, 0, `cli --help exited non-zero: ${child.stderr}`);
241
+ assert.ok(
242
+ !/ExperimentalWarning/.test(child.stderr),
243
+ `expected no ExperimentalWarning from cli --help, got:\n${child.stderr}`
244
+ );
245
+ });
246
+
247
+ // ─── Test 9: withExperimentalWarningSuppressed helper is well-behaved ─────────
248
+ test('withExperimentalWarningSuppressed adds flag when none present', () => {
249
+ const { withExperimentalWarningSuppressed } = require(FILTER_PATH);
250
+ assert.strictEqual(withExperimentalWarningSuppressed(undefined), '--no-warnings=ExperimentalWarning');
251
+ assert.strictEqual(withExperimentalWarningSuppressed(''), '--no-warnings=ExperimentalWarning');
252
+ });
253
+
254
+ test('withExperimentalWarningSuppressed preserves existing --no-warnings', () => {
255
+ const { withExperimentalWarningSuppressed } = require(FILTER_PATH);
256
+ assert.strictEqual(withExperimentalWarningSuppressed('--no-warnings'), '--no-warnings');
257
+ assert.strictEqual(
258
+ withExperimentalWarningSuppressed('--no-warnings=DeprecationWarning'),
259
+ '--no-warnings=DeprecationWarning'
260
+ );
261
+ });
262
+
263
+ test('withExperimentalWarningSuppressed appends flag to other NODE_OPTIONS', () => {
264
+ const { withExperimentalWarningSuppressed } = require(FILTER_PATH);
265
+ assert.strictEqual(
266
+ withExperimentalWarningSuppressed('--max-old-space-size=4096'),
267
+ '--max-old-space-size=4096 --no-warnings=ExperimentalWarning'
268
+ );
269
+ });
270
+
271
+ // ─── Test 10: regression — require('bin/cli.js') must NOT re-exec ───────────
272
+ test('require("bin/cli.js") from another process does not re-exec', () => {
273
+ // If the CLI silently re-execs on require, the "before pid" line
274
+ // would print twice in the stderr/stdout stream.
275
+ const child = spawnSync(process.execPath, [
276
+ '-e',
277
+ `const before = process.pid;
278
+ console.log('MARK_BEFORE', before);
279
+ require(${JSON.stringify(CLI_PATH)});
280
+ console.log('MARK_AFTER', process.pid, 'same:', process.pid === before);`,
281
+ ], { encoding: 'utf8' });
282
+ assert.strictEqual(child.status, 0, `child exited non-zero: ${child.stderr}`);
283
+ const beforeCount = (child.stdout.match(/MARK_BEFORE/g) || []).length;
284
+ assert.strictEqual(beforeCount, 1, `expected exactly one MARK_BEFORE, got ${beforeCount}\n${child.stdout}`);
285
+ assert.ok(/same: true/.test(child.stdout), 'expected PID to be unchanged after require');
286
+ });
287
+
288
+ // ─── Test 11: regression — fast commands must NOT re-exec ────────────────────
289
+ test('bin/cli.js --version runs in a single process (no re-exec)', () => {
290
+ // Spawn cli.js --version and assert PID stability via NODE_OPTIONS check.
291
+ // We approximate by running it with the sentinel unset and checking it
292
+ // still completes without looping. A process-tree check is overkill;
293
+ // we just verify exit code and output shape.
294
+ const child = spawnSync(process.execPath, [CLI_PATH, '--version'], {
295
+ encoding: 'utf8',
296
+ env: { ...process.env, [require(FILTER_PATH)._SENTINEL_ENV]: '' },
297
+ });
298
+ assert.strictEqual(child.status, 0);
299
+ assert.ok(/ma-agents v/.test(child.stdout), `expected version banner, got: ${child.stdout}`);
300
+ assert.ok(!/ExperimentalWarning/.test(child.stderr));
301
+ });
302
+
303
+ // ─── Test 12: --inspect is not forwarded to the re-exec child ────────────────
304
+ test('installExperimentalWarningFilter drops --inspect* from forwarded execArgv', () => {
305
+ const src = fs.readFileSync(FILTER_PATH, 'utf8');
306
+ assert.ok(
307
+ /--inspect/.test(src) && /filter/.test(src),
308
+ 'expected warning-filter.js to filter --inspect out of forwarded execArgv'
309
+ );
310
+ });
311
+
312
+ // ─── Summary ──────────────────────────────────────────────────────────────────
313
+ console.log(`\n ${passed} passed, ${failed} failed\n`);
314
+ if (failed > 0) process.exit(1);
package/.ma-agents.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "manifestVersion": "1.2.0",
3
- "agent": null,
4
- "agents": [
5
- null
6
- ],
7
- "scope": "project",
8
- "skills": {},
9
- "profile": "standard"
10
- }
package/MANIFEST.yaml DELETED
@@ -1,3 +0,0 @@
1
- # MANIFEST.yaml
2
-
3
- skills:
@@ -1,7 +0,0 @@
1
- {
2
- "version": "1.0.0",
3
- "description": "BMAD-METHOD AI Development Training Presentation",
4
- "files": [
5
- "BMAD_AI_Development_Training.pptx"
6
- ]
7
- }