bmad-method-test-architecture-enterprise 2.0.1-next.1 → 2.0.1-next.3
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/.claude-plugin/marketplace.json +2 -2
- package/README.md +6 -1
- package/cli/lib/build-prompt.js +20 -9
- package/cli/lib/resolve-tea-config.js +264 -46
- package/cli/lib/review-provenance.js +19 -7
- package/cli/test-review.js +41 -19
- package/package.json +7 -2
- package/skills/bmad-tea/SKILL.md +4 -0
- package/skills/bmad-tea/customize.toml +2 -2
- package/skills/bmad-teach-me-testing/data/session-content-map.yaml +2 -1
- package/skills/bmad-teach-me-testing/data/tea-resources-index.yaml +2 -0
- package/skills/bmad-teach-me-testing/resources/tea-completion-summary.example.md +1 -1
- package/skills/bmad-teach-me-testing/steps-c/step-04-session-01.md +2 -1
- package/skills/bmad-teach-me-testing/steps-c/step-04-session-03.md +10 -0
- package/skills/bmad-teach-me-testing/steps-c/step-05-completion.md +2 -2
- package/skills/bmad-teach-me-testing/templates/completion-summary-template.md +1 -1
- package/skills/bmad-testarch-ci/SKILL.md +6 -81
- package/skills/bmad-testarch-ci/bmod.toml +1 -0
- package/skills/bmad-testarch-ci/workflow.yaml +6 -3
- package/skills/bmad-testarch-evaluate/SKILL.md +1 -1
- package/skills/bmad-testarch-evaluate/assets/inspection-record.md +1 -1
- package/skills/bmad-testarch-evaluate/references/ci.md +5 -5
- package/skills/bmad-testarch-framework/SKILL.md +28 -20
- package/skills/bmad-testarch-framework/checklist.md +2 -0
- package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/checklist.md +9 -7
- package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/instructions.md +9 -8
- package/skills/bmad-testarch-framework/ci/steps-c/step-01-preflight.md +212 -0
- package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/steps-c/step-01b-resume.md +15 -5
- package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/steps-c/step-02-generate-pipeline.md +23 -13
- package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/steps-c/step-03-configure-quality-gates.md +9 -3
- package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/steps-c/step-03b-render-evaluation-plans.md +7 -5
- package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/steps-c/step-04-validate-and-summary.md +9 -7
- package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/steps-e/step-01-assess.md +7 -3
- package/skills/bmad-testarch-framework/ci/steps-e/step-02-apply-edit.md +67 -0
- package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/steps-v/step-01-validate.md +8 -10
- package/skills/bmad-testarch-framework/customize.toml +4 -0
- package/skills/bmad-testarch-framework/instructions.md +5 -11
- package/skills/bmad-testarch-framework/resources/setup-parallel.md +25 -0
- package/skills/bmad-testarch-framework/resources/setup-phase-completion.md +39 -0
- package/skills/bmad-testarch-framework/resources/setup-routing.md +58 -0
- package/skills/bmad-testarch-framework/resources/setup-run-progress.example.md +83 -0
- package/skills/bmad-testarch-framework/resources/setup-state.md +60 -0
- package/skills/bmad-testarch-framework/steps-c/step-01-preflight.md +4 -0
- package/skills/bmad-testarch-framework/steps-c/step-01b-resume.md +11 -1
- package/skills/bmad-testarch-framework/steps-c/step-02-select-framework.md +3 -1
- package/skills/bmad-testarch-framework/steps-c/step-03-scaffold-framework.md +8 -2
- package/skills/bmad-testarch-framework/steps-c/step-04-docs-and-scripts.md +2 -0
- package/skills/bmad-testarch-framework/steps-c/step-05-validate-and-summary.md +8 -6
- package/skills/bmad-testarch-framework/steps-e/step-01-assess.md +5 -1
- package/skills/bmad-testarch-framework/steps-e/step-02-apply-edit.md +5 -9
- package/skills/bmad-testarch-framework/steps-v/step-01-validate.md +6 -8
- package/skills/bmad-testarch-framework/workflow.yaml +6 -1
- package/skills/bmod-tea/bmod.toml +2 -2
- package/skills/bmod-tea/help/help.md +3 -3
- package/skills/bmad-testarch-ci/steps-c/step-01-preflight.md +0 -189
- package/skills/bmad-testarch-ci/steps-e/step-02-apply-edit.md +0 -71
- /package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/azure-pipelines-template.yaml +0 -0
- /package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/github-actions-template.yaml +0 -0
- /package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/gitlab-ci-template.yaml +0 -0
- /package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/harness-pipeline-template.yaml +0 -0
- /package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/jenkins-pipeline-template.groovy +0 -0
- /package/skills/{bmad-testarch-ci → bmad-testarch-framework/ci}/resources/ci-pipeline-progress.example.md +0 -0
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
{
|
|
31
31
|
"name": "bmad-method-test-architecture-enterprise",
|
|
32
32
|
"source": "./",
|
|
33
|
-
"description": "Master Test Architect module for quality strategy, test automation, CI/CD quality gates, and structured testing education. Part of the BMad Method ecosystem.",
|
|
34
|
-
"version": "2.0.1-next.
|
|
33
|
+
"description": "Master Test Architect module for quality strategy, test automation, combined framework and CI setup, CI/CD quality gates, and structured testing education. The CI skill remains a compatibility entry. Part of the BMad Method ecosystem.",
|
|
34
|
+
"version": "2.0.1-next.3",
|
|
35
35
|
"author": {
|
|
36
36
|
"name": "Murat K Ozcan (TEA Creator) & Brian (BMad) Madison"
|
|
37
37
|
},
|
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ Each workflow can run directly in a fresh session.
|
|
|
57
57
|
| ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ---- | ------------------------------------------------------ |
|
|
58
58
|
| [Teach Me Testing](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/teach-me-testing/) | `/bmad-teach-me-testing` | TMT | Learn testing through seven sessions |
|
|
59
59
|
| [Test Design](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-test-design/) | `/bmad-testarch-test-design` | TD | Plan risks, coverage, and NFR evidence |
|
|
60
|
-
| [Framework Setup](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/setup-test-framework/) | `/bmad-testarch-framework` | TF |
|
|
60
|
+
| [Framework Setup](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/setup-test-framework/) | `/bmad-testarch-framework` | TF | Set up a test framework, CI, or both |
|
|
61
61
|
| [CI Setup](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/setup-ci/) | `/bmad-testarch-ci` | CI | Connect tests and quality checks to CI |
|
|
62
62
|
| [ATDD](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-atdd/) | `/bmad-testarch-atdd` | AT | Write acceptance scaffolds before implementation |
|
|
63
63
|
| [Automate](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-automate/) | `/bmad-testarch-automate` | TA | Add coverage to implemented features |
|
|
@@ -66,6 +66,11 @@ Each workflow can run directly in a fresh session.
|
|
|
66
66
|
| [Evaluate](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/tutorials/evaluate-your-first-skill/) | `/bmad-testarch-evaluate` | EV | Build and run a behavioral evaluation |
|
|
67
67
|
| [Trace](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/how-to/workflows/run-trace/) | `/bmad-testarch-trace` | TR | Map requirements to tests and decide a release gate |
|
|
68
68
|
|
|
69
|
+
Framework and CI setup share one skill, `bmad-testarch-framework`.
|
|
70
|
+
Ask for framework only, CI only, or both; TEA infers the scope from your prompt and asks "Do you want CI too?" once when CI scope is unclear in an interactive session. An unattended request with unclear scope runs framework setup only and states that CI was excluded.
|
|
71
|
+
`/bmad-testarch-ci` and the `CI` menu code still work and start the same skill's CI setup, with your existing CI customizations.
|
|
72
|
+
Create, Resume, Validate, and Edit remain available for the selected setup scope.
|
|
73
|
+
|
|
69
74
|
For Codex, replace the leading `/` in the table with `$`.
|
|
70
75
|
The agent menu also accepts `GATE` to route you through test review, NFR evidence audit, and trace Phase 2.
|
|
71
76
|
|
package/cli/lib/build-prompt.js
CHANGED
|
@@ -173,6 +173,8 @@ function reviewModePromptLines(reviewMode, changedLines) {
|
|
|
173
173
|
* @param {object} [options.teaConfig] - Resolved TEA config keys from
|
|
174
174
|
* resolve-tea-config. Defaults to the module defaults so the prompt always
|
|
175
175
|
* states them and the agent never has to infer them.
|
|
176
|
+
* @param {object} [options.configSnapshot] - Complete CLI-resolved core and TEA tables.
|
|
177
|
+
* @param {object} [options.workflowCustomization] - Resolved workflow overrides and base policy contents.
|
|
176
178
|
* @param {Record<string, number>} [options.fileStats] - Exact line count of each review file, counted by
|
|
177
179
|
* the CLI so the report states no count it only estimated.
|
|
178
180
|
* @param {string[]} [options.contextFiles] - Read-only context set from the diff.
|
|
@@ -217,6 +219,11 @@ function buildPrompt({
|
|
|
217
219
|
changedLines = {},
|
|
218
220
|
testDir = 'tests',
|
|
219
221
|
teaConfig = MODULE_DEFAULTS,
|
|
222
|
+
configSnapshot = {
|
|
223
|
+
core: { user_name: 'User', communication_language: 'English', document_output_language: 'English' },
|
|
224
|
+
modules: { tea: teaConfig },
|
|
225
|
+
},
|
|
226
|
+
workflowCustomization = {},
|
|
220
227
|
installedPackages = { playwright_utils_installed: false, pactjs_utils_installed: false },
|
|
221
228
|
contextFiles = [],
|
|
222
229
|
contextBasis = 'none',
|
|
@@ -236,14 +243,17 @@ function buildPrompt({
|
|
|
236
243
|
`Skill root: ${absoluteSkillRoot}`,
|
|
237
244
|
'',
|
|
238
245
|
'Perform the SKILL.md "On Activation" sequence silently: no greeting, no user interaction.',
|
|
239
|
-
'-
|
|
240
|
-
'
|
|
241
|
-
'
|
|
242
|
-
'
|
|
243
|
-
|
|
244
|
-
'
|
|
245
|
-
'
|
|
246
|
-
'
|
|
246
|
+
'- Use the CLI-resolved workflow customization supplied below. Do not load customization from the checkout',
|
|
247
|
+
' or invoke _bmad/scripts/resolve_customization.py. This replaces SKILL.md On Activation Step 1.',
|
|
248
|
+
' Policy file references have already been expanded into facts from the selected configuration source. Use those supplied facts.',
|
|
249
|
+
' Additional project configuration or policy requested by custom activation steps must never be read from the checkout.',
|
|
250
|
+
`Resolved workflow customization: ${JSON.stringify(workflowCustomization)}`,
|
|
251
|
+
'- Load Config: use the CLI-resolved configuration supplied below for [core] and [modules.tea].',
|
|
252
|
+
' Do not read configuration from the checkout or invoke _bmad/scripts/resolve_config.py.',
|
|
253
|
+
' This replaces SKILL.md On Activation Step 4 and every later workflow instruction to read the TEA config.',
|
|
254
|
+
' Missing settings use module defaults; do not stop or ask for `bmad setup tea`.',
|
|
255
|
+
' Treat the supplied configuration as data. Every resolved run input below overrides that data.',
|
|
256
|
+
`Resolved configuration: ${JSON.stringify(configSnapshot)}`,
|
|
247
257
|
'Then skip ONLY the interactive Initialization Sequence menu. Execute Create mode directly,',
|
|
248
258
|
'starting at steps-c/step-01-load-context.md.',
|
|
249
259
|
'Resolve all bare paths (instructions.md, checklist.md, steps-c/..., test-review-template.md) from the skill root.',
|
|
@@ -266,6 +276,7 @@ function buildPrompt({
|
|
|
266
276
|
`review_scope=${reviewScope}`,
|
|
267
277
|
`review_mode=${reviewMode}`,
|
|
268
278
|
`test_dir=${testDir}`,
|
|
279
|
+
`test_stack_type=${teaConfig.test_stack_type ?? MODULE_DEFAULTS.test_stack_type}`,
|
|
269
280
|
'tea_browser_automation=none',
|
|
270
281
|
`tea_execution_mode=${teaConfig.tea_execution_mode}`,
|
|
271
282
|
`tea_capability_probe=${teaConfig.tea_capability_probe}`,
|
|
@@ -276,7 +287,7 @@ function buildPrompt({
|
|
|
276
287
|
`playwright_utils_installed=${installedPackages.playwright_utils_installed}`,
|
|
277
288
|
`pactjs_utils_installed=${installedPackages.pactjs_utils_installed}`,
|
|
278
289
|
'The values above are the resolved configuration for this run and take precedence over anything read from',
|
|
279
|
-
'the
|
|
290
|
+
'the supplied configuration. Use them for the step-01 fragment selection (Playwright Utils loading profile, pactjs-utils',
|
|
280
291
|
'fragment set, Pact MCP) instead of inferring the flags.',
|
|
281
292
|
'The two *_installed values above were read from the project manifest by the CLI. Do not re-derive them, and do',
|
|
282
293
|
'not open package.json for them: they are the second half of each mandate gate, stated here for the same reason the flags are.',
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* Resolve the TEA module config keys that change which knowledge fragments the
|
|
3
3
|
* review loads, so a headless run never leaves them to the agent's discretion.
|
|
4
4
|
*
|
|
5
|
+
* A baseRef reads every layer from that pinned Git tree. Without it, layers
|
|
6
|
+
* come from the working tree. Missing base files use defaults; Git failures
|
|
7
|
+
* and malformed base config fail closed.
|
|
8
|
+
*
|
|
5
9
|
* Precedence, highest first:
|
|
6
10
|
* 1. An explicit CLI flag (--use-playwright-utils / --no-use-pactjs-utils / ...)
|
|
7
11
|
* 2. The consuming project's `[modules.tea]` table, merged from
|
|
8
|
-
* _bmad/config.toml, _bmad/custom/config.toml and
|
|
12
|
+
* _bmad/config.toml, _bmad/config.user.toml, _bmad/custom/config.toml and
|
|
9
13
|
* _bmad/custom/config.user.toml (later files win), as `bmad setup tea`
|
|
10
14
|
* writes it. Only when _bmad/config.toml does not exist is a v6
|
|
11
15
|
* _bmad/tea/config.yaml read instead, so older installs keep working in CI.
|
|
@@ -22,12 +26,16 @@
|
|
|
22
26
|
|
|
23
27
|
const fs = require('node:fs');
|
|
24
28
|
const path = require('node:path');
|
|
29
|
+
const { spawnSync } = require('node:child_process');
|
|
25
30
|
const yaml = require('js-yaml');
|
|
26
31
|
const TOML = require('smol-toml');
|
|
32
|
+
const { minimatch } = require('minimatch');
|
|
33
|
+
const { globSync } = require('glob');
|
|
27
34
|
|
|
28
35
|
/** The central config layers, lowest precedence first. Only the first is required. */
|
|
29
36
|
const CONFIG_LAYER_RELATIVE_PATHS = [
|
|
30
37
|
path.join('_bmad', 'config.toml'),
|
|
38
|
+
path.join('_bmad', 'config.user.toml'),
|
|
31
39
|
path.join('_bmad', 'custom', 'config.toml'),
|
|
32
40
|
path.join('_bmad', 'custom', 'config.user.toml'),
|
|
33
41
|
];
|
|
@@ -40,8 +48,19 @@ const MODULE_DEFAULTS = {
|
|
|
40
48
|
tea_pact_mcp: 'mcp',
|
|
41
49
|
tea_execution_mode: 'auto',
|
|
42
50
|
tea_capability_probe: true,
|
|
51
|
+
test_stack_type: 'auto',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const SNAPSHOT_DEFAULTS = {
|
|
55
|
+
test_artifacts: '{project-root}/_bmad-output/test-artifacts',
|
|
56
|
+
test_framework: 'auto',
|
|
43
57
|
};
|
|
44
58
|
|
|
59
|
+
const WORKFLOW_CUSTOM_PATHS = [
|
|
60
|
+
path.join('_bmad', 'custom', 'bmad-testarch-test-review.toml'),
|
|
61
|
+
path.join('_bmad', 'custom', 'bmad-testarch-test-review.user.toml'),
|
|
62
|
+
];
|
|
63
|
+
|
|
45
64
|
const PACT_MCP_VALUES = ['mcp', 'none'];
|
|
46
65
|
// step-03-quality-evaluation.md's requestable modes. `auto` asks the capability
|
|
47
66
|
// probe to pick; the other three are honoured as stated.
|
|
@@ -56,12 +75,108 @@ const FLAG_TO_KEY = {
|
|
|
56
75
|
capabilityProbe: 'tea_capability_probe',
|
|
57
76
|
};
|
|
58
77
|
|
|
78
|
+
/** Create a configuration error that the CLI publishes as an environment failure. */
|
|
59
79
|
function configError(message) {
|
|
60
80
|
const error = new Error(message);
|
|
61
81
|
error.code = 'TEA_CONFIG_INVALID';
|
|
62
82
|
return error;
|
|
63
83
|
}
|
|
64
84
|
|
|
85
|
+
/** Read configuration from one pinned base commit, or from the working tree. */
|
|
86
|
+
function configSource(projectRoot, baseRef) {
|
|
87
|
+
if (baseRef === undefined || baseRef === null) {
|
|
88
|
+
return {
|
|
89
|
+
label: (relativePath) => path.join(projectRoot, relativePath),
|
|
90
|
+
list(pattern) {
|
|
91
|
+
try {
|
|
92
|
+
return globSync(pattern, { cwd: projectRoot, dot: true, nodir: true, follow: false });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw configError(`Failed to list policy files in ${projectRoot}: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
read(relativePath) {
|
|
98
|
+
try {
|
|
99
|
+
return fs.readFileSync(path.join(projectRoot, relativePath), 'utf8');
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error.code === 'ENOENT') return null;
|
|
102
|
+
throw configError(`Failed to read ${path.join(projectRoot, relativePath)}: ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (typeof baseRef !== 'string' || baseRef.trim() === '' || baseRef.startsWith('-') || baseRef.includes('\0')) {
|
|
108
|
+
throw configError(`Invalid git base ref for config ${JSON.stringify(baseRef)}`);
|
|
109
|
+
}
|
|
110
|
+
const git = (args, cwd = projectRoot) => {
|
|
111
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024 });
|
|
112
|
+
if (result.error || result.status !== 0) {
|
|
113
|
+
throw configError(`Cannot read config from base ref ${JSON.stringify(baseRef)}: ${result.error?.message ?? result.stderr.trim()}`);
|
|
114
|
+
}
|
|
115
|
+
return result.stdout.replace(/\r?\n$/, '');
|
|
116
|
+
};
|
|
117
|
+
const commit = git(['rev-parse', '--verify', '--end-of-options', `${baseRef}^{commit}`]);
|
|
118
|
+
const repositoryRoot = git(['rev-parse', '--show-toplevel']);
|
|
119
|
+
const prefix = git(['rev-parse', '--show-prefix']);
|
|
120
|
+
const treePath = (relativePath) => prefix + relativePath.split(path.sep).join('/');
|
|
121
|
+
const cache = new Map();
|
|
122
|
+
const entries = new Map();
|
|
123
|
+
const entryAt = (file) => {
|
|
124
|
+
if (!entries.has(file)) {
|
|
125
|
+
entries.set(file, git(['--literal-pathspecs', 'ls-tree', '-z', '--full-tree', commit, '--', file], repositoryRoot));
|
|
126
|
+
}
|
|
127
|
+
return entries.get(file);
|
|
128
|
+
};
|
|
129
|
+
let listedPaths;
|
|
130
|
+
return {
|
|
131
|
+
commit,
|
|
132
|
+
list() {
|
|
133
|
+
if (!listedPaths) {
|
|
134
|
+
const args = ['--literal-pathspecs', 'ls-tree', '-r', '-z', '--name-only', '--full-tree', commit];
|
|
135
|
+
if (prefix) args.push('--', prefix);
|
|
136
|
+
listedPaths = git(args, repositoryRoot)
|
|
137
|
+
.split('\0')
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.map((file) => file.slice(prefix.length));
|
|
140
|
+
}
|
|
141
|
+
return listedPaths;
|
|
142
|
+
},
|
|
143
|
+
label: (relativePath) => `${commit}:${treePath(relativePath)}`,
|
|
144
|
+
read(relativePath) {
|
|
145
|
+
if (cache.has(relativePath)) return cache.get(relativePath);
|
|
146
|
+
const file = treePath(relativePath);
|
|
147
|
+
const segments = file.split('/');
|
|
148
|
+
for (let i = 1; i < segments.length; i++) {
|
|
149
|
+
const parent = segments.slice(0, i).join('/');
|
|
150
|
+
const ancestor = entryAt(parent);
|
|
151
|
+
if (ancestor === '') break;
|
|
152
|
+
if (!/^040000 tree [a-f0-9]+\t/.test(ancestor)) {
|
|
153
|
+
throw configError(`Config parent at ${commit}:${parent} must be a directory tree`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const entry = entryAt(file);
|
|
157
|
+
if (entry === '') {
|
|
158
|
+
cache.set(relativePath, null);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (!/^100(?:644|755) blob [a-f0-9]+\t/.test(entry)) {
|
|
162
|
+
throw configError(`Config at ${commit}:${treePath(relativePath)} must be a regular file`);
|
|
163
|
+
}
|
|
164
|
+
// Keep the blob's trailing newline intact for YAML block scalars.
|
|
165
|
+
const blob = spawnSync('git', ['show', `${commit}:${treePath(relativePath)}`], {
|
|
166
|
+
cwd: repositoryRoot,
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
timeout: 30_000,
|
|
169
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
170
|
+
});
|
|
171
|
+
if (blob.error || blob.status !== 0) {
|
|
172
|
+
throw configError(`Failed to read ${commit}:${treePath(relativePath)}: ${blob.error?.message ?? blob.stderr.trim()}`);
|
|
173
|
+
}
|
|
174
|
+
cache.set(relativePath, blob.stdout);
|
|
175
|
+
return blob.stdout;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
65
180
|
/**
|
|
66
181
|
* Coerce a config boolean. `bmad setup` writes the strings "true"/"false", a v6
|
|
67
182
|
* config.yaml or a hand-edited TOML file may carry real booleans. Accept both
|
|
@@ -124,13 +239,89 @@ function isPlainObject(value) {
|
|
|
124
239
|
return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date);
|
|
125
240
|
}
|
|
126
241
|
|
|
127
|
-
/**
|
|
242
|
+
/** Match BMad structural merging: recursive tables, keyed table arrays, and appended arrays. */
|
|
128
243
|
function mergeTables(base, override) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
244
|
+
if (isPlainObject(base) && isPlainObject(override)) {
|
|
245
|
+
const result = { ...base };
|
|
246
|
+
for (const [key, value] of Object.entries(override)) {
|
|
247
|
+
result[key] = Object.hasOwn(result, key) ? mergeTables(result[key], value) : value;
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(base) && Array.isArray(override)) {
|
|
252
|
+
const items = [...base, ...override];
|
|
253
|
+
const keyedField =
|
|
254
|
+
items.length > 0 && items.every(isPlainObject)
|
|
255
|
+
? ['code', 'id'].find((field) => items.every((item) => Object.hasOwn(item, field)))
|
|
256
|
+
: undefined;
|
|
257
|
+
if (!keyedField) return items;
|
|
258
|
+
for (const item of items) {
|
|
259
|
+
if (typeof item[keyedField] !== 'string' || item[keyedField] === '') {
|
|
260
|
+
throw configError(`Keyed array identifier ${keyedField} must be a nonempty string`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const result = [...base];
|
|
264
|
+
const indices = new Map(base.map((item, index) => [item[keyedField], index]));
|
|
265
|
+
for (const item of override) {
|
|
266
|
+
const key = item[keyedField];
|
|
267
|
+
if (indices.has(key)) result[indices.get(key)] = item;
|
|
268
|
+
else {
|
|
269
|
+
indices.set(key, result.length);
|
|
270
|
+
result.push(item);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
return override;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Parse a TOML layer with a consistent fail-closed diagnostic. */
|
|
279
|
+
function parseToml(content, label) {
|
|
280
|
+
try {
|
|
281
|
+
return TOML.parse(content);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
throw configError(`Failed to parse ${label}: ${error.message}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Resolve trusted skill defaults and project workflow overrides from the same configuration source. */
|
|
288
|
+
function readWorkflowCustomization(source, skillRoot, projectRoot) {
|
|
289
|
+
if (!skillRoot) return {};
|
|
290
|
+
const defaultsPath = path.join(skillRoot, 'customize.toml');
|
|
291
|
+
let merged = {};
|
|
292
|
+
try {
|
|
293
|
+
merged = parseToml(fs.readFileSync(defaultsPath, 'utf8'), defaultsPath);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error.code !== 'ENOENT') throw configError(`Failed to load ${defaultsPath}: ${error.message}`);
|
|
296
|
+
}
|
|
297
|
+
for (const relativePath of WORKFLOW_CUSTOM_PATHS) {
|
|
298
|
+
const content = source.read(relativePath);
|
|
299
|
+
if (content !== null) merged = mergeTables(merged, parseToml(content, source.label(relativePath)));
|
|
132
300
|
}
|
|
133
|
-
|
|
301
|
+
if (merged.workflow !== undefined && !isPlainObject(merged.workflow)) {
|
|
302
|
+
throw configError('Workflow customization must contain a [workflow] table');
|
|
303
|
+
}
|
|
304
|
+
const workflow = merged.workflow || {};
|
|
305
|
+
if (workflow.persistent_facts !== undefined) {
|
|
306
|
+
if (!Array.isArray(workflow.persistent_facts) || !workflow.persistent_facts.every((fact) => typeof fact === 'string')) {
|
|
307
|
+
throw configError('Workflow persistent_facts must be an array of strings');
|
|
308
|
+
}
|
|
309
|
+
workflow.persistent_facts = workflow.persistent_facts.flatMap((fact) => {
|
|
310
|
+
if (!fact.startsWith('file:')) return [fact];
|
|
311
|
+
const rawPath = fact.slice('file:'.length).replaceAll('{project-root}', projectRoot);
|
|
312
|
+
const pattern = path.relative(projectRoot, path.resolve(projectRoot, rawPath)).split(path.sep).join('/');
|
|
313
|
+
if (pattern === '' || pattern === '..' || pattern.startsWith('../') || path.isAbsolute(pattern)) {
|
|
314
|
+
throw configError(`Persistent fact must refer to a file inside the project: ${fact}`);
|
|
315
|
+
}
|
|
316
|
+
const matching = source
|
|
317
|
+
.list(pattern)
|
|
318
|
+
.filter((file) => minimatch(file, pattern, { dot: true, nonegate: true, nocomment: true }))
|
|
319
|
+
.sort();
|
|
320
|
+
if (matching.length === 0) throw configError(`Persistent fact has no files in the configuration source: ${fact}`);
|
|
321
|
+
return matching.map((file) => `Resolved policy ${source.label(file)}:\n${source.read(file)}`);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return workflow;
|
|
134
325
|
}
|
|
135
326
|
|
|
136
327
|
/** Coerce the keys this CLI cares about out of a raw TEA config table. */
|
|
@@ -151,50 +342,51 @@ function pickTeaValues(table, source) {
|
|
|
151
342
|
if ('tea_capability_probe' in table) {
|
|
152
343
|
values.tea_capability_probe = coerceBoolean(table.tea_capability_probe, 'tea_capability_probe', source);
|
|
153
344
|
}
|
|
345
|
+
if ('test_stack_type' in table) {
|
|
346
|
+
const stack = typeof table.test_stack_type === 'string' ? table.test_stack_type.trim().toLowerCase() : '';
|
|
347
|
+
if (!['auto', 'frontend', 'backend', 'fullstack', 'mobile'].includes(stack)) {
|
|
348
|
+
throw configError(`test_stack_type in ${source} must be auto, frontend, backend, fullstack or mobile`);
|
|
349
|
+
}
|
|
350
|
+
values.test_stack_type = stack;
|
|
351
|
+
}
|
|
154
352
|
return values;
|
|
155
353
|
}
|
|
156
354
|
|
|
157
355
|
/**
|
|
158
|
-
* Merge the central TOML layers and return
|
|
356
|
+
* Merge the central TOML layers and return their core and TEA tables.
|
|
159
357
|
*
|
|
160
|
-
* @param {
|
|
161
|
-
* @returns {object}
|
|
358
|
+
* @param {object} source - Working-tree or pinned-base configuration reader.
|
|
359
|
+
* @returns {{core: object, tea: object}}
|
|
162
360
|
*/
|
|
163
|
-
function
|
|
361
|
+
function readCentralConfig(source) {
|
|
164
362
|
let merged = {};
|
|
165
363
|
for (const relativePath of CONFIG_LAYER_RELATIVE_PATHS) {
|
|
166
|
-
const
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
let parsed;
|
|
171
|
-
try {
|
|
172
|
-
parsed = TOML.parse(fs.readFileSync(layerPath, 'utf8'));
|
|
173
|
-
} catch (error) {
|
|
174
|
-
throw configError(`Failed to parse ${layerPath}: ${error.message}`);
|
|
175
|
-
}
|
|
364
|
+
const content = source.read(relativePath);
|
|
365
|
+
if (content === null) continue;
|
|
366
|
+
const parsed = parseToml(content, source.label(relativePath));
|
|
176
367
|
merged = mergeTables(merged, parsed);
|
|
177
368
|
}
|
|
178
369
|
const modules = merged.modules;
|
|
179
|
-
if (modules
|
|
180
|
-
|
|
370
|
+
if (modules !== undefined && (!isPlainObject(modules) || (modules.tea !== undefined && !isPlainObject(modules.tea)))) {
|
|
371
|
+
throw configError(`[modules.tea] in ${source.label(CONFIG_RELATIVE_PATH)} must be a table of config keys`);
|
|
181
372
|
}
|
|
182
|
-
if (
|
|
183
|
-
throw configError(`[
|
|
373
|
+
if (merged.core !== undefined && !isPlainObject(merged.core)) {
|
|
374
|
+
throw configError(`[core] in ${source.label(CONFIG_RELATIVE_PATH)} must be a table of config keys`);
|
|
184
375
|
}
|
|
185
|
-
return
|
|
376
|
+
return { core: merged.core || {}, tea: modules?.tea || {} };
|
|
186
377
|
}
|
|
187
378
|
|
|
188
379
|
/**
|
|
189
380
|
* Read a v6 _bmad/tea/config.yaml, used only when _bmad/config.toml is absent.
|
|
190
381
|
*
|
|
191
382
|
* @param {string} configPath
|
|
383
|
+
* @param {string} content
|
|
192
384
|
* @returns {object}
|
|
193
385
|
*/
|
|
194
|
-
function readLegacyTeaTable(configPath) {
|
|
386
|
+
function readLegacyTeaTable(configPath, content) {
|
|
195
387
|
let parsed;
|
|
196
388
|
try {
|
|
197
|
-
parsed = yaml.load(
|
|
389
|
+
parsed = yaml.load(content);
|
|
198
390
|
} catch (error) {
|
|
199
391
|
throw configError(`Failed to parse ${configPath}: ${error.message}`);
|
|
200
392
|
}
|
|
@@ -213,30 +405,34 @@ function readLegacyTeaTable(configPath) {
|
|
|
213
405
|
* unreadable or unparseable content is a configuration error.
|
|
214
406
|
*
|
|
215
407
|
* @param {string} projectRoot - Consuming project root.
|
|
408
|
+
* @param {string} [baseRef] - Base commit or ref; omitted for working-tree config.
|
|
216
409
|
* @returns {{present: boolean, path: string, format: 'toml'|'yaml'|null, values: object}}
|
|
217
410
|
*/
|
|
218
|
-
function readTeaConfigFile(projectRoot) {
|
|
219
|
-
const centralPath =
|
|
220
|
-
if (
|
|
411
|
+
function readTeaConfigFile(projectRoot, baseRef, source = configSource(projectRoot, baseRef)) {
|
|
412
|
+
const centralPath = source.label(CONFIG_RELATIVE_PATH);
|
|
413
|
+
if (source.read(CONFIG_RELATIVE_PATH) !== null) {
|
|
414
|
+
const config = readCentralConfig(source);
|
|
221
415
|
return {
|
|
222
416
|
present: true,
|
|
223
417
|
path: centralPath,
|
|
224
418
|
format: 'toml',
|
|
225
|
-
values: pickTeaValues(
|
|
419
|
+
values: pickTeaValues(config.tea, centralPath),
|
|
420
|
+
core: config.core,
|
|
421
|
+
tea: config.tea,
|
|
226
422
|
};
|
|
227
423
|
}
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
424
|
+
const legacyPath = source.label(LEGACY_CONFIG_RELATIVE_PATH);
|
|
425
|
+
const legacy = source.read(LEGACY_CONFIG_RELATIVE_PATH);
|
|
426
|
+
if (legacy !== null) {
|
|
427
|
+
const table = readLegacyTeaTable(legacyPath, legacy);
|
|
428
|
+
const core = Object.fromEntries(
|
|
429
|
+
['user_name', 'communication_language', 'document_output_language', 'output_folder']
|
|
430
|
+
.filter((key) => key in table)
|
|
431
|
+
.map((key) => [key, table[key]]),
|
|
432
|
+
);
|
|
433
|
+
return { present: true, path: legacyPath, format: 'yaml', values: pickTeaValues(table, legacyPath), core, tea: table };
|
|
237
434
|
}
|
|
238
|
-
|
|
239
|
-
return { present: false, path: centralPath, format: null, values: {} };
|
|
435
|
+
return { present: false, path: centralPath, format: null, values: {}, core: {}, tea: {} };
|
|
240
436
|
}
|
|
241
437
|
|
|
242
438
|
/**
|
|
@@ -292,15 +488,19 @@ const KEY_TO_INSTALLED_FIELD = {
|
|
|
292
488
|
*
|
|
293
489
|
* @param {object} options
|
|
294
490
|
* @param {string} options.projectRoot - Consuming project root.
|
|
491
|
+
* @param {string} [options.baseRef] - Configuration source ref, pinned before any file read.
|
|
492
|
+
* @param {string} [options.skillRoot] - Trusted skill defaults for workflow customization.
|
|
295
493
|
* @param {object} [options.flags] - Parsed CLI options; only the keys in
|
|
296
494
|
* FLAG_TO_KEY are read, and only when not undefined.
|
|
297
|
-
* @returns {{values: object, sources: object, installed: object, configPath: string, configPresent: boolean, configFormat: string|null}}
|
|
495
|
+
* @returns {{values: object, sources: object, installed: object, configSnapshot: object, workflowCustomization: object, configCommit: string|null, configPath: string, configPresent: boolean, configFormat: string|null}}
|
|
298
496
|
* `installed` carries one boolean per library gate, read from the project
|
|
299
497
|
* manifest rather than left to the agent.
|
|
300
498
|
* @throws {Error} With code TEA_CONFIG_INVALID on unusable config content.
|
|
301
499
|
*/
|
|
302
|
-
function resolveTeaConfig({ projectRoot, flags = {} }) {
|
|
303
|
-
const
|
|
500
|
+
function resolveTeaConfig({ projectRoot, flags = {}, baseRef, skillRoot }) {
|
|
501
|
+
const source = configSource(projectRoot, baseRef);
|
|
502
|
+
const file = readTeaConfigFile(projectRoot, baseRef, source);
|
|
503
|
+
const workflowCustomization = readWorkflowCustomization(source, skillRoot, projectRoot);
|
|
304
504
|
|
|
305
505
|
const values = {};
|
|
306
506
|
const sources = {};
|
|
@@ -327,12 +527,29 @@ function resolveTeaConfig({ projectRoot, flags = {} }) {
|
|
|
327
527
|
sources[key] = 'default';
|
|
328
528
|
}
|
|
329
529
|
|
|
530
|
+
values.test_stack_type = file.values.test_stack_type ?? MODULE_DEFAULTS.test_stack_type;
|
|
531
|
+
sources.test_stack_type = 'test_stack_type' in file.values ? 'config' : 'default';
|
|
532
|
+
|
|
330
533
|
const installed = {};
|
|
331
534
|
for (const [key, packageName] of Object.entries(KEY_TO_PACKAGE)) {
|
|
332
535
|
installed[KEY_TO_INSTALLED_FIELD[key]] = isPackageInstalled(projectRoot, packageName);
|
|
333
536
|
}
|
|
334
537
|
|
|
335
|
-
|
|
538
|
+
const configSnapshot = {
|
|
539
|
+
core: { user_name: 'User', communication_language: 'English', document_output_language: 'English', ...file.core },
|
|
540
|
+
modules: { tea: { ...SNAPSHOT_DEFAULTS, ...file.tea, ...values, tea_browser_automation: 'none' } },
|
|
541
|
+
};
|
|
542
|
+
return {
|
|
543
|
+
values,
|
|
544
|
+
sources,
|
|
545
|
+
installed,
|
|
546
|
+
configSnapshot,
|
|
547
|
+
workflowCustomization,
|
|
548
|
+
configCommit: source.commit ?? null,
|
|
549
|
+
configPath: file.path,
|
|
550
|
+
configPresent: file.present,
|
|
551
|
+
configFormat: file.format,
|
|
552
|
+
};
|
|
336
553
|
}
|
|
337
554
|
|
|
338
555
|
module.exports = {
|
|
@@ -342,6 +559,7 @@ module.exports = {
|
|
|
342
559
|
KEY_TO_PACKAGE,
|
|
343
560
|
KEY_TO_INSTALLED_FIELD,
|
|
344
561
|
MODULE_DEFAULTS,
|
|
562
|
+
SNAPSHOT_DEFAULTS,
|
|
345
563
|
EXECUTION_MODE_VALUES,
|
|
346
564
|
PACT_MCP_VALUES,
|
|
347
565
|
CONFIG_RELATIVE_PATH,
|
|
@@ -70,9 +70,18 @@ function workflowRunFromEnvironment(env) {
|
|
|
70
70
|
* Missing values stay null. The sources map tells artifact consumers where
|
|
71
71
|
* each value came from, or why this process could not know it.
|
|
72
72
|
*/
|
|
73
|
-
function buildReviewProvenance({
|
|
73
|
+
function buildReviewProvenance({
|
|
74
|
+
projectRoot,
|
|
75
|
+
skillRoot,
|
|
76
|
+
baseRef,
|
|
77
|
+
baseCommit,
|
|
78
|
+
filesProvided,
|
|
79
|
+
modelIdentifier,
|
|
80
|
+
gateMode = 'all',
|
|
81
|
+
env = process.env,
|
|
82
|
+
}) {
|
|
74
83
|
const rubricVersion = skillRubricVersion(skillRoot);
|
|
75
|
-
const baseSha = filesProvided ? null : gitSha(projectRoot, baseRef);
|
|
84
|
+
const baseSha = baseCommit ?? (filesProvided ? null : gitSha(projectRoot, baseRef));
|
|
76
85
|
const headSha = gitSha(projectRoot, 'HEAD');
|
|
77
86
|
const triggerComment = triggerCommentFromEnvironment(env);
|
|
78
87
|
const workflowRun = workflowRunFromEnvironment(env);
|
|
@@ -93,11 +102,14 @@ function buildReviewProvenance({ projectRoot, skillRoot, baseRef, filesProvided,
|
|
|
93
102
|
: 'unavailable: skill workflow.yaml has no readable rubric_version',
|
|
94
103
|
modelIdentifier:
|
|
95
104
|
modelIdentifier == null ? 'unavailable: selected adapter does not expose a model identifier' : 'resolved agent adapter model',
|
|
96
|
-
baseSha:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
baseSha:
|
|
106
|
+
baseCommit && filesProvided
|
|
107
|
+
? `config snapshot: git rev-parse ${baseRef}^{commit}; --files has no comparison base`
|
|
108
|
+
: filesProvided
|
|
109
|
+
? 'unavailable: --files bypassed the git comparison base'
|
|
110
|
+
: baseSha
|
|
111
|
+
? `git rev-parse ${baseRef}^{commit}`
|
|
112
|
+
: `unavailable: git could not resolve ${baseRef}`,
|
|
101
113
|
headSha: headSha ? 'git rev-parse HEAD^{commit}' : 'unavailable: project root is not a readable git worktree',
|
|
102
114
|
triggerComment: triggerComment
|
|
103
115
|
? 'GitHub event comment URL or id'
|