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.
@@ -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 projectScoredRisks(cwd, relativeDesignPath) {
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
- const text = fs.readFileSync(designPath, 'utf8');
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
- ({ stdout } = runAgent(prompt, {
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) process.stdout.write(`${JSON.stringify(projection)}\n`);
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.2.0",
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.