bmad-method-test-architecture-enterprise 3.2.0 → 3.3.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/.claude-plugin/marketplace.json +1 -1
- package/cli/evaluate.js +20 -0
- package/cli/lib/agent-adapters.js +46 -0
- package/cli/lib/evaluate/configuration-experiment.js +3401 -0
- package/cli/lib/evaluate/experiments.js +1299 -0
- package/cli/lib/evaluate/retain-skill-artifacts.js +78 -0
- package/cli/lib/evaluate/test-design-corpus/clean/docs/epics/epic-9-last-sync-indicator.md +47 -0
- package/cli/lib/evaluate/test-design-corpus/ground-truth.json +222 -0
- package/cli/lib/evaluate/test-design-corpus/native-identity.json +4 -0
- package/cli/lib/evaluate/test-design-corpus/seeded/docs/epics/epic-7-offline-order-capture.md +75 -0
- package/cli/lib/evaluate/test-design-replay.js +452 -0
- package/cli/lib/run-agent.js +18 -2
- package/cli/lib/test-design-staged-config.js +60 -0
- package/cli/lib/test-design-staging.js +229 -0
- package/cli/test-design-runner.js +245 -11
- package/package.json +3 -2
- package/skills/bmad-tea-evaluate/SKILL.md +11 -0
- package/skills/bmad-tea-evaluate/references/configuration-experiments.md +37 -0
- package/skills/bmod-tea/bmod.toml +1 -1
- package/skills/bmod-tea/help/help.md +1 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/** Native Test Design staging, shared with the checkout harness. */
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { digest } = require('./evaluate/digest');
|
|
7
|
+
const { TEA_CONFIG_RELATIVE_PATH, TEA_KNOWLEDGE_PROMPT_LINE, stageTeaKnowledge, teaConfigToml } = require('./test-design-staged-config');
|
|
8
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
9
|
+
const FIXTURE_ROOT = path.join(__dirname, 'evaluate/test-design-corpus');
|
|
10
|
+
const GROUND_TRUTH = path.join(FIXTURE_ROOT, 'ground-truth.json');
|
|
11
|
+
const SKILL_ROOT = path.join(PROJECT_ROOT, 'skills/bmad-tea-test-design');
|
|
12
|
+
async function readText(file) {
|
|
13
|
+
try {
|
|
14
|
+
return { present: true, text: fs.readFileSync(file, 'utf8') };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error.code === 'ENOENT') return { present: false };
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function writeText(file, text) {
|
|
21
|
+
fs.writeFileSync(file, text);
|
|
22
|
+
}
|
|
23
|
+
const GROUND_TRUTH_ONLY_TOKENS = [
|
|
24
|
+
'materialRisks',
|
|
25
|
+
'unsupportedRisks',
|
|
26
|
+
'severityRank',
|
|
27
|
+
'acceptableCoverage',
|
|
28
|
+
'groundingQuote',
|
|
29
|
+
'exclusionQuote',
|
|
30
|
+
'maxRisks',
|
|
31
|
+
// The two keys that carry the matcher vocabulary itself. Neither string occurs in
|
|
32
|
+
// the staged skill tree or in either epic, so naming them costs nothing and closes
|
|
33
|
+
// the one channel through which the matchers could reach the agent.
|
|
34
|
+
'anyOf',
|
|
35
|
+
'exampleDescription',
|
|
36
|
+
];
|
|
37
|
+
function projectRootOf(set) {
|
|
38
|
+
return set.projectRoot;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function designArtifactPaths(set) {
|
|
42
|
+
const design = path.posix.join(projectRootOf(set), 'test-artifacts', 'test-design', `test-design-epic-${set.epicNum}.md`);
|
|
43
|
+
return { design };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function filesUnder(root) {
|
|
47
|
+
const found = [];
|
|
48
|
+
const walk = (directory, prefix) => {
|
|
49
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
50
|
+
const absolute = path.join(directory, entry.name);
|
|
51
|
+
const relative = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
52
|
+
if (entry.isDirectory()) walk(absolute, relative);
|
|
53
|
+
else if (entry.isFile()) found.push(relative);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
if (fs.existsSync(root)) walk(root, '');
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function digestTree(root, relativePaths, io = {}) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
for (const relative of relativePaths) {
|
|
63
|
+
const read = await (io.readText ?? readText)(path.join(root, relative));
|
|
64
|
+
// `fs.readFileSync` threw here and the behaviour is kept, named. Absence has
|
|
65
|
+
// to be loud: the alternative is decoding a null into the digest, so a file
|
|
66
|
+
// the walk found and the read did not would produce a digest that compares
|
|
67
|
+
// equal to nothing and reads as a mutation nobody can locate.
|
|
68
|
+
if (!read.present) {
|
|
69
|
+
throw new Error(`${relative} was listed under ${root} and could not be read, so the corpus digest cannot be taken`);
|
|
70
|
+
}
|
|
71
|
+
parts.push(`${relative}\0${read.text}`);
|
|
72
|
+
}
|
|
73
|
+
return digest(parts.join('\u0001'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function configToml() {
|
|
77
|
+
return teaConfigToml({
|
|
78
|
+
header: ['# Written by the native Test Design fixture runner for one staged fixture set.'],
|
|
79
|
+
core: [
|
|
80
|
+
['user_name', 'tea-eval-harness'],
|
|
81
|
+
['project_name', 'field-service-platform'],
|
|
82
|
+
['communication_language', 'English'],
|
|
83
|
+
['document_output_language', 'English'],
|
|
84
|
+
['output_folder', 'docs'],
|
|
85
|
+
],
|
|
86
|
+
tea: [['test_artifacts', 'test-artifacts']],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function corpusMutations(workspace, io = {}) {
|
|
91
|
+
const changed = (await digestTree(workspace.projectDir, workspace.corpusFiles, io)) === workspace.corpusDigest ? 0 : 1;
|
|
92
|
+
const added = filesUnder(workspace.projectDir).filter(
|
|
93
|
+
(relative) =>
|
|
94
|
+
!relative.startsWith(`test-artifacts${path.sep}`) &&
|
|
95
|
+
!relative.startsWith(`_bmad${path.sep}`) &&
|
|
96
|
+
!workspace.corpusFiles.includes(relative),
|
|
97
|
+
);
|
|
98
|
+
return changed + added.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function stageWorkspace(set, io = {}) {
|
|
102
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tea-test-design-eval-'));
|
|
103
|
+
try {
|
|
104
|
+
return await stageIntoWorkspace(dir, set, io);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function stageIntoWorkspace(dir, set, io = {}) {
|
|
112
|
+
const projectDir = path.join(dir, projectRootOf(set));
|
|
113
|
+
const setRoot = path.join(FIXTURE_ROOT, set.root);
|
|
114
|
+
|
|
115
|
+
for (const relative of filesUnder(setRoot)) {
|
|
116
|
+
const target = path.join(projectDir, relative);
|
|
117
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
118
|
+
fs.copyFileSync(path.join(setRoot, relative), target);
|
|
119
|
+
}
|
|
120
|
+
// The workflow writes its document and its progress checkpoint here. Neither set
|
|
121
|
+
// brings anything into this directory, so a run cannot inherit a checkpoint.
|
|
122
|
+
fs.mkdirSync(path.join(projectDir, 'test-artifacts'), { recursive: true });
|
|
123
|
+
fs.mkdirSync(path.join(projectDir, '_bmad'), { recursive: true });
|
|
124
|
+
await (io.writeText ?? writeText)(path.join(projectDir, TEA_CONFIG_RELATIVE_PATH), configToml());
|
|
125
|
+
|
|
126
|
+
for (const relative of filesUnder(SKILL_ROOT)) {
|
|
127
|
+
const target = path.join(dir, 'skill', relative);
|
|
128
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
129
|
+
fs.copyFileSync(path.join(SKILL_ROOT, relative), target);
|
|
130
|
+
}
|
|
131
|
+
stageTeaKnowledge(dir);
|
|
132
|
+
|
|
133
|
+
// The corpus files the run must leave alone. test-artifacts and _bmad are excluded
|
|
134
|
+
// because the run legitimately writes into the first and the harness wrote the second.
|
|
135
|
+
const corpusFiles = filesUnder(projectDir).filter(
|
|
136
|
+
(relative) => !relative.startsWith(`test-artifacts${path.sep}`) && !relative.startsWith(`_bmad${path.sep}`),
|
|
137
|
+
);
|
|
138
|
+
return { dir, projectDir, corpusFiles, corpusDigest: await digestTree(projectDir, corpusFiles, io) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function assertGroundTruthAbsent(dir, io = {}) {
|
|
142
|
+
const problems = [];
|
|
143
|
+
// Both existence checks are gone: each read answers absence itself. A staged
|
|
144
|
+
// file that vanished between the walk and the read used to contribute an empty
|
|
145
|
+
// string, which is the same silence one file at a time, and is reported now.
|
|
146
|
+
const groundTruthBytes = (await (io.readText ?? readText)(io.groundTruth ?? GROUND_TRUTH)).text;
|
|
147
|
+
for (const relative of filesUnder(dir)) {
|
|
148
|
+
if (path.basename(relative) === 'ground-truth.json') {
|
|
149
|
+
problems.push(`staged workspace contains ${relative}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const staged = await (io.readText ?? readText)(path.join(dir, relative));
|
|
153
|
+
if (!staged.present) {
|
|
154
|
+
problems.push(`staged file ${relative} disappeared between the walk and the read, so it was not checked`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const text = staged.text;
|
|
158
|
+
if (groundTruthBytes && text === groundTruthBytes) {
|
|
159
|
+
problems.push(`staged file ${relative} carries the ground truth verbatim`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
for (const token of GROUND_TRUTH_ONLY_TOKENS) {
|
|
163
|
+
if (text.includes(token)) problems.push(`staged file ${relative} carries the ground-truth-only key "${token}"`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return problems;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildPrompt(set, { designLevel = 'full' } = {}) {
|
|
170
|
+
const root = projectRootOf(set);
|
|
171
|
+
return [
|
|
172
|
+
`You are running the TEA workflow \`bmad-tea-test-design\` against the project in \`${root}/\`.`,
|
|
173
|
+
'',
|
|
174
|
+
'The workflow is in `skill/`. Read `skill/instructions.md` first, then execute every step file it',
|
|
175
|
+
'names in order, in full, without skipping or reordering. The step files are under `skill/steps-c/`.',
|
|
176
|
+
'',
|
|
177
|
+
'----- run configuration -----',
|
|
178
|
+
'Take mode C (Create). Resolve the workflow placeholders to these values:',
|
|
179
|
+
'',
|
|
180
|
+
`- \`{project-root}\`: \`${root}\``,
|
|
181
|
+
`- TEA config (\`[core]\` and \`[modules.tea]\`): \`${root}/_bmad/config.toml\``,
|
|
182
|
+
`- \`{test_artifacts}\`: \`${root}/test-artifacts\``,
|
|
183
|
+
'- `{skill-root}`: `skill`',
|
|
184
|
+
TEA_KNOWLEDGE_PROMPT_LINE,
|
|
185
|
+
'- `mode`: `epic-level`',
|
|
186
|
+
'- `run_scope`: `epic`',
|
|
187
|
+
`- \`epic_num\`: \`${set.epicNum}\``,
|
|
188
|
+
`- \`run_key\`: \`epic-${set.epicNum}\``,
|
|
189
|
+
`- \`design_level\`: \`${designLevel}\``,
|
|
190
|
+
'- `tea_execution_mode`: `sequential`',
|
|
191
|
+
'- `tea_capability_probe`: `false`',
|
|
192
|
+
'',
|
|
193
|
+
`The epic is the one document under \`${root}/docs/epics/\`. It is the only requirements source for this`,
|
|
194
|
+
'run; there is no PRD, no architecture document and no sprint status file, and none is missing.',
|
|
195
|
+
'',
|
|
196
|
+
'----- what to produce -----',
|
|
197
|
+
'Write the one deliverable epic-level mode declares:',
|
|
198
|
+
'',
|
|
199
|
+
`- \`${root}/test-artifacts/test-design/test-design-epic-${set.epicNum}.md\`, from \`skill/test-design-template.md\`, carrying`,
|
|
200
|
+
' the risk assessment and the test coverage plan the template lays out, with each risk row stating its',
|
|
201
|
+
' category, probability, impact and score, and each coverage row naming its test level and the risk it',
|
|
202
|
+
' covers.',
|
|
203
|
+
'',
|
|
204
|
+
`Do not add, edit, or delete any file under \`${root}/docs/\`. This workflow does not change its inputs,`,
|
|
205
|
+
'and it does not generate tests.',
|
|
206
|
+
'',
|
|
207
|
+
'Do not stop to ask a question. Every value the workflow would otherwise ask for is stated above.',
|
|
208
|
+
'',
|
|
209
|
+
'When you are done, print one line naming the file you wrote. Nothing else you print is read.',
|
|
210
|
+
].join('\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function caseIndex(sets) {
|
|
214
|
+
return sets.map((set) => ({ id: set.id, prompt: buildPrompt(set) }));
|
|
215
|
+
}
|
|
216
|
+
module.exports = {
|
|
217
|
+
TEA_CONFIG_RELATIVE_PATH,
|
|
218
|
+
corpusMutations,
|
|
219
|
+
projectRootOf,
|
|
220
|
+
designArtifactPaths,
|
|
221
|
+
filesUnder,
|
|
222
|
+
digestTree,
|
|
223
|
+
configToml,
|
|
224
|
+
stageWorkspace,
|
|
225
|
+
stageIntoWorkspace,
|
|
226
|
+
assertGroundTruthAbsent,
|
|
227
|
+
buildPrompt,
|
|
228
|
+
caseIndex,
|
|
229
|
+
};
|
|
@@ -52,9 +52,10 @@
|
|
|
52
52
|
|
|
53
53
|
const fs = require('node:fs');
|
|
54
54
|
const path = require('node:path');
|
|
55
|
+
const { createHash, randomUUID } = require('node:crypto');
|
|
55
56
|
const { Command } = require('commander');
|
|
56
57
|
|
|
57
|
-
const { AGENT_ADAPTERS } = require('./lib/agent-adapters');
|
|
58
|
+
const { AGENT_ADAPTERS, resolveModel, agentCaptureTelemetry } = require('./lib/agent-adapters');
|
|
58
59
|
const { runAgent } = require('./lib/run-agent');
|
|
59
60
|
const { EXIT_CODES, classOfAgentError, failureClassForExit, vendorEnvironmentNames } = require('./lib/runner-exit-codes');
|
|
60
61
|
const { readDesign, scoredRiskProjection } = require('./lib/test-design-parser');
|
|
@@ -81,7 +82,8 @@ const TEST_DESIGN_REQUEST_KEYS = {
|
|
|
81
82
|
argument: { required: [], permitted: [] },
|
|
82
83
|
option: {
|
|
83
84
|
required: ['agent', 'design-path'],
|
|
84
|
-
permitted: ['agent', 'agent-cmd', 'agent-arg', 'design-path', 'env-pass', 'model', 'timeout-ms'],
|
|
85
|
+
permitted: ['agent', 'agent-cmd', 'agent-arg', 'design-path', 'env-pass', 'model', 'timeout-ms', 'capture-path', 'capture-test-double'],
|
|
86
|
+
types: { 'capture-test-double': 'boolean' },
|
|
85
87
|
},
|
|
86
88
|
environment: { required: [], permitted: vendorEnvironmentNames() },
|
|
87
89
|
stdin: { required: ['prompt'], permitted: ['prompt'] },
|
|
@@ -107,7 +109,7 @@ function readPrompt() {
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
/** Derive a projection only after the agent has finished its document. */
|
|
110
|
-
function
|
|
112
|
+
function designText(cwd, relativeDesignPath) {
|
|
111
113
|
const relative = path.normalize(relativeDesignPath);
|
|
112
114
|
if (path.isAbsolute(relative) || relative.startsWith(`..${path.sep}`) || relative === '..' || !relative.endsWith('.md')) {
|
|
113
115
|
throw new Error(`--design-path must name a markdown file inside the working directory: ${JSON.stringify(relativeDesignPath)}`);
|
|
@@ -120,8 +122,13 @@ function projectScoredRisks(cwd, relativeDesignPath) {
|
|
|
120
122
|
if (fromRoot === '..' || fromRoot.startsWith(`..${path.sep}`) || path.isAbsolute(fromRoot) || fs.lstatSync(designPath).isSymbolicLink()) {
|
|
121
123
|
throw new Error(`design path escapes the working directory: ${JSON.stringify(relativeDesignPath)}`);
|
|
122
124
|
}
|
|
123
|
-
|
|
125
|
+
return fs.readFileSync(designPath, 'utf8');
|
|
126
|
+
}
|
|
127
|
+
function projectScoredRisks(cwd, relativeDesignPath, { requireDeliverable = false } = {}) {
|
|
128
|
+
const text = designText(cwd, relativeDesignPath);
|
|
129
|
+
if (text === null) return null;
|
|
124
130
|
const read = readDesign({ kind: 'text', value: text });
|
|
131
|
+
if (!read.ok && requireDeliverable) throw Object.assign(new Error(read.reason), { failureClass: read.failureClass });
|
|
125
132
|
return scoredRiskProjection(read.ok ? read.design : { risks: [], text });
|
|
126
133
|
}
|
|
127
134
|
|
|
@@ -136,6 +143,8 @@ function main(argv) {
|
|
|
136
143
|
.option('--env-pass <NAME>', 'environment variable name allowed through to the agent (repeatable)', collect, [])
|
|
137
144
|
.option('--model <name>', 'model to pin for this run; defaults to the adapter default')
|
|
138
145
|
.option('--design-path <path>', 'staged design document path for a deterministic scored-risk projection')
|
|
146
|
+
.option('--capture-path <path>', 'opt-in source receipt and telemetry inside the workspace')
|
|
147
|
+
.option('--capture-test-double', 'label custom telemetry as a deterministic test double')
|
|
139
148
|
.option('--timeout-ms <n>', 'agent wall-clock timeout in milliseconds', String(DEFAULT_TIMEOUT_MS));
|
|
140
149
|
|
|
141
150
|
program.exitOverride();
|
|
@@ -151,6 +160,9 @@ function main(argv) {
|
|
|
151
160
|
if (!Object.hasOwn(AGENT_ADAPTERS, options.agent)) {
|
|
152
161
|
fail('environment-configuration', `unknown agent "${options.agent}"; expected one of ${Object.keys(AGENT_ADAPTERS).join(', ')}`);
|
|
153
162
|
}
|
|
163
|
+
if (options.captureTestDouble && options.agent !== 'custom') {
|
|
164
|
+
fail('usage', '--capture-test-double requires --agent custom');
|
|
165
|
+
}
|
|
154
166
|
// Tested on the raw string before the parse. Number.parseInt truncates, so
|
|
155
167
|
// `--timeout-ms 20abc` became 20 and every run then timed out at twenty
|
|
156
168
|
// milliseconds, and `--timeout-ms 1e9` became 1.
|
|
@@ -168,29 +180,251 @@ function main(argv) {
|
|
|
168
180
|
}
|
|
169
181
|
|
|
170
182
|
let stdout = '';
|
|
183
|
+
let source = null;
|
|
184
|
+
let captureFile = null;
|
|
185
|
+
let captureFd = null;
|
|
186
|
+
let captureIdentity = null;
|
|
187
|
+
let captureBytes = null;
|
|
188
|
+
let captureResult = null;
|
|
189
|
+
const reportCapture = () => {
|
|
190
|
+
if (captureResult) process.stdout.write(`${JSON.stringify({ sourceCapture: captureResult })}\n`);
|
|
191
|
+
};
|
|
192
|
+
const saveCapture = () => {
|
|
193
|
+
if (captureFd === null) return;
|
|
194
|
+
let integrityFailure = null;
|
|
195
|
+
try {
|
|
196
|
+
const held = fs.fstatSync(captureFd);
|
|
197
|
+
const named = fs.lstatSync(captureFile);
|
|
198
|
+
if (
|
|
199
|
+
!held.isFile() ||
|
|
200
|
+
held.nlink !== 1 ||
|
|
201
|
+
!named.isFile() ||
|
|
202
|
+
named.nlink !== 1 ||
|
|
203
|
+
held.dev !== captureIdentity.dev ||
|
|
204
|
+
held.ino !== captureIdentity.ino ||
|
|
205
|
+
named.dev !== held.dev ||
|
|
206
|
+
named.ino !== held.ino
|
|
207
|
+
)
|
|
208
|
+
integrityFailure = 'source capture path or link identity changed';
|
|
209
|
+
if (!integrityFailure && captureBytes !== null) {
|
|
210
|
+
if (held.size === captureBytes.length) {
|
|
211
|
+
const current = Buffer.alloc(held.size);
|
|
212
|
+
fs.readSync(captureFd, current, 0, current.length, 0);
|
|
213
|
+
if (!current.equals(captureBytes)) integrityFailure = 'source capture bytes changed';
|
|
214
|
+
} else integrityFailure = 'source capture bytes changed';
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
integrityFailure = `source capture identity could not be checked: ${error.message}`;
|
|
218
|
+
}
|
|
219
|
+
if (integrityFailure) {
|
|
220
|
+
const recovery = path.join(process.cwd(), `source-capture-recovery-${randomUUID()}.json`);
|
|
221
|
+
source.captureIntegrityViolation = { reason: integrityFailure, originalPath: captureFile, agentStatus: source.status };
|
|
222
|
+
source.status = 'failed';
|
|
223
|
+
const recoveryBytes = Buffer.from(`${JSON.stringify(source, null, 2)}\n`);
|
|
224
|
+
let recoveryFd = null;
|
|
225
|
+
let recoveryFailure = null;
|
|
226
|
+
try {
|
|
227
|
+
recoveryFd = fs.openSync(recovery, 'wx', 0o600);
|
|
228
|
+
fs.writeFileSync(recoveryFd, recoveryBytes);
|
|
229
|
+
fs.fsyncSync(recoveryFd);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
recoveryFailure = error;
|
|
232
|
+
} finally {
|
|
233
|
+
for (const fd of [recoveryFd, captureFd]) {
|
|
234
|
+
if (fd === null) continue;
|
|
235
|
+
try {
|
|
236
|
+
fs.closeSync(fd);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
recoveryFailure ??= error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
captureFd = null;
|
|
242
|
+
}
|
|
243
|
+
if (recoveryFailure) {
|
|
244
|
+
captureResult = {
|
|
245
|
+
status: 'unverified',
|
|
246
|
+
reason: integrityFailure,
|
|
247
|
+
recoveryError: recoveryFailure.message,
|
|
248
|
+
path: path.basename(recovery),
|
|
249
|
+
};
|
|
250
|
+
reportCapture();
|
|
251
|
+
fail(
|
|
252
|
+
'environment-configuration',
|
|
253
|
+
`${integrityFailure}; recovery receipt could not be preserved at ${recovery}: ${recoveryFailure.message}`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
captureResult = {
|
|
257
|
+
status: 'rejected',
|
|
258
|
+
path: path.basename(recovery),
|
|
259
|
+
sha256: `sha256:${createHash('sha256').update(recoveryBytes).digest('hex')}`,
|
|
260
|
+
};
|
|
261
|
+
reportCapture();
|
|
262
|
+
fail('environment-configuration', `${integrityFailure}; source evidence preserved at ${recovery}`);
|
|
263
|
+
}
|
|
264
|
+
if (options.designPath) {
|
|
265
|
+
try {
|
|
266
|
+
const text = designText(process.cwd(), options.designPath);
|
|
267
|
+
source.artifactSha256 = text === null ? null : `sha256:${createHash('sha256').update(text).digest('hex')}`;
|
|
268
|
+
} catch {
|
|
269
|
+
source.artifactSha256 = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
captureBytes = Buffer.from(`${JSON.stringify(source, null, 2)}\n`);
|
|
273
|
+
fs.ftruncateSync(captureFd, 0);
|
|
274
|
+
fs.writeSync(captureFd, captureBytes, 0, captureBytes.length, 0);
|
|
275
|
+
fs.fsyncSync(captureFd);
|
|
276
|
+
captureResult = {
|
|
277
|
+
status: 'verified',
|
|
278
|
+
path: path.basename(captureFile),
|
|
279
|
+
sha256: `sha256:${createHash('sha256').update(captureBytes).digest('hex')}`,
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
const captureDoubleTelemetry = (stderr) => {
|
|
283
|
+
if (!source || !options.captureTestDouble || options.agent !== 'custom') return;
|
|
284
|
+
const line = stderr.split('\n').find((line) => line.startsWith('TEA_EXPERIMENT_TEST_TELEMETRY:'));
|
|
285
|
+
if (line) {
|
|
286
|
+
try {
|
|
287
|
+
const telemetry = JSON.parse(line.slice('TEA_EXPERIMENT_TEST_TELEMETRY:'.length));
|
|
288
|
+
source.observed = telemetry.observed ?? source.observed;
|
|
289
|
+
source.usage = telemetry.usage ?? null;
|
|
290
|
+
source.cost = telemetry.cost ?? null;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
(source.telemetryErrors ??= []).push(`test-double telemetry unavailable: ${error.message}`);
|
|
293
|
+
source.observed = { resolvedModel: null, appliedEffort: null };
|
|
294
|
+
source.usage = null;
|
|
295
|
+
source.cost = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
if (options.capturePath) {
|
|
300
|
+
const relative = path.normalize(options.capturePath);
|
|
301
|
+
if (path.isAbsolute(relative) || relative === '..' || relative.startsWith(`..${path.sep}`))
|
|
302
|
+
fail('usage', '--capture-path must stay inside the workspace');
|
|
303
|
+
captureFile = path.join(process.cwd(), relative);
|
|
304
|
+
if (fs.existsSync(captureFile)) fail('usage', '--capture-path already exists');
|
|
305
|
+
let captureParent;
|
|
306
|
+
let workspaceRoot;
|
|
307
|
+
try {
|
|
308
|
+
captureParent = fs.realpathSync(path.dirname(captureFile));
|
|
309
|
+
workspaceRoot = fs.realpathSync(process.cwd());
|
|
310
|
+
} catch (error) {
|
|
311
|
+
fail('usage', `--capture-path parent cannot be resolved: ${error.message}`);
|
|
312
|
+
}
|
|
313
|
+
if (captureParent !== workspaceRoot) fail('usage', '--capture-path must be directly inside the workspace');
|
|
314
|
+
let requestedModel;
|
|
315
|
+
try {
|
|
316
|
+
requestedModel = options.captureTestDouble ? (options.model ?? null) : resolveModel(options.agent, options.model, options.agentArg);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
fail(classOfAgentError(error), error.message);
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
captureFd = fs.openSync(captureFile, 'wx+', 0o600);
|
|
322
|
+
captureIdentity = fs.fstatSync(captureFd);
|
|
323
|
+
source = {
|
|
324
|
+
kind: 'test-design-source-capture',
|
|
325
|
+
label: options.captureTestDouble ? 'deterministic-test-double' : 'native',
|
|
326
|
+
workspaceId: randomUUID(),
|
|
327
|
+
workspace: process.cwd(),
|
|
328
|
+
requested: {
|
|
329
|
+
agent: options.agent,
|
|
330
|
+
model: requestedModel,
|
|
331
|
+
},
|
|
332
|
+
inputSha256: `sha256:${createHash('sha256').update(prompt).digest('hex')}`,
|
|
333
|
+
startedAt: new Date().toISOString(),
|
|
334
|
+
endedAt: null,
|
|
335
|
+
status: 'dispatched',
|
|
336
|
+
observed: { resolvedModel: null, appliedEffort: null },
|
|
337
|
+
usage: null,
|
|
338
|
+
cost: null,
|
|
339
|
+
};
|
|
340
|
+
saveCapture();
|
|
341
|
+
} catch (error) {
|
|
342
|
+
let message = error.message;
|
|
343
|
+
if (captureFd !== null) {
|
|
344
|
+
try {
|
|
345
|
+
fs.closeSync(captureFd);
|
|
346
|
+
} catch (closeError) {
|
|
347
|
+
message += `; capture close failed: ${closeError.message}`;
|
|
348
|
+
}
|
|
349
|
+
captureFd = null;
|
|
350
|
+
}
|
|
351
|
+
fail('environment-configuration', `could not initialize source capture: ${message}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
171
354
|
try {
|
|
172
|
-
|
|
355
|
+
const answer = runAgent(prompt, {
|
|
173
356
|
agent: options.agent,
|
|
174
357
|
agentCommand: options.agentCmd,
|
|
175
358
|
agentArgs: options.agentArg,
|
|
176
359
|
envPass: options.envPass,
|
|
177
|
-
model: options.model,
|
|
360
|
+
model: options.captureTestDouble && options.agent === 'custom' ? undefined : options.model,
|
|
178
361
|
timeout,
|
|
179
362
|
cwd: process.cwd(),
|
|
180
363
|
capabilities: RUNNER_CAPABILITIES,
|
|
181
|
-
|
|
364
|
+
usageReport: Boolean(source) && options.agent !== 'custom',
|
|
365
|
+
captureTelemetry: Boolean(source) && options.agent !== 'custom',
|
|
366
|
+
});
|
|
367
|
+
stdout = answer.stdout;
|
|
368
|
+
if (source) {
|
|
369
|
+
source.observed = { resolvedModel: answer.telemetry?.resolvedModel ?? null, appliedEffort: answer.telemetry?.appliedEffort ?? null };
|
|
370
|
+
source.providerStdout = answer.sourceStdout ?? answer.stdout;
|
|
371
|
+
source.providerStderr = answer.sourceStderr ?? answer.stderr;
|
|
372
|
+
source.usage = answer.telemetry?.usage ?? null;
|
|
373
|
+
source.telemetryErrors = answer.telemetry?.errors ?? [];
|
|
374
|
+
const usd = source.usage?.costUsd;
|
|
375
|
+
if (usd !== undefined) source.cost = { currency: 'USD', components: { target: Number(usd), tools: null } };
|
|
376
|
+
captureDoubleTelemetry(answer.stderr);
|
|
377
|
+
source.status = 'completed';
|
|
378
|
+
source.processOutcome = { status: 'completed', exitCode: 0 };
|
|
379
|
+
source.endedAt = new Date().toISOString();
|
|
380
|
+
saveCapture();
|
|
381
|
+
}
|
|
182
382
|
} catch (error) {
|
|
383
|
+
if (source) {
|
|
384
|
+
source.providerStdout = error.stdout ?? '';
|
|
385
|
+
source.providerStderr = error.stderr ?? '';
|
|
386
|
+
captureDoubleTelemetry(error.stderr ?? '');
|
|
387
|
+
if (options.agent !== 'custom') {
|
|
388
|
+
const parsed = agentCaptureTelemetry(options.agent, error.stdout ?? '', error.stderr ?? '');
|
|
389
|
+
source.usage = parsed.usage;
|
|
390
|
+
source.observed.resolvedModel = parsed.resolvedModel;
|
|
391
|
+
source.telemetryErrors = parsed.errors;
|
|
392
|
+
source.providerStdout = error.stdout ?? '';
|
|
393
|
+
source.providerStderr = error.stderr ?? '';
|
|
394
|
+
if (parsed.usage?.costUsd !== undefined)
|
|
395
|
+
source.cost = { currency: 'USD', components: { target: Number(parsed.usage.costUsd), tools: null } };
|
|
396
|
+
}
|
|
397
|
+
source.status = /timed out/.test(error.message) ? 'timeout' : /signal SIG(?:INT|TERM)/.test(error.message) ? 'canceled' : 'failed';
|
|
398
|
+
source.processOutcome = { status: source.status, exitCode: null };
|
|
399
|
+
source.endedAt = new Date().toISOString();
|
|
400
|
+
source.error = { code: error.code ?? null, message: error.message };
|
|
401
|
+
source.stdout = error.stdout ?? '';
|
|
402
|
+
source.stderr = error.stderr ?? '';
|
|
403
|
+
saveCapture();
|
|
404
|
+
}
|
|
405
|
+
reportCapture();
|
|
183
406
|
fail(classOfAgentError(error), error.message);
|
|
184
407
|
}
|
|
185
|
-
|
|
186
408
|
if (options.designPath) {
|
|
187
409
|
try {
|
|
188
|
-
const projection = projectScoredRisks(process.cwd(), options.designPath);
|
|
189
|
-
if (projection)
|
|
410
|
+
const projection = projectScoredRisks(process.cwd(), options.designPath, { requireDeliverable: Boolean(source) });
|
|
411
|
+
if (projection)
|
|
412
|
+
process.stdout.write(`${JSON.stringify(captureResult ? { ...projection, sourceCapture: captureResult } : projection)}\n`);
|
|
413
|
+
else if (source)
|
|
414
|
+
throw Object.assign(new Error('the native design deliverable is absent'), { failureClass: 'environment-missing-artifact' });
|
|
190
415
|
} catch (error) {
|
|
416
|
+
if (source) {
|
|
417
|
+
source.agentStatus = source.status;
|
|
418
|
+
source.status = 'failed';
|
|
419
|
+
source.failureClass = error.failureClass ?? 'environment-parser';
|
|
420
|
+
source.error = { code: 'RUNNER_PROJECTION_FAILED', message: error.message };
|
|
421
|
+
saveCapture();
|
|
422
|
+
}
|
|
423
|
+
reportCapture();
|
|
191
424
|
fail('environment-parser', `could not project scored risks: ${error.message}`);
|
|
192
425
|
}
|
|
193
|
-
}
|
|
426
|
+
} else reportCapture();
|
|
427
|
+
if (captureFd !== null) fs.closeSync(captureFd);
|
|
194
428
|
|
|
195
429
|
if (stdout.length > 0) process.stderr.write(stdout.endsWith('\n') ? stdout : `${stdout}\n`);
|
|
196
430
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bmad-method-test-architecture-enterprise",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.3.0",
|
|
5
5
|
"description": "Master Test Architect for quality strategy, test automation, and release gates",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bmad",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"release:major": "gh workflow run publish.yaml -f bump=major",
|
|
96
96
|
"release:minor": "gh workflow run publish.yaml -f bump=minor",
|
|
97
97
|
"release:patch": "gh workflow run publish.yaml -f bump=patch",
|
|
98
|
-
"test": "npm run test:conflict-markers && npm run test:schemas && npm run test:install && npm run test:evaluate-guidance && npm run test:evaluate-authoring && npm run test:evaluate-gap-loop && npm run test:evaluate-dogfood && npm run test:evaluate-check && npm run test:evaluate-discovery && npm run test:evaluate-generator && npm run test:evaluate-candidate-review && npm run test:evaluate-compare && npm run test:evaluate-before-state && npm run test:evaluate-ci-gates && npm run test:evaluate-ci-tiers && npm run test:evaluate-ci-repositories:tagged-release-pr && npm run test:evaluate-ci-repositories:tagged-release-merge && npm run test:evaluate-ci-repositories:tagged-release-release && npm run test:evaluate-ci-repositories:nightly-deploy-pr && npm run test:evaluate-ci-repositories:nightly-deploy-merge && npm run test:evaluate-ci-repositories:nightly-deploy-scheduled && npm run test:evaluate-ci-repositories:nightly-deploy-release && npm run test:evaluate-ci-render && npm run test:evaluate-pr-mcp && npm run test:evaluate-pr-api && npm run test:evaluate-pr-suite && npm run test:evaluate-pr-workflow && npm run test:evaluate-pr-tool-use && npm run test:evaluate-pr-promptfoo && npm run test:evaluate-pr-ai-feature && npm run test:evaluate-pr-test-review && npm run test:evaluate-pr-gap-loop && npm run test:evaluate-pr-learn && npm run test:evaluate-pr-tutorial && npm run test:evaluate-partitions && npm run test:evaluate-partition-plans-views-and-check && npm run test:evaluate-partition-plans-preflight-and-run && npm run test:evaluate-partition-plans-score && npm run test:evaluate-partition-plans-attempts-seal-and-mappings && npm run test:evaluate-partition-plans-attempts-records && npm run test:evaluate-partition-plans-attempts-gameability && npm run test:evaluate-interpret && npm run test:evaluate-calibration && npm run test:evaluate-boundaries && npm run test:evaluate-preflight && npm run test:evaluate-isolation-conformance && npm run test:evaluate-mutation:qualification-basics && npm run test:evaluate-mutation:qualification-guards && npm run test:evaluate-mutation:recovery && npm run test:evaluate-mutation:journals && npm run test:evaluate-run && npm run test:evaluate-progress && npm run test:evaluate-aggregate && npm run test:evaluate-confinement && npm run test:evaluate-confinement-audit && npm run test:evaluate-confinement-history && npm run test:evaluate-confinement-git-state && npm run test:evaluate-confinement-network && npm run test:evaluate-held-inputs && npm run test:evaluate-arms-historical && npm run test:evaluate-arms-deployments && npm run test:evaluate-evaluators && npm run test:evaluate-evaluators-agent-version && npm run test:evaluate-agents && npm run test:evaluate-agents-importers && npm run test:evaluate-agents-sealed-brief && npm run test:evaluate-held-attempts && npm run test:evaluate-private && npm run test:evaluate-records && npm run test:evaluate-mcp && npm run test:evaluate-api && npm run test:evaluate-workflow && npm run test:evaluate-tool-use && npm run test:evaluate-promptfoo && npm run test:evaluate-learned-framework && npm run test:docs-tutorial && npm run test:knowledge && npm run test:criteria-fragments && npm run test:doc-count-sources && npm run test:planning-doc-sources && npm run test:doc-counts && npm run test:doc-claim-sources && npm run test:doc-claims && npm run test:doc-invocation-entry && npm run test:doc-invocations && npm run test:doc-link-anchors && npm run test:docs-build-names && npm run test:doc-shell-blocks && npm run test:doc-ci-samples && npm run test:doc-playwright-examples && npm run test:docs-evaluate-sidebar && npm run test:docs-general-claims && npm run test:docs-gap-reader && npm run test:enforce-hook && npm run test:framework-scaffold && npm run test:isolation-primitives && npm run test:atdd-isolation && npm run test:atdd-net-guard && npm run test:atdd-scoring && npm run test:atdd-workflow-guidance && npm run test:generation-healing && npm run test:framework-scaffold-install-isolation && npm run test:transcript-harness && npm run test:suite-manifest && npm run test:teach-me-testing-scoring && npm run test:teach-cli && npm run test:eval-data && npm run test:eval-atdd-data && npm run test:eval-ci-data && npm run test:eval-framework-scaffold-data && npm run test:eval-nfr-data && npm run test:nfr-validate-corpus && npm run test:nfr-evidence-guidance && npm run test:nfr-cli && npm run test:eval-test-design-data && npm run test:eval-trace-data && npm run test:eval-routing-data && npm run test:eval-routing-boundaries && npm run test:eval-routing-evidence && npm run test:eval-transcript-data && npm run test:eval-teach-me-testing-data && npm run test:eval-schemas && npm run test:eval-diagnostics && npm run test:eval-replay && npm run test:eval-quality-corpus && npm run test:schema-versions && npm run test:contract-sources && npm run test:contracts && npm run test:contract-oracles && npm run test:probe-sources && npm run test:test-design-qualification && npm run test:test-review-qualification && npm run test:trace-qualification && npm run test:nfr-qualification && npm run test:ci-qualification && npm run test:probe-corpus && npm run test:compare-dominance && npm run test:compare-eval-runs && npm run test:configuration-experiment && npm run test:trial-set-scoring && npm run test:automate-cli && npm run test:automate-unit-runners && npm run test:automate-generation-evidence && npm run test:automate-eval-fixture && npm run test:eval-automate-data && npm run test:clock-port && npm run test:probe-conformance && npm run test:corpus-conformance && npm run test:file-system-conformance && npm run test:file-system-port && npm run test:probe-targets && npm run test:port-totality && npm run test:ci-coverage && npm run test:ci-coverage-filters && npm run test:install-actionlint && npm run test:workflows-lint && npm run test:artifact-retry && npm run test:install-linux-sandbox && npm run test:shards && npm run test:groups && npm run test:cli && npm run test:workflow-cli && npm run test:migrate-customizations && npm run test:framework-cli && npm run test:test-review-pr-scope && npm run test:test-review-report && npm run test:test-review-rubric && npm run test:bmad-output-gated && npm run test:release-metadata && npm run test:release-notes && npm run test:lockfile-age && npm run test:lockfile-age-cache && npm run test:licences && npm run test:supply-chain && npm run test:boundary && npm run test:direction && npm run test:lineage && npm run test:guard-publish && npm run test:layering-boundary-lineage && npm run test:changelog && npm run test:tea-workflow-descriptions && npm run test:docs-mermaid && npm run test:llms-headroom && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
|
|
98
|
+
"test": "npm run test:conflict-markers && npm run test:schemas && npm run test:install && npm run test:evaluate-guidance && npm run test:evaluate-authoring && npm run test:evaluate-gap-loop && npm run test:evaluate-dogfood && npm run test:evaluate-check && npm run test:evaluate-discovery && npm run test:evaluate-generator && npm run test:evaluate-candidate-review && npm run test:evaluate-compare && npm run test:evaluate-before-state && npm run test:evaluate-ci-gates && npm run test:evaluate-ci-tiers && npm run test:evaluate-ci-repositories:tagged-release-pr && npm run test:evaluate-ci-repositories:tagged-release-merge && npm run test:evaluate-ci-repositories:tagged-release-release && npm run test:evaluate-ci-repositories:nightly-deploy-pr && npm run test:evaluate-ci-repositories:nightly-deploy-merge && npm run test:evaluate-ci-repositories:nightly-deploy-scheduled && npm run test:evaluate-ci-repositories:nightly-deploy-release && npm run test:evaluate-ci-render && npm run test:evaluate-pr-mcp && npm run test:evaluate-pr-api && npm run test:evaluate-pr-suite && npm run test:evaluate-pr-workflow && npm run test:evaluate-pr-tool-use && npm run test:evaluate-pr-promptfoo && npm run test:evaluate-pr-ai-feature && npm run test:evaluate-pr-test-review && npm run test:evaluate-pr-gap-loop && npm run test:evaluate-pr-learn && npm run test:evaluate-pr-tutorial && npm run test:evaluate-partitions && npm run test:evaluate-partition-plans-views-and-check && npm run test:evaluate-partition-plans-preflight-and-run && npm run test:evaluate-partition-plans-score && npm run test:evaluate-partition-plans-attempts-seal-and-mappings && npm run test:evaluate-partition-plans-attempts-records && npm run test:evaluate-partition-plans-attempts-gameability && npm run test:evaluate-interpret && npm run test:evaluate-calibration && npm run test:evaluate-boundaries && npm run test:evaluate-preflight && npm run test:evaluate-isolation-conformance && npm run test:evaluate-mutation:qualification-basics && npm run test:evaluate-mutation:qualification-guards && npm run test:evaluate-mutation:recovery && npm run test:evaluate-mutation:journals && npm run test:evaluate-run && npm run test:evaluate-progress && npm run test:evaluate-aggregate && npm run test:evaluate-confinement && npm run test:evaluate-confinement-audit && npm run test:evaluate-confinement-history && npm run test:evaluate-confinement-git-state && npm run test:evaluate-confinement-network && npm run test:evaluate-held-inputs && npm run test:evaluate-arms-historical && npm run test:evaluate-arms-deployments && npm run test:evaluate-evaluators && npm run test:evaluate-evaluators-agent-version && npm run test:evaluate-agents && npm run test:evaluate-agents-importers && npm run test:evaluate-agents-sealed-brief && npm run test:evaluate-held-attempts && npm run test:evaluate-private && npm run test:evaluate-records && npm run test:evaluate-mcp && npm run test:evaluate-api && npm run test:evaluate-workflow && npm run test:evaluate-tool-use && npm run test:evaluate-promptfoo && npm run test:evaluate-learned-framework && npm run test:docs-tutorial && npm run test:knowledge && npm run test:criteria-fragments && npm run test:doc-count-sources && npm run test:planning-doc-sources && npm run test:doc-counts && npm run test:doc-claim-sources && npm run test:doc-claims && npm run test:doc-invocation-entry && npm run test:doc-invocations && npm run test:doc-link-anchors && npm run test:docs-build-names && npm run test:doc-shell-blocks && npm run test:doc-ci-samples && npm run test:doc-playwright-examples && npm run test:docs-evaluate-sidebar && npm run test:docs-general-claims && npm run test:docs-gap-reader && npm run test:enforce-hook && npm run test:framework-scaffold && npm run test:isolation-primitives && npm run test:atdd-isolation && npm run test:atdd-net-guard && npm run test:atdd-scoring && npm run test:atdd-workflow-guidance && npm run test:generation-healing && npm run test:framework-scaffold-install-isolation && npm run test:transcript-harness && npm run test:suite-manifest && npm run test:teach-me-testing-scoring && npm run test:teach-cli && npm run test:eval-data && npm run test:eval-atdd-data && npm run test:eval-ci-data && npm run test:eval-framework-scaffold-data && npm run test:eval-nfr-data && npm run test:nfr-validate-corpus && npm run test:nfr-evidence-guidance && npm run test:nfr-cli && npm run test:eval-test-design-data && npm run test:eval-trace-data && npm run test:eval-routing-data && npm run test:eval-routing-boundaries && npm run test:eval-routing-evidence && npm run test:eval-transcript-data && npm run test:eval-teach-me-testing-data && npm run test:eval-schemas && npm run test:eval-diagnostics && npm run test:eval-replay && npm run test:eval-quality-corpus && npm run test:schema-versions && npm run test:contract-sources && npm run test:contracts && npm run test:contract-oracles && npm run test:probe-sources && npm run test:test-design-qualification && npm run test:test-review-qualification && npm run test:trace-qualification && npm run test:nfr-qualification && npm run test:ci-qualification && npm run test:probe-corpus && npm run test:compare-dominance && npm run test:compare-eval-runs && npm run test:configuration-experiment && npm run test:evaluate-experiments && npm run test:trial-set-scoring && npm run test:automate-cli && npm run test:automate-unit-runners && npm run test:automate-generation-evidence && npm run test:automate-eval-fixture && npm run test:eval-automate-data && npm run test:clock-port && npm run test:probe-conformance && npm run test:corpus-conformance && npm run test:file-system-conformance && npm run test:file-system-port && npm run test:probe-targets && npm run test:port-totality && npm run test:ci-coverage && npm run test:ci-coverage-filters && npm run test:install-actionlint && npm run test:workflows-lint && npm run test:artifact-retry && npm run test:install-linux-sandbox && npm run test:shards && npm run test:groups && npm run test:cli && npm run test:workflow-cli && npm run test:migrate-customizations && npm run test:framework-cli && npm run test:test-review-pr-scope && npm run test:test-review-report && npm run test:test-review-rubric && npm run test:bmad-output-gated && npm run test:release-metadata && npm run test:release-notes && npm run test:lockfile-age && npm run test:lockfile-age-cache && npm run test:licences && npm run test:supply-chain && npm run test:boundary && npm run test:direction && npm run test:lineage && npm run test:guard-publish && npm run test:layering-boundary-lineage && npm run test:changelog && npm run test:tea-workflow-descriptions && npm run test:docs-mermaid && npm run test:llms-headroom && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
|
|
99
99
|
"test:artifact-retry": "node test/test-artifact-retry.js",
|
|
100
100
|
"test:atdd-isolation": "node test/test-atdd-isolation.js",
|
|
101
101
|
"test:atdd-net-guard": "node test/test-atdd-net-guard.js",
|
|
@@ -193,6 +193,7 @@
|
|
|
193
193
|
"test:evaluate-dogfood": "node test/test-evaluate-dogfood.js",
|
|
194
194
|
"test:evaluate-evaluators": "node test/test-evaluate-evaluators.js --group=evaluators",
|
|
195
195
|
"test:evaluate-evaluators-agent-version": "node test/test-evaluate-evaluators.js --group=evaluators-agent-version",
|
|
196
|
+
"test:evaluate-experiments": "node test/test-evaluate-experiments.js",
|
|
196
197
|
"test:evaluate-gap-loop": "node test/test-evaluate-gap-loop.js",
|
|
197
198
|
"test:evaluate-generator": "node test/test-evaluate-generator.js",
|
|
198
199
|
"test:evaluate-guidance": "node test/test-evaluate-guidance.js",
|
|
@@ -21,6 +21,17 @@ You will continue to operate with your given name, identity, and communication_s
|
|
|
21
21
|
- `{skill-name}` resolves to the skill directory's basename.
|
|
22
22
|
- Resolve sibling files such as `references/...` and `assets/...` from `{skill-root}`.
|
|
23
23
|
|
|
24
|
+
## Execution Authority
|
|
25
|
+
|
|
26
|
+
Resolve intent before activation, customization resolution or any hook. Ordinary evaluation keeps its current workflow unchanged. Native clean/mutated trials, run comparison and baseline acceptance retain their existing authority and meaning.
|
|
27
|
+
|
|
28
|
+
An explicit native compare/accept request keeps that command's authority, including a request that mentions consistency while comparing saved runs. Saved-result consistency is an experiment route only when the user asks to analyze variability in existing results. Resolve contextual words such as “these results” from the conversation before choosing a route.
|
|
29
|
+
|
|
30
|
+
- Saved-result consistency or explicitly requested offline experiment analysis: load `references/configuration-experiments.md` and take its offline route directly. Suppress prepend, append and completion hooks. Do not run configuration resolver scripts, model calls, judge calls or fresh trials. Return after reporting the saved evidence.
|
|
31
|
+
- Fresh repeatability or a one-factor configuration experiment: load `references/configuration-experiments.md` directly. Suppress prepend, append and completion hooks throughout this route. No hook can grant execution or spending authority.
|
|
32
|
+
- “Is it repeatable?” without a saved/fresh choice: clarify saved records versus fresh runs before executing anything.
|
|
33
|
+
- Ordinary evaluation, ordinary offline record evaluation or native compare/accept (including comparing saved runs): proceed with On Activation below. Do not load the experiment guide, analyze experiments or add trials after the normal result.
|
|
34
|
+
|
|
24
35
|
## On Activation
|
|
25
36
|
|
|
26
37
|
Before resolving customizations or running hooks, inspect `{project-root}/_bmad/custom/` for `bmad-testarch-evaluate.toml` and `bmad-testarch-evaluate.user.toml`. If either exists, stop and tell the user to run `npx --package=bmad-method-test-architecture-enterprise@3 tea-migrate-customizations --project-root {project-root}`. Resolve any reported manual merge, then start a fresh chat. Do not continue with default settings while legacy overrides remain.
|