bmad-method-test-architecture-enterprise 2.0.4-next.1 → 2.0.4-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.
@@ -31,7 +31,7 @@
31
31
  "name": "bmad-method-test-architecture-enterprise",
32
32
  "source": "./",
33
33
  "description": "Master Test Architect module for quality strategy, test generation in red and expand modes with automatic run-and-heal, combined framework and CI setup, CI/CD quality gates, and structured testing education. The CI and ATDD commands remain available with existing customizations. Part of the BMad Method ecosystem.",
34
- "version": "2.0.4-next.1",
34
+ "version": "2.0.4-next.3",
35
35
  "author": {
36
36
  "name": "Murat K Ozcan (TEA Creator) & Brian (BMad) Madison"
37
37
  },
package/README.md CHANGED
@@ -114,6 +114,7 @@ npm install --save-dev bmad-method-test-architecture-enterprise
114
114
  ```
115
115
 
116
116
  - [`tea-test-review`](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/reference/tea-test-review-cli/) runs the review skill against changed tests and returns a gate verdict.
117
+ - [`tea-framework`](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/reference/tea-framework-cli/) creates, resumes, validates, and edits test framework and CI setup with retained run evidence.
117
118
  - [`tea-trace`](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/reference/tea-trace-cli/) maps requirements to existing evidence and returns a coverage gate.
118
119
  - [`tea-evaluate`](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/reference/tea-evaluate-cli/) checks, runs, scores, and compares behavioral evaluations.
119
120
 
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ /** tea-framework drives the packaged framework and CI skill with durable run evidence. */
3
+ 'use strict';
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const { Command } = require('commander');
8
+ const { resolveModel } = require('./lib/agent-adapters');
9
+ const { agentOptions, WorkflowError } = require('./lib/workflow-cli');
10
+ const { resolveTeaConfig } = require('./lib/resolve-tea-config');
11
+ const { addAgentOptions, resolveWorkflowSkill, runWithEvidence } = require('./lib/workflow-cli');
12
+ const {
13
+ SKILL_NAME,
14
+ SCOPES,
15
+ OPERATIONS,
16
+ projectPath,
17
+ readJournal,
18
+ prepareSetup,
19
+ setupPrompt,
20
+ inspectCompletion,
21
+ protectResultPath,
22
+ saveRequestSnapshot,
23
+ } = require('./lib/framework-setup');
24
+
25
+ function collect(value, previous) {
26
+ return [...previous, value];
27
+ }
28
+
29
+ function resultReport(result) {
30
+ return [
31
+ '# Framework and CI setup result',
32
+ '',
33
+ `Operation: ${result.operation}. Scope: ${result.scope}.`,
34
+ `Completed: ${result.completed ? 'yes' : 'no'}.`,
35
+ `Agent: ${result.agent}. Model: ${result.model ?? 'unspecified'}.`,
36
+ '',
37
+ '## Completion evidence',
38
+ '',
39
+ ...result.artifacts.map((file) => `- ${file}`),
40
+ '',
41
+ ...(result.issues.length > 0 ? ['## Unresolved work', '', ...result.issues.map((issue) => `- ${issue}`), ''] : []),
42
+ `Retained agent evidence: ${result.runDirectory}.`,
43
+ '',
44
+ 'Create completion verifies the skill journal and referenced outputs, then reruns the frozen test commands. Native verification logs and agent transcripts are retained. Validate findings remain report outcomes.',
45
+ '',
46
+ ].join('\n');
47
+ }
48
+
49
+ function main(argv = process.argv) {
50
+ const program = new Command();
51
+ program
52
+ .name('tea-framework')
53
+ .description('Create, resume, validate or edit test framework and CI setup using the packaged TEA skill.')
54
+ .option('--project-root <dir>', 'project whose framework and CI are being set up', process.cwd())
55
+ .option('--scope <scope>', `requested phases (${SCOPES.join('|')})`, 'framework')
56
+ .option('--operation <operation>', `requested operation (${OPERATIONS.join('|')})`, 'create')
57
+ .option('--input <path>', 'exact project artifact to edit or validate (repeatable)', collect, [])
58
+ .option('--instructions <path>', 'project file containing the setup request or requested edit')
59
+ .option('--skill-root <dir>', 'explicit skill copy; defaults to the packaged framework skill')
60
+ .option('--output <dir>', 'project directory for retained CLI reports and agent evidence')
61
+ .option('--json <path>', 'additional project-relative machine-readable result path')
62
+ .option('--allow-network', 'allow unrestricted outbound network access in the Codex workspace sandbox')
63
+ .option('--use-playwright-utils', 'enable Playwright utilities when applicable')
64
+ .option('--no-use-playwright-utils', 'disable Playwright utilities')
65
+ .option('--use-pactjs-utils', 'enable Pact utilities when applicable')
66
+ .option('--no-use-pactjs-utils', 'disable Pact utilities')
67
+ .exitOverride()
68
+ .configureOutput({ writeErr: () => {} });
69
+ addAgentOptions(program);
70
+ program.setOptionValueWithSource('retries', '0', 'default');
71
+ try {
72
+ program.parse(argv);
73
+ } catch (error) {
74
+ if (error.exitCode === 0) return 0;
75
+ process.stderr.write(`tea-framework: ${error.message}\n`);
76
+ return 2;
77
+ }
78
+ const options = program.opts();
79
+ let request;
80
+ let jsonPath;
81
+ try {
82
+ if (options.allowNetwork && options.agent !== 'codex') throw new Error('--allow-network requires --agent codex');
83
+ if (options.allowNetwork) options.agentArg = ['-c', 'sandbox_workspace_write.network_access=true', ...options.agentArg];
84
+ const projectRoot = fs.realpathSync(path.resolve(options.projectRoot));
85
+ agentOptions(options, projectRoot);
86
+ if (!fs.statSync(projectRoot).isDirectory()) throw new Error('--project-root must be a directory');
87
+ const skillRoot = resolveWorkflowSkill({ projectRoot, skillName: SKILL_NAME, skillRoot: options.skillRoot });
88
+ const config = resolveTeaConfig({ projectRoot, skillRoot, skillName: SKILL_NAME, flags: options });
89
+ request = prepareSetup({ projectRoot, skillRoot, config, options });
90
+ const evidenceRoot = projectPath(projectRoot, options.output ?? path.join(request.artifactsRoot, 'framework', 'cli-runs'));
91
+ jsonPath = options.json ? projectPath(projectRoot, options.json) : null;
92
+ if (
93
+ jsonPath &&
94
+ (path.extname(jsonPath) !== '.json' ||
95
+ jsonPath === request.journalPath ||
96
+ request.inputs.includes(jsonPath) ||
97
+ jsonPath === request.instructionsPath)
98
+ ) {
99
+ throw new Error('--json must name a .json result file separate from setup inputs and journal');
100
+ }
101
+ protectResultPath(request, jsonPath);
102
+ if (options.agent === 'none') {
103
+ process.stdout.write(`${setupPrompt(request)}\n`);
104
+ return 0;
105
+ }
106
+ const execution = runWithEvidence({
107
+ name: 'tea-framework',
108
+ projectRoot,
109
+ evidenceRoot,
110
+ options,
111
+ capabilities: ['command-execution'],
112
+ prepare: ({ attempt }) => ({ prompt: setupPrompt(request, { retry: attempt > 1 }), request }),
113
+ validate: ({ attemptDir }) => {
114
+ saveRequestSnapshot(request);
115
+ try {
116
+ protectResultPath(request, jsonPath, readJournal(projectPath(projectRoot, request.journalPath))?.data);
117
+ } catch (error) {
118
+ jsonPath = null;
119
+ throw new WorkflowError('usage', error.message, { cause: error });
120
+ }
121
+ return inspectCompletion(request, { evidenceDir: attemptDir, timeoutMs: Number(options.timeoutMs) });
122
+ },
123
+ });
124
+ const outcome = execution.value;
125
+ const result = {
126
+ schema_version: '0.1.0',
127
+ completed: outcome.completed,
128
+ scope: request.scope,
129
+ operation: request.savedOperation,
130
+ agent: options.agent,
131
+ model: resolveModel(options.agent, options.model, options.agentArg),
132
+ runDirectory: execution.runDir,
133
+ journal: request.journalPath,
134
+ artifacts: outcome.artifacts,
135
+ issues: outcome.issues,
136
+ verification: outcome.executions,
137
+ };
138
+ const serialized = `${JSON.stringify(result, null, 2)}\n`;
139
+ fs.writeFileSync(path.join(execution.runDir, 'result.json'), serialized);
140
+ fs.writeFileSync(path.join(execution.runDir, 'report.md'), resultReport(result));
141
+ if (jsonPath) {
142
+ protectResultPath(request, jsonPath, readJournal(projectPath(projectRoot, request.journalPath))?.data);
143
+ fs.mkdirSync(path.dirname(jsonPath), { recursive: true });
144
+ fs.writeFileSync(jsonPath, serialized);
145
+ }
146
+ process.stdout.write(serialized);
147
+ return result.completed ? 0 : 1;
148
+ } catch (error) {
149
+ if (request) {
150
+ try {
151
+ saveRequestSnapshot(request);
152
+ } catch (snapshotError) {
153
+ process.stderr.write(`tea-framework: could not retain setup request: ${snapshotError.message}\n`);
154
+ }
155
+ }
156
+ if (error.runDir) {
157
+ const failureResult = {
158
+ schema_version: '0.1.0',
159
+ completed: false,
160
+ scope: request?.scope ?? options.scope,
161
+ operation: request?.savedOperation ?? options.operation,
162
+ agent: options.agent,
163
+ model: null,
164
+ runDirectory: error.runDir,
165
+ journal: request?.journalPath ?? null,
166
+ artifacts: [],
167
+ issues: [error.message],
168
+ failureClass: error.failureClass,
169
+ };
170
+ const serialized = `${JSON.stringify(failureResult, null, 2)}\n`;
171
+ fs.writeFileSync(path.join(error.runDir, 'result.json'), serialized);
172
+ fs.writeFileSync(path.join(error.runDir, 'report.md'), resultReport(failureResult));
173
+ if (jsonPath) {
174
+ try {
175
+ protectResultPath(request, jsonPath, readJournal(projectPath(request.projectRoot, request.journalPath))?.data);
176
+ fs.mkdirSync(path.dirname(jsonPath), { recursive: true });
177
+ fs.writeFileSync(jsonPath, serialized);
178
+ } catch (publicationError) {
179
+ process.stderr.write(`tea-framework: result publication refused: ${publicationError.message}\n`);
180
+ }
181
+ }
182
+ process.stdout.write(serialized);
183
+ }
184
+ process.stderr.write(`tea-framework: ${error.message}${error.runDir ? `\nRetained evidence: ${error.runDir}` : ''}\n`);
185
+ return ['environment-transport', 'environment-timeout', 'environment-parser'].includes(error.failureClass) ? 3 : 2;
186
+ }
187
+ }
188
+
189
+ if (require.main === module) process.exitCode = main();
190
+ module.exports = { main, resultReport };
@@ -699,19 +699,38 @@ async function deploymentAccess({
699
699
  return { origins, authorizations };
700
700
  }
701
701
 
702
- /** A port the system gives out now, free on every address, released for the server to take. */
703
- function freePort() {
702
+ /** Tries a port on one address, then releases it for the target; a busy candidate returns null. */
703
+ function availablePort(port, address, createServer) {
704
704
  return new Promise((resolve, reject) => {
705
- const server = net.createServer();
705
+ const server = createServer();
706
706
  server.unref();
707
- server.once('error', reject);
708
- server.listen(0, () => {
709
- const { port } = server.address();
710
- server.close(() => resolve(port));
707
+ server.once('error', (error) => (error.code === 'EADDRINUSE' ? resolve(null) : reject(error)));
708
+ server.listen(port, address, () => {
709
+ const selected = server.address().port;
710
+ server.close(() => resolve(selected));
711
711
  });
712
712
  });
713
713
  }
714
714
 
715
+ /** A port available on every address the policy may select, released for the server to take. */
716
+ async function freePort(addresses, createServer = () => net.createServer()) {
717
+ const distinct = [...new Set(addresses.map((address) => canonicalAddress(address) ?? address))];
718
+ if (distinct.length === 0) throw new Error('a started server has no authorized address');
719
+ for (let attempt = 0; attempt < 32; attempt += 1) {
720
+ const candidate = await availablePort(0, distinct[0], createServer);
721
+ if (candidate === null) continue;
722
+ let available = true;
723
+ for (const address of distinct.slice(1)) {
724
+ if ((await availablePort(candidate, address, createServer)) === null) {
725
+ available = false;
726
+ break;
727
+ }
728
+ }
729
+ if (available) return candidate;
730
+ }
731
+ throw new Error(`could not find a free port on ${distinct.join(', ')}`);
732
+ }
733
+
715
734
  /**
716
735
  * Whether `address:port` accepts a connection now: `true`, or why the attempt
717
736
  * failed (the socket error's code, `ECONNREFUSED` while nothing listens, or
@@ -1207,7 +1226,7 @@ function createApiPort({ entries, httpPort, cwd, targetOf, readEnvironment, mech
1207
1226
  // A server behind a bridge gets a private directory for the Unix socket its shim serves, which only that call's
1208
1227
  // server may write; it joins the scratch list like the port directory.
1209
1228
  if (launched !== null && mechanism.bridges === true) bridgeDirectory = makeBridgeDirectory(scratch);
1210
- const launchedPort = launched === null || reports ? null : await freePort();
1229
+ const launchedPort = launched === null || reports ? null : await freePort(launched.addresses);
1211
1230
  const configurationAt = (port) =>
1212
1231
  portConfiguration({
1213
1232
  entries,
@@ -1397,6 +1416,7 @@ module.exports = {
1397
1416
  defaultPortOf,
1398
1417
  deploymentAccess,
1399
1418
  deploymentAuthorizations,
1419
+ freePort,
1400
1420
  httpPortFile,
1401
1421
  isApiEntry,
1402
1422
  makeBridgeDirectory,