bmad-method-test-architecture-enterprise 2.0.3-next.0 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/cli/lib/evaluate/confinement-audit.js +2 -2
- package/cli/lib/evaluate/confinement.js +16 -25
- package/cli/lib/evaluate/isolation-allowlist.js +6 -4
- package/cli/lib/evaluate/preflight.js +3 -2
- package/cli/lib/evaluate/run.js +4 -2
- package/package.json +1 -1
- package/skills/bmad-testarch-evaluate/references/authoring-routing.md +3 -1
- package/skills/bmod-tea/bmod.toml +1 -1
|
@@ -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.3
|
|
34
|
+
"version": "2.0.3",
|
|
35
35
|
"author": {
|
|
36
36
|
"name": "Murat K Ozcan (TEA Creator) & Brian (BMad) Madison"
|
|
37
37
|
},
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* runtime reads. `confinement.js` holds the boundary and decides what each
|
|
7
7
|
* sandbox grants; this module only observes.
|
|
8
8
|
*
|
|
9
|
-
* Seatbelt the target's profile
|
|
10
|
-
*
|
|
9
|
+
* Seatbelt the target's profile refuses ungranted `file-read-data` and
|
|
10
|
+
* reports those refusals, tagging each file rule it
|
|
11
11
|
* holds with a random token of the sandbox (`with message`).
|
|
12
12
|
* The kernel logs a report as `Sandbox: <name>(<pid>) allow|
|
|
13
13
|
* deny(1) <operation> <real path>` with the token on the next
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
* as the isolation manifest's `observedMounts`, which
|
|
148
148
|
* eval-quality holds against the allowed mounts and records as
|
|
149
149
|
* an isolation violation (Story 1.60). Seatbelt reports each
|
|
150
|
-
* read its profile
|
|
150
|
+
* ungranted read its profile refuses and
|
|
151
151
|
* tags every file rule with a token of the sandbox (`with
|
|
152
152
|
* message`), and a `/usr/bin/log stream` child the runtime owns
|
|
153
153
|
* writes the kernel's reports of that token to a file beneath
|
|
@@ -463,15 +463,17 @@ function seatbeltLayerProfile(evaluationFolder, gitDirectory = null, hooksDirect
|
|
|
463
463
|
* A `connect()` to a path-based unix socket is refused anywhere but the workspace, the private directories, the home and the two system services in `SEATBELT_SYSTEM_SOCKETS` (Story 1.87).
|
|
464
464
|
* The rule names a path, so it holds for a socket bound after the call started.
|
|
465
465
|
*
|
|
466
|
-
*
|
|
466
|
+
* Read grants restrict file data to the workspace, system roots and explicitly declared dependencies.
|
|
467
|
+
* With `audit` (`{ token }`), the kernel reports refusals with the sandbox token.
|
|
467
468
|
* Every file rule that denies carries the sandbox's token (`with message`).
|
|
468
|
-
*
|
|
469
|
+
* Every ungranted `file-read-data` is denied even when its diagnostic is lost by the unified log.
|
|
469
470
|
* That rule is placed before every rule that follows it, so a later grant (the home, a call directory, the git directory's own entry) overrides it.
|
|
470
|
-
*
|
|
471
|
+
* The same read boundary applies when a qualification port carries no observer.
|
|
471
472
|
*/
|
|
472
473
|
function seatbeltTargetProfile({
|
|
473
474
|
workspace,
|
|
474
475
|
writable,
|
|
476
|
+
readable = [],
|
|
475
477
|
evaluationFolder,
|
|
476
478
|
git = null,
|
|
477
479
|
privateRoot = null,
|
|
@@ -485,11 +487,8 @@ function seatbeltTargetProfile({
|
|
|
485
487
|
const socketRules = seatbeltSocketRules(allowed);
|
|
486
488
|
const socketRoutes = (candidates) => candidates.map((entry) => `(remote unix-socket ${entry})`).join('\n ');
|
|
487
489
|
const tagged = audit === null ? '' : ` (with message "${audit.token}")`;
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
// therefore names `file-read-data` in each rule that governs reads, which keeps the textual order (the last matching rule
|
|
491
|
-
// wins) the unaudited profile relies on.
|
|
492
|
-
const reads = audit === null ? 'file-read*' : 'file-read-data file-read*';
|
|
490
|
+
// Specific data-read grants outrank wildcard rules in Seatbelt. Every protected denial and re-grant must name the data operation.
|
|
491
|
+
const reads = 'file-read-data file-read*';
|
|
493
492
|
const subpaths = (candidate) => spellings(candidate).map((entry) => `(subpath "${assertProfileSafePath(entry, refuseUnsafePath)}")`);
|
|
494
493
|
// A denial of reads and writes of `paths`; an audited profile names `file-read-data` and tags it (see `reads`).
|
|
495
494
|
const denials = (paths) => [`(deny ${reads} file-write*\n ${paths.join('\n ')}${tagged})`];
|
|
@@ -542,18 +541,12 @@ function seatbeltTargetProfile({
|
|
|
542
541
|
`(allow file-read-metadata\n ${reachableAncestors.flatMap(literals).join('\n ')})`,
|
|
543
542
|
`(allow network-outbound\n ${socketRoutes(reachable.flatMap(subpaths))})`,
|
|
544
543
|
];
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
...EXACT_GRANTS.map((entry) => `(require-not (literal "${entry}"))`),
|
|
552
|
-
...audit.exempt
|
|
553
|
-
.flatMap(spellings)
|
|
554
|
-
.map((entry) => `(require-not (subpath "${assertProfileSafePath(entry, refuseUnsafePath)}"))`),
|
|
555
|
-
].join('\n ')})\n (with report)${tagged})`,
|
|
556
|
-
];
|
|
544
|
+
// Kernel enforcement is independent of the lossy log: ungranted data reads are refused even if their diagnostic is dropped.
|
|
545
|
+
// Keep the specific operation in later denials/re-grants so Seatbelt's wildcard precedence cannot reopen protected paths.
|
|
546
|
+
const reportRule = [
|
|
547
|
+
`(deny file-read-data${tagged})`,
|
|
548
|
+
`(allow file-read-data\n ${[...EXACT_GRANTS.map((entry) => `(literal "${entry}")`), ...readable.flatMap(subpaths)].join('\n ')})`,
|
|
549
|
+
];
|
|
557
550
|
// A target's git tries to write the worktree's own entry and the private repository (the index lock of a `git status`), which
|
|
558
551
|
// the profile refuses and the target's git expects to fail; the refusal carries no token, so it is not an observed mount.
|
|
559
552
|
const quiet =
|
|
@@ -1457,15 +1450,13 @@ function targetSandbox({
|
|
|
1457
1450
|
const profile = seatbeltTargetProfile({
|
|
1458
1451
|
workspace,
|
|
1459
1452
|
writable: grants,
|
|
1453
|
+
readable: readRoots([...grants, ...readable]),
|
|
1460
1454
|
evaluationFolder,
|
|
1461
1455
|
git,
|
|
1462
1456
|
privateRoot,
|
|
1463
1457
|
rootHome,
|
|
1464
1458
|
rootGrants: callDirectories,
|
|
1465
|
-
audit:
|
|
1466
|
-
observer === null
|
|
1467
|
-
? null
|
|
1468
|
-
: { token: observer.token, exempt: readRoots([...grants, ...readable]), quiet: [...ownGitEntries(), ...linked] },
|
|
1459
|
+
audit: observer === null ? null : { token: observer.token, quiet: [...ownGitEntries(), ...linked] },
|
|
1469
1460
|
});
|
|
1470
1461
|
// `env` runs outside the sandbox and after the watchdog hop that starts every target, which is where Node adds the host's `NODE_V8_COVERAGE` to the environment; the sandbox then starts the target without it, as Bubblewrap's `--unsetenv` does.
|
|
1471
1462
|
return {
|
|
@@ -115,7 +115,7 @@ function chanceMountNotes(legs, folder) {
|
|
|
115
115
|
* @param {object} options
|
|
116
116
|
* @param {string[]} options.mounts the offending paths, as the audit reported them
|
|
117
117
|
* @param {string} options.folder the evaluation folder, for the neutral form of each path
|
|
118
|
-
* @param {'legs'|'trials'} options.who
|
|
118
|
+
* @param {'legs'|'denied-legs'|'trials'} options.who
|
|
119
119
|
* @returns {string|null}
|
|
120
120
|
*/
|
|
121
121
|
function mountRefusal({ mounts, folder, who }) {
|
|
@@ -124,9 +124,11 @@ function mountRefusal({ mounts, folder, who }) {
|
|
|
124
124
|
const named = mounts.slice(0, NAMED_PATHS).map((mount) => `mount outside allowlist: ${neutral(mount)}`);
|
|
125
125
|
const list = `${named.join('; ')}${mounts.length > named.length ? `; and ${mounts.length - named.length} more` : ''}`;
|
|
126
126
|
const finding =
|
|
127
|
-
who === 'legs'
|
|
128
|
-
? `isolation manifest violation: every preflight leg
|
|
129
|
-
:
|
|
127
|
+
who === 'denied-legs'
|
|
128
|
+
? `isolation manifest violation: every preflight leg attempted access to ${mounts.length} path(s) outside the allowlist (${list}); the kernel refused those accesses (exit 3).`
|
|
129
|
+
: who === 'legs'
|
|
130
|
+
? `isolation manifest violation: every preflight leg opened ${mounts.length} path(s) outside the allowlist (${list}), so every trial will too, and \`score\` refuses a trial that does (exit 3).`
|
|
131
|
+
: `isolation manifest violation: the trials opened ${mounts.length} path(s) outside the allowlist, so \`score\` would exit 3 (${list}).`;
|
|
130
132
|
return [
|
|
131
133
|
finding,
|
|
132
134
|
'If they are the files of a target that launches from outside its workspace, use one of two setups: make the registry `target` a path inside `launch.root` (for example `node_modules/.bin/tea-skill-runner` over a copy workspace, or a git workspace with `workspace.provision`), or keep the bare name and list the directories it runs from in `systemPaths`, with the bin directory that holds its link on `PATH`.',
|
|
@@ -1361,11 +1361,12 @@ async function runInWorkspaces({
|
|
|
1361
1361
|
// A passed `preflight` is refused when every leg of the pristine workspace opened a path outside the allowlist: a target that lives
|
|
1362
1362
|
// outside its workspace loads its own files on every launch, so every trial opens them too, and `score` refuses a trial that does
|
|
1363
1363
|
// (exit 3). `run` judges the trials' own manifests, the set `score` judges, so it does not refuse on the legs.
|
|
1364
|
-
|
|
1364
|
+
// Seatbelt can refuse an undeclared runner before it produces a passing leg. Preserve the captured denial's path and setup guidance.
|
|
1365
|
+
if (afterVerdict === null && (verdict.exitCode === 0 || confinement.mode === 'seatbelt')) {
|
|
1365
1366
|
const refusal = mountRefusal({
|
|
1366
1367
|
mounts: mountsOfEveryLeg(legMounts, [`${pristine.kind} pristine`]),
|
|
1367
1368
|
folder,
|
|
1368
|
-
who: 'legs',
|
|
1369
|
+
who: confinement.mode === 'seatbelt' ? 'denied-legs' : 'legs',
|
|
1369
1370
|
});
|
|
1370
1371
|
if (refusal !== null) return outcome({ stage: 'leg', exitCode: 3, message: refusal });
|
|
1371
1372
|
}
|
package/cli/lib/evaluate/run.js
CHANGED
|
@@ -1468,7 +1468,9 @@ async function runTrialSets(given) {
|
|
|
1468
1468
|
for (const arm of arms) {
|
|
1469
1469
|
arm.trials = [];
|
|
1470
1470
|
for (let trialIndex = 1; trialIndex <= trialCount; trialIndex += 1) {
|
|
1471
|
-
|
|
1471
|
+
const trialDetail = `${arm.conditionArm}: trial ${trialIndex} of ${trialCount}`;
|
|
1472
|
+
log(trialDetail);
|
|
1473
|
+
progress('trials', { detail: trialDetail, completedTrials });
|
|
1472
1474
|
const trial = await runTrial({ ...context, arm, trialIndex });
|
|
1473
1475
|
const versionReadBegan = Date.now();
|
|
1474
1476
|
const agentMoved = await agentVersionChange(context);
|
|
@@ -1480,7 +1482,7 @@ async function runTrialSets(given) {
|
|
|
1480
1482
|
missingReports += trial.unreportedSteps.length;
|
|
1481
1483
|
completedTrials += 1;
|
|
1482
1484
|
progress('trials', {
|
|
1483
|
-
detail: `${
|
|
1485
|
+
detail: `${trialDetail} completed`,
|
|
1484
1486
|
completedTrials,
|
|
1485
1487
|
usage: {
|
|
1486
1488
|
trialCostUsd: missingReports === 0 && completedTrials === totalTrials ? reportedUse.costUsd : null,
|
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": "2.0.3
|
|
4
|
+
"version": "2.0.3",
|
|
5
5
|
"description": "Master Test Architect for quality strategy, test automation, and release gates",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bmad",
|
|
@@ -16,13 +16,15 @@ When the adopter asks for the latest target model or model tier, resolve it from
|
|
|
16
16
|
|
|
17
17
|
Use at most two read-only workers at once. Give each a disjoint source scope, the confirmed requirements statement, a concrete question, a time bound, and this output shape: `candidate`, `source path and line`, `uncertainty`, and `files inspected`. Good tasks are entry-point inventory, failure-history search, candidate boundary cases, candidate mutation locations, and summaries of public evaluator documentation. Workers may draft a probe or oracle fragment as text, but the parent decides whether it enters the corpus or contract.
|
|
18
18
|
|
|
19
|
+
When the host supports an isolated worker context, start each task without inheriting the parent conversation. Supply a short task packet with the confirmed requirement, exact source paths, the question, and the output shape above. For a later task, start a fresh bounded worker with a new packet; a long worker history can cost more than the research itself. If the host requires inherited context, keep the worker task narrow and record that constraint. Verify source claims and run the same authoring gates regardless of the context mode.
|
|
20
|
+
|
|
19
21
|
Keep one writer: the parent. Instruct workers to avoid editing the repository, evaluation folder, policy, or run artifacts; launching targets; running `tea-evaluate` or `eval-quality`; or contacting other workers. Some hosts give subagents writable tools even for a read-only request. Pause parent writes during a worker group. Compare the scoped source and evaluation-file inventory and digests before dispatch and after every worker returns. If files changed, reject the affected drafts, identify the writer, and repair worker-attributed edits before continuing. Never send workers secrets, credentials, private user data, `corpus/held-out/`, a held-out plan, held-out results, or a complete held-out request. When a task cannot be split cleanly, keep it in the parent session.
|
|
20
22
|
|
|
21
23
|
The parent checks every file and line claim before adopting it. Draft probes still pass `tea-evaluate check`, `eval-quality compile` and `seal`, preflight, scored `run`, and `score` at their normal stages. A cheap worker's confidence or self-reported pass does not replace any gate. If a draft fails a gate, record the failure and repair time in the authoring log.
|
|
22
24
|
|
|
23
25
|
## Show authoring progress and cost
|
|
24
26
|
|
|
25
|
-
Start an authoring log in `{test_artifacts}/evaluate/` before Stage 1 inspection, then carry it to `<evaluation-folder>/authoring-log.md` when that folder exists. Record each stage's start and completion time, its artifact paths, and its next gate. For each worker task, record dispatch and completion time, scope, requested tier, resolved model, outcome, retries, parent verification and repair time, and reported input/output tokens or cost when available. Write `unknown` for missing usage. Count parent work separately from worker work. On resume, read the existing log, mark any unfinished worker task interrupted, and continue with new entries rather than replacing prior timings. Keep this log outside `corpus/`, `probes/`, `mutations/`, and `runs/` so it does not alter a corpus digest or sealed run.
|
|
27
|
+
Start an authoring log in `{test_artifacts}/evaluate/` before Stage 1 inspection, then carry it to `<evaluation-folder>/authoring-log.md` when that folder exists. Record each stage's start and completion time, its artifact paths, and its next gate. For each worker task, record dispatch and completion time, scope, context mode (isolated or inherited), requested tier, resolved model, outcome, retries, parent verification and repair time, and reported input/output tokens or cost when available. Write `unknown` for missing usage. Count parent work separately from worker work. On resume, read the existing log, mark any unfinished worker task interrupted, and continue with new entries rather than replacing prior timings. Keep this log outside `corpus/`, `probes/`, `mutations/`, and `runs/` so it does not alter a corpus digest or sealed run.
|
|
26
28
|
|
|
27
29
|
For a long target run, use the runtime's progress command when available and report its current phase, completed/planned trials, whole-run elapsed time, and last heartbeat. Report scored trial usage separately from authoring usage; a subtotal is not full-run cost. Continue through `score` before reporting a verdict.
|
|
28
30
|
|