ma-agents 3.5.6 → 3.6.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.
- package/.ma-agents.json +10 -0
- package/AGENTS.md +97 -0
- package/MANIFEST.yaml +3 -0
- package/README.md +17 -0
- package/_bmad-output/implementation-artifacts/21-10-profile-reconfigure.md +30 -6
- package/_bmad-output/implementation-artifacts/21-11-profile-uninstall.md +2 -1
- package/_bmad-output/implementation-artifacts/21-2-universal-instruction-block-expansion.md +217 -62
- package/_bmad-output/implementation-artifacts/21-3-roomodes-template-bmad-modes.md +196 -73
- package/_bmad-output/implementation-artifacts/21-4-agents-md-template-opencode.md +242 -53
- package/_bmad-output/implementation-artifacts/21-5-clinerules-template-extension.md +180 -41
- package/_bmad-output/implementation-artifacts/21-6-onprem-layered-guardrails.md +250 -75
- package/_bmad-output/implementation-artifacts/21-7-bmad-persona-phase-prefix.md +221 -89
- package/_bmad-output/implementation-artifacts/21-8-vllm-reference-doc-readme.md +121 -63
- package/_bmad-output/implementation-artifacts/21-9-tests-validation.md +332 -61
- package/_bmad-output/implementation-artifacts/bug-bmad-recompile-fails-on-airgapped-network.md +112 -0
- package/_bmad-output/implementation-artifacts/sprint-status.yaml +3 -2
- package/bin/cli.js +59 -0
- package/docs/deployment/vllm-nemotron.md +130 -0
- package/lib/agents.js +17 -2
- package/lib/bmad-customize/bmm-analyst.customize.yaml +8 -0
- package/lib/bmad-customize/bmm-architect.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-dev.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-pm.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-qa.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-quick-flow-solo-dev.customize.yaml +8 -0
- package/lib/bmad-customize/bmm-sm.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-tech-writer.customize.yaml +2 -0
- package/lib/bmad-customize/bmm-ux-designer.customize.yaml +2 -0
- package/lib/bmad.js +293 -1
- package/lib/installer.js +617 -43
- package/lib/merge/roomodes.js +125 -0
- package/lib/profile.js +25 -2
- package/lib/reconfigure.js +334 -0
- package/lib/templates/agents-md.template.md +67 -0
- package/lib/templates/clinerules.template.md +13 -0
- package/lib/templates/instruction-block-onprem.template.md +86 -0
- package/lib/templates/instruction-block-universal.template.md +29 -0
- package/lib/templates/roomodes.template.yaml +96 -0
- package/lib/uninstall.js +314 -0
- package/package.json +4 -3
- package/test/agents-md.test.js +398 -0
- package/test/bmad-extension.test.js +2 -2
- package/test/bmad-persona-phase-prefix.test.js +271 -0
- package/test/clinerules.test.js +339 -0
- package/test/instruction-block.test.js +388 -0
- package/test/integration-verification.test.js +2 -2
- package/test/migration-validation.test.js +2 -2
- package/test/offline-recompile.test.js +237 -0
- package/test/onprem-injection.test.js +425 -32
- package/test/onprem-layer.test.js +419 -0
- package/test/reconfigure.test.js +436 -0
- package/test/roomodes.test.js +343 -0
- package/test/uninstall.test.js +402 -0
package/lib/bmad.js
CHANGED
|
@@ -4,6 +4,7 @@ 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 jsYaml = require('js-yaml');
|
|
7
8
|
const { withExperimentalWarningSuppressed } = require('./warning-filter');
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -61,6 +62,166 @@ function isBmadInstalled(projectRoot = process.cwd()) {
|
|
|
61
62
|
return fs.existsSync(path.join(projectRoot, BMAD_DIR));
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Returns true when the operator has explicitly declared offline mode via env.
|
|
67
|
+
*
|
|
68
|
+
* Use `MA_AGENTS_OFFLINE=1` (or `true`) on air-gapped hosts so the installer
|
|
69
|
+
* skips network-dependent diagnostics and emits actionable guidance instead
|
|
70
|
+
* of bubbling a raw upstream `git fetch` failure.
|
|
71
|
+
*
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
function isOfflineModeDeclared() {
|
|
75
|
+
const flag = String(process.env.MA_AGENTS_OFFLINE || '').trim().toLowerCase();
|
|
76
|
+
return flag === '1' || flag === 'true' || flag === 'yes';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Heuristic: does the caught recompile error look like a network failure?
|
|
81
|
+
*
|
|
82
|
+
* bmad-method 6.2.2 always shells out to `git fetch` / `git clone` / `npm install`
|
|
83
|
+
* against external module repos. On an air-gapped host those commands fail with
|
|
84
|
+
* recognizable DNS/routing/git errors. We match on common substrings rather than
|
|
85
|
+
* exit codes because execSync collapses everything into a single Error.message.
|
|
86
|
+
*
|
|
87
|
+
* @param {Error} error - The error thrown from runCommand() during recompile
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
function looksLikeOfflineFailure(error) {
|
|
91
|
+
if (!error || !error.message) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const haystack = `${error.message} ${error.stderr || ''} ${error.stdout || ''}`.toLowerCase();
|
|
95
|
+
const needles = [
|
|
96
|
+
'enotfound',
|
|
97
|
+
'getaddrinfo',
|
|
98
|
+
'could not resolve host',
|
|
99
|
+
'could not resolve',
|
|
100
|
+
'network is unreachable',
|
|
101
|
+
'connection refused',
|
|
102
|
+
'connection timed out',
|
|
103
|
+
'etimedout',
|
|
104
|
+
'econnrefused',
|
|
105
|
+
'econnreset',
|
|
106
|
+
'unable to access',
|
|
107
|
+
'failed to connect',
|
|
108
|
+
'fatal: unable to',
|
|
109
|
+
'git fetch',
|
|
110
|
+
'git clone',
|
|
111
|
+
// Intentionally NOT matching bare 'proxy' / 'ssl' / 'certificate' —
|
|
112
|
+
// those words appear in benign error messages (e.g. YAML errors that
|
|
113
|
+
// reference certificate-generation skills) and would cause false
|
|
114
|
+
// positives that push non-network failures through the offline path.
|
|
115
|
+
];
|
|
116
|
+
return needles.some(n => haystack.includes(n));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Inspect the vendored BMAD cache (post pre-population) and report which
|
|
121
|
+
* modules are present. Used to classify recompile failures: when the cache is
|
|
122
|
+
* intact we can safely downgrade an offline-mode failure to a warning.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} [cacheRoot] - Override the cache dir (primarily for tests)
|
|
125
|
+
* @returns {{ present: string[], missing: string[], intact: boolean, cacheDir: string }}
|
|
126
|
+
*/
|
|
127
|
+
function inspectBmadCache(cacheRoot) {
|
|
128
|
+
const cacheDir = cacheRoot || path.join(os.homedir(), '.bmad', 'cache', 'external-modules');
|
|
129
|
+
const manifestPath = path.join(__dirname, 'bmad-cache', 'cache-manifest.json');
|
|
130
|
+
|
|
131
|
+
let expected = [];
|
|
132
|
+
try {
|
|
133
|
+
if (fs.existsSync(manifestPath)) {
|
|
134
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
135
|
+
expected = Object.keys(manifest.modules || {});
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// If the manifest is unreadable, treat the cache as not-intact so the
|
|
139
|
+
// caller surfaces a loud error rather than silently proceeding.
|
|
140
|
+
expected = [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const present = [];
|
|
144
|
+
const missing = [];
|
|
145
|
+
for (const mod of expected) {
|
|
146
|
+
if (fs.existsSync(path.join(cacheDir, mod))) {
|
|
147
|
+
present.push(mod);
|
|
148
|
+
} else {
|
|
149
|
+
missing.push(mod);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
present,
|
|
155
|
+
missing,
|
|
156
|
+
intact: expected.length > 0 && missing.length === 0,
|
|
157
|
+
cacheDir,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Decide what to do when the BMAD recompile step throws.
|
|
163
|
+
*
|
|
164
|
+
* Classifies the failure and returns a structured diagnosis so the caller can:
|
|
165
|
+
* (a) 'warn' — downgrade to a yellow diagnostic and proceed
|
|
166
|
+
* (offline + cache intact)
|
|
167
|
+
* (b) 'error' — print an actionable red error listing missing modules
|
|
168
|
+
* and the remediation command (offline + cache incomplete)
|
|
169
|
+
* (c) 'rethrow' — the failure does not look offline-related; historically
|
|
170
|
+
* the caller in applyCustomizations() prints red and
|
|
171
|
+
* continues (we preserve that behaviour to avoid breaking
|
|
172
|
+
* the install pipeline when a non-network error happens
|
|
173
|
+
* mid-pipeline). Callers that want harder failure semantics
|
|
174
|
+
* can throw on `action === 'rethrow'` themselves.
|
|
175
|
+
*
|
|
176
|
+
* Keeping this pure/sync makes it trivial to unit-test without spawning
|
|
177
|
+
* subprocesses.
|
|
178
|
+
*
|
|
179
|
+
* Note on cache validity: we only verify module *directory presence*, not
|
|
180
|
+
* structural integrity. prePopulateBmadCache() runs immediately before
|
|
181
|
+
* recompile and performs structural repair (see its structurallyBroken
|
|
182
|
+
* check), so by the time this classifier is consulted the cache is either
|
|
183
|
+
* present-and-repaired or absent.
|
|
184
|
+
*
|
|
185
|
+
* @param {Error} error - The error thrown from runCommand()
|
|
186
|
+
* @param {Object} [opts]
|
|
187
|
+
* @param {Function} [opts.cacheInspector] - Override for testing
|
|
188
|
+
* @returns {{ action: 'warn' | 'error' | 'rethrow', message: string, missing?: string[] }}
|
|
189
|
+
*/
|
|
190
|
+
function classifyRecompileFailure(error, opts = {}) {
|
|
191
|
+
const declaredOffline = isOfflineModeDeclared();
|
|
192
|
+
const looksOffline = looksLikeOfflineFailure(error);
|
|
193
|
+
const inspector = opts.cacheInspector || inspectBmadCache;
|
|
194
|
+
|
|
195
|
+
if (!declaredOffline && !looksOffline) {
|
|
196
|
+
return {
|
|
197
|
+
action: 'rethrow',
|
|
198
|
+
message: error && error.message ? error.message : 'unknown error',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const cache = inspector();
|
|
203
|
+
|
|
204
|
+
if (cache.intact) {
|
|
205
|
+
return {
|
|
206
|
+
action: 'warn',
|
|
207
|
+
message:
|
|
208
|
+
'BMAD recompile reported a network-related failure, but the vendored cache is intact — ' +
|
|
209
|
+
`proceeding with cached modules (${cache.present.join(', ')}). ` +
|
|
210
|
+
'Set MA_AGENTS_OFFLINE=1 on air-gapped hosts to silence this diagnostic.',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
action: 'error',
|
|
216
|
+
missing: cache.missing,
|
|
217
|
+
message:
|
|
218
|
+
'BMAD recompile failed on an air-gapped host and the vendored cache is incomplete. ' +
|
|
219
|
+
`Missing modules: ${cache.missing.join(', ') || '(cache manifest unreadable)'}. ` +
|
|
220
|
+
'Remediation: on a connected host run `npm run build:bmad-cache` then re-run the installer; ' +
|
|
221
|
+
`or copy a populated cache into ${cache.cacheDir}.`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
64
225
|
/**
|
|
65
226
|
* Build a complete bmad-method CLI command from an install context object.
|
|
66
227
|
* Replaces ad-hoc string concatenation with a single authoritative builder.
|
|
@@ -314,6 +475,112 @@ async function updateBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = pr
|
|
|
314
475
|
}
|
|
315
476
|
}
|
|
316
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Strip `phase` and `on_prem_phase_prefix` top-level keys from all deployed
|
|
480
|
+
* customize.yaml files in `configTargetDir`. Called for EVERY profile so that
|
|
481
|
+
* standard-profile deployments never contain the extension keys (NFR44).
|
|
482
|
+
*
|
|
483
|
+
* @param {string} configTargetDir Path to _bmad/_config/agents/
|
|
484
|
+
*/
|
|
485
|
+
async function stripOnPremKeys(configTargetDir) {
|
|
486
|
+
if (!fs.existsSync(configTargetDir)) return;
|
|
487
|
+
const files = (await fs.readdir(configTargetDir)).filter(f => f.endsWith('.customize.yaml'));
|
|
488
|
+
for (const file of files) {
|
|
489
|
+
const filePath = path.join(configTargetDir, file);
|
|
490
|
+
let raw;
|
|
491
|
+
try {
|
|
492
|
+
raw = await fs.readFile(filePath, 'utf8');
|
|
493
|
+
} catch {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
let doc;
|
|
497
|
+
try {
|
|
498
|
+
doc = jsYaml.load(raw);
|
|
499
|
+
} catch {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (!doc || typeof doc !== 'object') continue;
|
|
503
|
+
if (!Object.prototype.hasOwnProperty.call(doc, 'phase') &&
|
|
504
|
+
!Object.prototype.hasOwnProperty.call(doc, 'on_prem_phase_prefix')) {
|
|
505
|
+
continue; // Nothing to strip — pre-Epic-21 file, leave untouched
|
|
506
|
+
}
|
|
507
|
+
delete doc.phase;
|
|
508
|
+
delete doc.on_prem_phase_prefix;
|
|
509
|
+
await fs.writeFile(filePath, jsYaml.dump(doc, { lineWidth: -1 }), 'utf8');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* For on-prem profile: inject `on_prem_phase_prefix` as `critical_actions[0]`
|
|
515
|
+
* into each deployed customize.yaml that carries the extension keys, then strip
|
|
516
|
+
* those keys from the deployed file. Idempotent: the prefix is inserted once
|
|
517
|
+
* (the extension keys are removed afterwards so a second run is a no-op on the
|
|
518
|
+
* deployed file — `stripOnPremKeys` covers the second pass).
|
|
519
|
+
*
|
|
520
|
+
* Must be called AFTER the STAGE:CUSTOMIZE copy AND after `stripOnPremKeys`
|
|
521
|
+
* has already been called (or will be called immediately after on the same
|
|
522
|
+
* set of files). In practice we call stripOnPremKeys once at the end for all
|
|
523
|
+
* profiles, and applyOnPremPhasePrefix writes the merged file atomically.
|
|
524
|
+
*
|
|
525
|
+
* @param {string} customizeSourceDir lib/bmad-customize/ (source of truth)
|
|
526
|
+
* @param {string} configTargetDir _bmad/_config/agents/ (deployed location)
|
|
527
|
+
*/
|
|
528
|
+
async function applyOnPremPhasePrefix(customizeSourceDir, configTargetDir) {
|
|
529
|
+
if (!fs.existsSync(configTargetDir)) return;
|
|
530
|
+
const files = (await fs.readdir(customizeSourceDir)).filter(f => f.endsWith('.customize.yaml'));
|
|
531
|
+
for (const file of files) {
|
|
532
|
+
const sourceFile = path.join(customizeSourceDir, file);
|
|
533
|
+
const targetFile = path.join(configTargetDir, file);
|
|
534
|
+
if (!fs.existsSync(targetFile)) continue;
|
|
535
|
+
let sourceDoc;
|
|
536
|
+
try {
|
|
537
|
+
sourceDoc = jsYaml.load(await fs.readFile(sourceFile, 'utf8'));
|
|
538
|
+
} catch {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (!sourceDoc || typeof sourceDoc !== 'object') continue;
|
|
542
|
+
const prefix = sourceDoc.on_prem_phase_prefix;
|
|
543
|
+
if (!prefix || typeof prefix !== 'string' || !prefix.trim()) continue;
|
|
544
|
+
// Read the deployed file (may already be stripped if stripOnPremKeys ran first,
|
|
545
|
+
// or may still have the keys if we run in combined mode)
|
|
546
|
+
let deployedDoc;
|
|
547
|
+
try {
|
|
548
|
+
deployedDoc = jsYaml.load(await fs.readFile(targetFile, 'utf8'));
|
|
549
|
+
} catch {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (!deployedDoc || typeof deployedDoc !== 'object') continue;
|
|
553
|
+
// Build final critical_actions: [prefix, ...original entries]
|
|
554
|
+
const originalActions = Array.isArray(deployedDoc.critical_actions)
|
|
555
|
+
? deployedDoc.critical_actions
|
|
556
|
+
: [];
|
|
557
|
+
deployedDoc.critical_actions = [prefix, ...originalActions];
|
|
558
|
+
// Remove extension keys (idempotency: they were in the copy, we strip them now)
|
|
559
|
+
delete deployedDoc.phase;
|
|
560
|
+
delete deployedDoc.on_prem_phase_prefix;
|
|
561
|
+
await fs.writeFile(targetFile, jsYaml.dump(deployedDoc, { lineWidth: -1 }), 'utf8');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Exported for testing without running the full installer.
|
|
567
|
+
* Applies post-deploy YAML rewriting for phase-aware persona prefixes.
|
|
568
|
+
*
|
|
569
|
+
* - Always strips `phase` and `on_prem_phase_prefix` from deployed files (NFR44).
|
|
570
|
+
* - When profile === 'on-prem', additionally injects the prefix as critical_actions[0].
|
|
571
|
+
*
|
|
572
|
+
* @param {string} customizeSourceDir lib/bmad-customize/
|
|
573
|
+
* @param {string} configTargetDir _bmad/_config/agents/
|
|
574
|
+
* @param {string} profile 'on-prem' | 'standard' | undefined
|
|
575
|
+
*/
|
|
576
|
+
async function applyPersonaPhasePrefix(customizeSourceDir, configTargetDir, profile) {
|
|
577
|
+
if (profile === 'on-prem') {
|
|
578
|
+
await applyOnPremPhasePrefix(customizeSourceDir, configTargetDir);
|
|
579
|
+
} else {
|
|
580
|
+
await stripOnPremKeys(configTargetDir);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
317
584
|
async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm', 'bmb'], tools = [], selectedAgentIds = [], force = false, { userName = '', commLang = '', docLang = '', outputFolder = '' } = {}) {
|
|
318
585
|
const sourceDir = path.join(__dirname, 'bmad-customizations');
|
|
319
586
|
const workflowSourceDir = path.join(__dirname, 'bmad-workflows');
|
|
@@ -384,7 +651,20 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
|
|
|
384
651
|
try {
|
|
385
652
|
runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
|
|
386
653
|
} catch (error) {
|
|
387
|
-
|
|
654
|
+
// Air-gapped / offline classification (bug-bmad-recompile-fails-on-airgapped-network).
|
|
655
|
+
// bmad-method 6.2.2 unconditionally performs `git fetch` / `git clone` / `npm install`
|
|
656
|
+
// against external module repos; on a disconnected host those calls throw and we used
|
|
657
|
+
// to swallow the error with a one-line red banner. Instead: classify the failure,
|
|
658
|
+
// downgrade to a warning when the vendored cache is intact, or surface an actionable
|
|
659
|
+
// error listing the missing modules and the remediation command.
|
|
660
|
+
const diagnosis = classifyRecompileFailure(error);
|
|
661
|
+
if (diagnosis.action === 'warn') {
|
|
662
|
+
console.warn(chalk.yellow(` BMAD recompile warning (offline): ${diagnosis.message}`));
|
|
663
|
+
} else if (diagnosis.action === 'error') {
|
|
664
|
+
console.error(chalk.red(` BMAD recompile failed: ${diagnosis.message}`));
|
|
665
|
+
} else {
|
|
666
|
+
console.error(chalk.red(` BMAD recompile failed: ${diagnosis.message}`));
|
|
667
|
+
}
|
|
388
668
|
}
|
|
389
669
|
|
|
390
670
|
// STAGE:EXTENSION — Deploy BMAD extension module (AFTER recompile — extensions/ survives recompile)
|
|
@@ -407,6 +687,8 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
|
|
|
407
687
|
// These add critical_actions to the 8 built-in BMM agents (pm, architect, dev, qa,
|
|
408
688
|
// sm, tech-writer, ux-designer, bmad-master) so they load project skills at startup.
|
|
409
689
|
// Runs AFTER --custom-content (extension module) and BEFORE quick-update.
|
|
690
|
+
// Post-deploy: phase:/on_prem_phase_prefix: keys are stripped for standard profile;
|
|
691
|
+
// for on-prem profile the prefix is injected as critical_actions[0] instead.
|
|
410
692
|
const customizeSource = path.join(__dirname, 'bmad-customize');
|
|
411
693
|
if (fs.existsSync(customizeSource)) {
|
|
412
694
|
await fs.ensureDir(configTargetDir);
|
|
@@ -415,6 +697,8 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
|
|
|
415
697
|
await fs.copy(path.join(customizeSource, file), path.join(configTargetDir, file));
|
|
416
698
|
console.log(chalk.cyan(` + Applied built-in customization: ${file}`));
|
|
417
699
|
}
|
|
700
|
+
const profile = require('./profile').getProfile(projectRoot) ?? 'standard';
|
|
701
|
+
await applyPersonaPhasePrefix(customizeSource, configTargetDir, profile);
|
|
418
702
|
}
|
|
419
703
|
|
|
420
704
|
// STAGE:WORKFLOWS — Apply workflows (AFTER recompile so they persist)
|
|
@@ -1203,4 +1487,12 @@ module.exports = {
|
|
|
1203
1487
|
cleanupLegacyArtifacts,
|
|
1204
1488
|
parseCustomizeYaml,
|
|
1205
1489
|
registerMlWorkflows,
|
|
1490
|
+
// Story 21.7 — phase-aware persona prefix (exported for testing)
|
|
1491
|
+
applyPersonaPhasePrefix,
|
|
1492
|
+
// Offline-safe recompile helpers (exported for testing — see bug
|
|
1493
|
+
// bug-bmad-recompile-fails-on-airgapped-network)
|
|
1494
|
+
isOfflineModeDeclared,
|
|
1495
|
+
looksLikeOfflineFailure,
|
|
1496
|
+
inspectBmadCache,
|
|
1497
|
+
classifyRecompileFailure,
|
|
1206
1498
|
};
|