bmad-method-test-architecture-enterprise 2.0.2 → 2.0.3-next.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.
Files changed (43) hide show
  1. package/.claude-plugin/marketplace.json +1 -3
  2. package/README.md +3 -3
  3. package/cli/evaluate.js +25 -2
  4. package/cli/lib/evaluate/preflight.js +77 -1
  5. package/cli/lib/evaluate/run.js +43 -4
  6. package/cli/lib/evaluate/status.js +184 -0
  7. package/package.json +3 -2
  8. package/skills/bmad-tea/SKILL.md +2 -2
  9. package/skills/bmad-tea/customize.toml +4 -4
  10. package/skills/bmad-teach-me-testing/steps-c/step-04-session-01.md +2 -2
  11. package/skills/bmad-teach-me-testing/steps-c/step-04-session-03.md +1 -1
  12. package/skills/bmad-teach-me-testing/steps-c/step-04-session-05.md +2 -2
  13. package/skills/bmad-testarch-automate/red/instructions.md +1 -2
  14. package/skills/bmad-testarch-automate/resources/test-generation-routing.md +17 -21
  15. package/skills/bmad-testarch-evaluate/SKILL.md +5 -3
  16. package/skills/bmad-testarch-evaluate/assets/inspection-record.md +1 -1
  17. package/skills/bmad-testarch-evaluate/references/authoring-routing.md +29 -0
  18. package/skills/bmad-testarch-evaluate/references/ci.md +5 -5
  19. package/skills/bmad-testarch-evaluate/references/run.md +19 -0
  20. package/skills/bmad-testarch-framework/SKILL.md +2 -2
  21. package/skills/bmad-testarch-framework/checklist.md +4 -4
  22. package/skills/bmad-testarch-framework/ci/checklist.md +2 -2
  23. package/skills/bmad-testarch-framework/ci/instructions.md +1 -1
  24. package/skills/bmad-testarch-framework/resources/setup-routing.md +4 -4
  25. package/skills/bmad-testarch-test-design/checklist.md +6 -6
  26. package/skills/bmad-testarch-test-design/resources/test-design-epic-3.example.md +1 -1
  27. package/skills/bmad-testarch-test-design/test-design-template.md +1 -1
  28. package/skills/bmad-testarch-trace/checklist.md +2 -2
  29. package/skills/bmad-testarch-trace/steps-c/step-04-analyze-gaps.md +1 -1
  30. package/skills/bmad-testarch-trace/trace-template.md +1 -1
  31. package/skills/bmod-tea/bmod.toml +2 -4
  32. package/skills/bmod-tea/help/help.md +1 -3
  33. package/skills/bmod-tea/help/integrations.md +1 -1
  34. package/skills/bmod-tea/knowledge/burn-in.md +1 -1
  35. package/skills/bmod-tea/migration-1.toml +1 -1
  36. package/skills/bmad-testarch-atdd/SKILL.md +0 -16
  37. package/skills/bmad-testarch-atdd/bmod.toml +0 -4
  38. package/skills/bmad-testarch-atdd/customize.toml +0 -38
  39. package/skills/bmad-testarch-atdd/workflow.yaml +0 -40
  40. package/skills/bmad-testarch-ci/SKILL.md +0 -14
  41. package/skills/bmad-testarch-ci/bmod.toml +0 -4
  42. package/skills/bmad-testarch-ci/customize.toml +0 -43
  43. package/skills/bmad-testarch-ci/workflow.yaml +0 -44
@@ -31,16 +31,14 @@
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.2",
34
+ "version": "2.0.3-next.0",
35
35
  "author": {
36
36
  "name": "Murat K Ozcan (TEA Creator) & Brian (BMad) Madison"
37
37
  },
38
38
  "skills": [
39
39
  "./skills/bmod-tea",
40
40
  "./skills/bmad-tea",
41
- "./skills/bmad-testarch-atdd",
42
41
  "./skills/bmad-testarch-automate",
43
- "./skills/bmad-testarch-ci",
44
42
  "./skills/bmad-testarch-evaluate",
45
43
  "./skills/bmad-testarch-framework",
46
44
  "./skills/bmad-testarch-nfr",
package/README.md CHANGED
@@ -50,7 +50,7 @@ npx skills add https://github.com/bmad-code-org/bmad-method-test-architecture-en
50
50
 
51
51
  ## Start with one skill
52
52
 
53
- Ten commands start the eight skills: Framework includes CI setup; Automate includes red and expand modes.
53
+ Eight commands start the eight workflows: Framework includes CI setup; Automate includes red and expand modes.
54
54
 
55
55
  In Claude Code, Cursor, and Windsurf, type `/bmad-testarch-test-design` in chat.
56
56
  In Codex, type `$bmad-testarch-test-design`.
@@ -73,10 +73,10 @@ Evaluate:
73
73
 
74
74
  Framework and CI setup share one skill, `bmad-testarch-framework`.
75
75
  Ask for framework only, CI only, or both; TEA infers the scope from your prompt and asks "Do you want CI too?" once when CI scope is unclear in an interactive session. An unattended request with unclear scope runs framework setup only and states that CI was excluded.
76
- `/bmad-testarch-ci` and the `CI` menu code still work and start the same skill's CI setup, with your existing CI customizations.
76
+ Request CI setup through `/bmad-testarch-framework` or the agent's `CI` menu code. Existing CI customizations remain usable.
77
77
  Create, Resume, Validate, and Edit remain available for the selected setup scope.
78
78
 
79
- Automation uses `red` mode before implementation and `expand` mode for existing code. Your prompt selects the mode. `/bmad-testarch-atdd` and `AT` default to red; `/bmad-testarch-automate` and `TA` default to expand. Existing commands and customization files keep working. Create runs execute the generated tests and repair test issues for up to three rounds. Red confirms the intended missing behavior; expand aims for passing coverage and reports product defects.
79
+ Automation uses `red` mode before implementation and `expand` mode for existing code. Request red mode through `/bmad-testarch-automate` or the agent's `AT` menu code; the direct command defaults to expand. Existing customization files and saved progress remain usable. Create runs execute the generated tests and repair test issues for up to three rounds. Red confirms the intended missing behavior; expand aims for passing coverage and reports product defects.
80
80
 
81
81
  For Codex, replace the leading `/` in the table with `$`.
82
82
  The agent menu also accepts `GATE` to route you through test review, NFR evidence audit, and trace Phase 2.
package/cli/evaluate.js CHANGED
@@ -29,6 +29,8 @@
29
29
  * compare a scored run's evidence with baseline/ through eval-quality's
30
30
  * compareDominance (compared, first-run or refused, every one exit 0), or
31
31
  * with --accept replace baseline/ with a byte-identical snapshot of the run
32
+ * tea-evaluate status --evaluation <path> [--run <invocationId>] [--follow]
33
+ * show the latest run's progress, or follow it until it ends
32
34
  * tea-evaluate ci --evaluation <path> --tier <pr|merge|scheduled|release>
33
35
  * run exactly the checks ci/evaluation-ci-plan.json places on the tier, in
34
36
  * plan order, each one's exit, stdout and stderr kept under
@@ -130,6 +132,7 @@ const { runRunCommand } = require('./lib/evaluate/run');
130
132
  const { runCompareCommand } = require('./lib/evaluate/compare');
131
133
  const { runScoreCommand } = require('./lib/evaluate/score');
132
134
  const { runCiCommand } = require('./lib/evaluate/ci');
135
+ const { runStatusCommand } = require('./lib/evaluate/status');
133
136
  const { TIERS } = require('./lib/evaluate/ci-plan');
134
137
  const { escapeUnprintable, findingLine } = require('./lib/evaluate/finding-lines');
135
138
  const { calibrationInputsOf } = require('./lib/evaluate/records-calibration');
@@ -265,6 +268,17 @@ function compareCommand(options) {
265
268
  return runDriven('compare', runCompareCommand, options, { run: options.run, accept: options.accept === true });
266
269
  }
267
270
 
271
+ async function statusCommand(options) {
272
+ const folder = folderFrom(options);
273
+ const result = await runStatusCommand(folder, {
274
+ run: options.run,
275
+ follow: options.follow === true,
276
+ emit: (line) => process.stdout.write(`${NAME} status: ${escapeUnprintable(line)}\n`),
277
+ });
278
+ if (result.exitCode !== 0) process.stderr.write(`${NAME} status: ${escapeUnprintable(result.message)}\n`);
279
+ return result.exitCode;
280
+ }
281
+
268
282
  async function ciCommand(options) {
269
283
  if (!TIERS.includes(options.tier)) {
270
284
  throw new UsageError(
@@ -278,7 +292,7 @@ function buildProgram(run) {
278
292
  const program = new Command();
279
293
  program
280
294
  .name(NAME)
281
- .description('Validate, digest, preflight, run, score, compare and run the CI tiers of an Evaluate evaluation folder.')
295
+ .description('Validate, digest, preflight, run, score, compare, report status and run the CI tiers of an Evaluate evaluation folder.')
282
296
  .showHelpAfterError()
283
297
  .exitOverride()
284
298
  .configureOutput({ writeErr: (text) => process.stderr.write(text) });
@@ -338,6 +352,13 @@ function buildProgram(run) {
338
352
  .option('--run <invocationId>', 'the run to compare or accept; the most recent run when omitted')
339
353
  .option('--accept', 'replace baseline/ with a byte-identical snapshot of the run; refused for a dirty run')
340
354
  .action((options) => run(compareCommand, options));
355
+ program
356
+ .command('status')
357
+ .description("Show the newest run's progress, or follow it until completion; --run selects a specific invocation.")
358
+ .option('--evaluation <path>', 'the evaluation folder, or its evaluation.json')
359
+ .option('--run <invocationId>', 'the run to inspect; the most recent run when omitted')
360
+ .option('--follow', 'print progress changes and periodic heartbeats until the run ends')
361
+ .action((options) => run(statusCommand, options));
341
362
  program
342
363
  .command('ci')
343
364
  .description(
@@ -377,7 +398,9 @@ async function main(argv) {
377
398
  return await pending;
378
399
  } catch (error) {
379
400
  if (error instanceof UsageError) {
380
- process.stderr.write(`${NAME}: ${error.message}\nUsage: ${NAME} <check|digest|preflight|run|score|compare|ci> --evaluation <path>\n`);
401
+ process.stderr.write(
402
+ `${NAME}: ${error.message}\nUsage: ${NAME} <check|digest|preflight|run|score|compare|status|ci> --evaluation <path>\n`,
403
+ );
381
404
  return EXIT_CODES.usage;
382
405
  }
383
406
  if (error instanceof CorpusIndexError) {
@@ -513,6 +513,8 @@ function runPreflightCommand(folder, { partition, requirePartition = false, ...o
513
513
  * @param {Set<string>|null} [options.selectedProbeIds] the probes the partition selects; every probe when null
514
514
  * @param {NodeJS.ProcessEnv} [options.env]
515
515
  * @param {(line: string) => void} [options.log]
516
+ * @param {number} [options.progressIntervalMs] heartbeat interval, for tests and long-running runs
517
+ * @param {number} [options.progressStartedAt] invocation start time, before setup
516
518
  * @param {(context: object) => PreflightOutcome|null|Promise<PreflightOutcome|null>} [options.prepare]
517
519
  * @param {(context: object) => Promise<PreflightOutcome>} [options.afterVerdict]
518
520
  * @returns {Promise<PreflightOutcome>}
@@ -520,10 +522,11 @@ function runPreflightCommand(folder, { partition, requirePartition = false, ...o
520
522
  async function runPipeline(folder, options) {
521
523
  // The invocation's run directory and run.json, held in memory, so its end is
522
524
  // recorded from what the runtime knows, never from a file read back.
523
- const state = { writer: null, run: null, sealed: false, retractUnlessSealed: [] };
525
+ const state = { writer: null, run: null, sealed: false, retractUnlessSealed: [], progressTimer: null, finishProgress: null };
524
526
  try {
525
527
  return await recordEnd(await pipeline(folder, options, state), state);
526
528
  } finally {
529
+ if (state.progressTimer !== null) clearInterval(state.progressTimer);
527
530
  // The directories the run made stay held open until here, so none of
528
531
  // their inode numbers can pass to a directory the target makes.
529
532
  state.writer?.close();
@@ -543,6 +546,7 @@ function recordEnd(outcome, state) {
543
546
  const failed = [];
544
547
  for (const [what, step] of [
545
548
  ...state.retractUnlessSealed.map((file) => [`remove ${file}`, () => state.writer.remove(file)]),
549
+ ['record final progress', () => state.finishProgress?.('stopped')],
546
550
  ['record this end in run.json', () => state.writer.replaceJson('run.json', state.run)],
547
551
  ]) {
548
552
  try {
@@ -574,6 +578,8 @@ async function pipeline(
574
578
  selectedProbeIds = null,
575
579
  partition,
576
580
  seed,
581
+ progressIntervalMs = 30_000,
582
+ progressStartedAt,
577
583
  },
578
584
  state,
579
585
  ) {
@@ -658,6 +664,7 @@ async function pipeline(
658
664
  }
659
665
  };
660
666
  const onSignal = (name) => {
667
+ if (state.progressTimer !== null) clearInterval(state.progressTimer);
661
668
  removeScratch();
662
669
  if (journal) {
663
670
  try {
@@ -671,6 +678,7 @@ async function pipeline(
671
678
  for (const file of [...retractOnSignal, ...state.retractUnlessSealed]) state.writer.remove(file);
672
679
  state.run.outcome = { stage: 'signal', exitCode: null, signal: name, message: `stopped by ${name} before it finished` };
673
680
  if (state.run.command === 'run') state.run.completed = false;
681
+ state.finishProgress?.('stopped');
674
682
  state.writer.replaceJson('run.json', state.run);
675
683
  } catch {
676
684
  // The signal still ends the process; run.json keeps its last state.
@@ -735,6 +743,11 @@ async function pipeline(
735
743
  log(
736
744
  `pristine workspace, ${pristine.kind === 'git-worktree' ? `a detached worktree at ${pristine.commit}` : `a temp copy${pristine.dirty ? ' of the working tree' : ''}`}: ${pristine.root}`,
737
745
  );
746
+ if (pristine.kind === 'git-worktree' && sharedState) {
747
+ log(
748
+ 'Git worktrees share repository refs, config and hooks. Keep Git writes in every worktree of this repository paused until this command ends, or use a dedicated clone for concurrent work.',
749
+ );
750
+ }
738
751
  // The evaluation folder is read from the working tree (the workspaces
739
752
  // leave it out), so uncommitted work in it reaches the run, which is then
740
753
  // dirty: no committed state names what it measured.
@@ -766,6 +779,8 @@ async function pipeline(
766
779
  runsDirectory,
767
780
  invocationId,
768
781
  seed: runSeed,
782
+ progressStartedAt,
783
+ progressIntervalMs,
769
784
  retractOnSignal,
770
785
  scratch,
771
786
  state,
@@ -826,6 +841,8 @@ async function runInWorkspaces({
826
841
  runsDirectory,
827
842
  invocationId,
828
843
  seed,
844
+ progressStartedAt,
845
+ progressIntervalMs,
829
846
  retractOnSignal,
830
847
  scratch,
831
848
  state,
@@ -877,9 +894,65 @@ async function runInWorkspaces({
877
894
  adopterTree: { repository: before.repository === null ? null : REPOSITORY, unchanged: null },
878
895
  refused: [],
879
896
  };
897
+ const progressStarted = progressStartedAt ?? Date.now();
898
+ const progressCreatedAt = Date.now();
899
+ const progressCreatedTick = globalThis.performance.now();
900
+ const setupBeforeRecordMs = Math.max(0, progressCreatedAt - progressStarted);
901
+ let phaseStartedTick = progressCreatedTick;
902
+ if (command === 'run') {
903
+ run.progress = {
904
+ phase: 'setup',
905
+ detail: 'Preparing the run',
906
+ startedAt: new Date(progressStarted).toISOString(),
907
+ updatedAt: new Date(progressCreatedAt).toISOString(),
908
+ phaseStartedAt: new Date(progressStarted).toISOString(),
909
+ phaseElapsedMs: setupBeforeRecordMs,
910
+ elapsedMs: setupBeforeRecordMs,
911
+ phaseDurationsMs: {},
912
+ completedTrials: 0,
913
+ totalTrials: null,
914
+ usage: null,
915
+ };
916
+ }
880
917
  state.run = run;
881
918
  const writeRun = () => writer.replaceJson('run.json', run);
882
919
  writeRun();
920
+ const progress = (phase, fields = {}) => {
921
+ if (command !== 'run' || state.sealed) return;
922
+ const now = Date.now();
923
+ const tick = globalThis.performance.now();
924
+ const sinceCreated = Math.max(0, tick - progressCreatedTick);
925
+ if (phase !== run.progress.phase) {
926
+ const prior = run.progress.phase;
927
+ const priorDuration = Math.max(0, tick - phaseStartedTick) + (prior === 'setup' ? setupBeforeRecordMs : 0);
928
+ run.progress.phaseDurationsMs[prior] = (run.progress.phaseDurationsMs[prior] ?? 0) + priorDuration;
929
+ phaseStartedTick = tick;
930
+ run.progress.phase = phase;
931
+ run.progress.phaseStartedAt = new Date(now).toISOString();
932
+ }
933
+ Object.assign(run.progress, fields, {
934
+ updatedAt: new Date(now).toISOString(),
935
+ elapsedMs: setupBeforeRecordMs + sinceCreated,
936
+ phaseElapsedMs: Math.max(0, tick - phaseStartedTick) + (phase === 'setup' ? setupBeforeRecordMs : 0),
937
+ });
938
+ writeRun();
939
+ };
940
+ state.finishProgress = (phase) => progress(phase, { detail: phase === 'complete' ? 'Run sealed' : 'Run stopped' });
941
+ if (command === 'run') {
942
+ state.progressTimer = setInterval(() => {
943
+ try {
944
+ progress(run.progress.phase);
945
+ log(
946
+ `progress: ${run.progress.phase}; ${run.progress.completedTrials ?? '?'}/${run.progress.totalTrials ?? '?'} trials; ${Math.floor(run.progress.elapsedMs / 1000)}s elapsed`,
947
+ );
948
+ } catch (error) {
949
+ // A progress write is guarded like every other run-directory write. The main
950
+ // pipeline will detect the same changed directory at its next verification.
951
+ log(`progress could not be recorded: ${error.message}`);
952
+ }
953
+ }, progressIntervalMs);
954
+ state.progressTimer.unref?.();
955
+ }
883
956
 
884
957
  // The run keeps its own copy of the contract this partition runs, so every stage below reads the
885
958
  // same bytes and the verdict can be reproduced from the run directory alone.
@@ -951,6 +1024,7 @@ async function runInWorkspaces({
951
1024
  }
952
1025
  };
953
1026
 
1027
+ progress('qualification', { detail: 'Qualifying probes and clean controls' });
954
1028
  const qualified = [];
955
1029
  const policyPath = path.join(folder, POLICY_PATH);
956
1030
  const policy = fs.existsSync(policyPath) ? readJson(policyPath) : null;
@@ -1192,6 +1266,7 @@ async function runInWorkspaces({
1192
1266
  // Every leg has a port, and so a home, of its own.
1193
1267
  resetHome() {},
1194
1268
  };
1269
+ progress('preflight', { detail: 'Running preflight legs and verdict' });
1195
1270
  try {
1196
1271
  const recorder = recordingPort({
1197
1272
  pristine: { label: 'pristine', cwd: workspaceDirectory(pristine), port: auditedLegs },
@@ -1325,6 +1400,7 @@ async function runInWorkspaces({
1325
1400
  writer,
1326
1401
  run,
1327
1402
  writeRun,
1403
+ progress,
1328
1404
  treeUnchanged,
1329
1405
  retractUnlessSealed: state.retractUnlessSealed,
1330
1406
  markSealed: () => {
@@ -236,11 +236,12 @@ function refusal({ folder, selectedProbeIds = null }) {
236
236
  * (`before-state.js`) may fail its baseline, and the run is marked so it is never accepted as a baseline
237
237
  * @param {NodeJS.ProcessEnv} [options.env]
238
238
  * @param {(line: string) => void} [options.log]
239
+ * @param {number} [options.progressIntervalMs] heartbeat interval used by integration tests
239
240
  * @returns {Promise<PreflightOutcome>}
240
241
  */
241
242
  function runRunCommand(
242
243
  folder,
243
- { fromWorkingTree = false, partition, trials, seed, beforeState = false, env = process.env, log = () => {} } = {},
244
+ { fromWorkingTree = false, partition, trials, seed, beforeState = false, env = process.env, log = () => {}, progressIntervalMs } = {},
244
245
  ) {
245
246
  if (trials !== undefined && !(Number.isInteger(trials) && trials >= 1))
246
247
  return Promise.resolve(
@@ -260,7 +261,7 @@ function runRunCommand(
260
261
  } catch {
261
262
  // The pipeline's check reports malformed source files with authoring findings; it keeps the partition, so a development
262
263
  // run still does not open the held-out plan.
263
- return runPipeline(folder, { command: 'run', partition, fromWorkingTree, seed, env, log });
264
+ return runPipeline(folder, { command: 'run', partition, fromWorkingTree, seed, env, log, progressIntervalMs });
264
265
  }
265
266
  if (selection.refusal !== undefined) return Promise.resolve(new PreflightOutcome({ stage: 'check', ...selection.refusal }));
266
267
  const { selectedProbeIds } = selection;
@@ -276,6 +277,8 @@ function runRunCommand(
276
277
  seed,
277
278
  env,
278
279
  log,
280
+ progressIntervalMs,
281
+ progressStartedAt: started,
279
282
  prepare: async (context) => {
280
283
  const refused = refusal({ ...context, selectedProbeIds });
281
284
  if (refused !== null) return refused;
@@ -1148,6 +1151,8 @@ async function runTrialSets(given) {
1148
1151
  sealed,
1149
1152
  } = context;
1150
1153
  const { treeUnchanged, stop, log, snapshot } = context;
1154
+ const progress = context.progress ?? (() => {});
1155
+ progress('clean-control-qualification', { detail: 'Qualifying clean controls' });
1151
1156
 
1152
1157
  const cleanControls = await qualifyCleanControls({ ...context, seed: run.seed });
1153
1158
  // The gameability probes the shared pipeline qualified before the verdict.
@@ -1214,8 +1219,19 @@ async function runTrialSets(given) {
1214
1219
  const { kind } = snapshot.layer.evaluator;
1215
1220
  if (kind === 'records') {
1216
1221
  if (sealed === null) throw noStages();
1222
+ // Imported sets can share arm work, and the importer does not attest to
1223
+ // complete usage reports. Leave both the trial count and cost unknown.
1224
+ progress('records-import', {
1225
+ detail: 'Importing existing evaluator records',
1226
+ completedTrials: null,
1227
+ totalTrials: null,
1228
+ usage: null,
1229
+ });
1217
1230
  return concludeImportedRecords({ ...context, arms, refusedIds });
1218
1231
  }
1232
+ const trialCount = snapshot.trials ?? evaluation.trials;
1233
+ const totalTrials = arms.length * trialCount;
1234
+ progress('trial-preparation', { detail: 'Preparing scored trials', totalTrials });
1219
1235
  if (convertsRows(kind)) {
1220
1236
  // A command evaluator and a sealed-brief agent read the brief the run sealed, as its bytes were written.
1221
1237
  if (sealed === null) throw noStages();
@@ -1242,6 +1258,7 @@ async function runTrialSets(given) {
1242
1258
 
1243
1259
  let calibrationDigest = null;
1244
1260
  if ((contract.rubrics ?? []).length > 0) {
1261
+ progress('calibration', { detail: 'Calibrating rubric judge' });
1245
1262
  const { layer } = snapshot;
1246
1263
  const judgeItem = async ({ rubric, criterion, response, responseKind }) => {
1247
1264
  const stepId = /^\/interactions\/([^/]+)/.exec(criterion.evidence)?.[1];
@@ -1357,7 +1374,6 @@ async function runTrialSets(given) {
1357
1374
  }
1358
1375
  }
1359
1376
 
1360
- const trialCount = snapshot.trials ?? evaluation.trials;
1361
1377
  if (sealed === null) throw noStages();
1362
1378
  const { contractDigest, sealedBriefDigest } = sealed;
1363
1379
  const { conditions, layer } = snapshot;
@@ -1439,7 +1455,15 @@ async function runTrialSets(given) {
1439
1455
  };
1440
1456
 
1441
1457
  // A sealed-brief agent chooses its own calls, so its verdicts count only once it agrees with itself on the arms it will judge.
1442
- if (kind === 'sealed-brief-agent') await qualifyEvaluator({ ...context, arms, sealing });
1458
+ if (kind === 'sealed-brief-agent') {
1459
+ progress('evaluator-qualification', { detail: 'Qualifying evaluator agreement' });
1460
+ await qualifyEvaluator({ ...context, arms, sealing });
1461
+ }
1462
+
1463
+ let completedTrials = 0;
1464
+ let reportedUse = ZERO;
1465
+ let missingReports = 0;
1466
+ progress('trials', { detail: 'Running scored trials', totalTrials, completedTrials });
1443
1467
 
1444
1468
  for (const arm of arms) {
1445
1469
  arm.trials = [];
@@ -1452,6 +1476,19 @@ async function runTrialSets(given) {
1452
1476
  throw stop({ stage: 'trial', exitCode: 12, message: `the installed agent version could not be held after a trial: ${agentMoved}` });
1453
1477
  trial.elapsedMs += Date.now() - versionReadBegan;
1454
1478
  arm.trials.push(trial);
1479
+ reportedUse = addUsage(reportedUse, trial.resourceUse);
1480
+ missingReports += trial.unreportedSteps.length;
1481
+ completedTrials += 1;
1482
+ progress('trials', {
1483
+ detail: `${arm.conditionArm}: trial ${trialIndex} of ${trialCount}`,
1484
+ completedTrials,
1485
+ usage: {
1486
+ trialCostUsd: missingReports === 0 && completedTrials === totalTrials ? reportedUse.costUsd : null,
1487
+ reportedTrialCostUsd: reportedUse.costUsd,
1488
+ missingReports,
1489
+ scope: 'trials-only',
1490
+ },
1491
+ });
1455
1492
  // Read after every trial, so a target that writes into the project stops the run at once.
1456
1493
  treeUnchanged('trials');
1457
1494
  }
@@ -1493,6 +1530,7 @@ async function runTrialSets(given) {
1493
1530
  // wrote, and the run directory nothing else, before any trial set is written.
1494
1531
  writer.verify('after the trials');
1495
1532
 
1533
+ progress('sealing', { detail: 'Sealing trial sets and evidence' });
1496
1534
  const trialSets = [];
1497
1535
  const recordDigests = {};
1498
1536
  const manifestDigests = {};
@@ -2103,6 +2141,7 @@ async function completeRun(
2103
2141
  // runtime wrote, before run.json says completed; that write is the run's last.
2104
2142
  treeUnchanged('sealing', { record: false });
2105
2143
  writer.verify('after the trial sets were sealed');
2144
+ context.progress?.('complete', { detail: 'Run sealed' });
2106
2145
  Object.assign(run, {
2107
2146
  artifacts,
2108
2147
  contractDigest: sealed.contractDigest,
@@ -0,0 +1,184 @@
1
+ /** Read-only progress view for a running or finished Evaluate invocation. */
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+
8
+ const { readFolderFile } = require('./folder');
9
+
10
+ const STALE_AFTER_MS = 90_000;
11
+ const POLL_MS = 2000;
12
+ const REPORT_EVERY_MS = 30_000;
13
+
14
+ /** Only the runtime's flat invocation identifiers may name a run. */
15
+ function validRunId(value) {
16
+ return typeof value === 'string' && /^[A-Za-z0-9_-]+$/.test(value) && value !== '.' && value !== '..';
17
+ }
18
+
19
+ function readRecord(folder, runId) {
20
+ const relative = `runs/${runId}/run.json`;
21
+ // An atomic progress replacement can land between the guarded path walk and
22
+ // open. Retry that one race, while retaining every link and file-type refusal.
23
+ let read;
24
+ for (let attempt = 0; attempt < 3; attempt += 1) {
25
+ read = readFolderFile(folder, relative);
26
+ if (read.ok || read.reason !== 'changed while it was read; run the command again') break;
27
+ }
28
+ if (!read.ok) return { error: `${relative} ${read.reason}` };
29
+ let record;
30
+ try {
31
+ record = JSON.parse(read.bytes.toString('utf8'));
32
+ } catch {
33
+ return { error: `${relative} is not valid JSON` };
34
+ }
35
+ if (record === null || typeof record !== 'object' || Array.isArray(record) || record.command !== 'run' || record.invocationId !== runId) {
36
+ return { error: `${relative} does not identify a tea-evaluate run` };
37
+ }
38
+ return { record };
39
+ }
40
+
41
+ /** Resolve the requested run, or the newest run whose guarded record identifies it. */
42
+ function locateRun(folder, requested) {
43
+ if (requested !== undefined) {
44
+ if (!validRunId(requested)) return { error: `--run ${JSON.stringify(requested)} is not a run identifier` };
45
+ const read = readRecord(folder, requested);
46
+ return read.error === undefined ? { runId: requested, record: read.record } : read;
47
+ }
48
+ const runs = path.join(folder, 'runs');
49
+ let names;
50
+ try {
51
+ if (!fs.lstatSync(runs).isDirectory()) return { error: `${runs} is not a runs directory` };
52
+ names = fs.readdirSync(runs, { withFileTypes: true });
53
+ } catch (error) {
54
+ if (error.code === 'ENOENT') return { error: `${runs} holds no tea-evaluate run` };
55
+ return { error: `${runs} cannot be read (${error.code ?? error.message})` };
56
+ }
57
+ for (const entry of names
58
+ .map((item) => item.name)
59
+ .filter(validRunId)
60
+ .sort()
61
+ .toReversed()) {
62
+ const read = readRecord(folder, entry);
63
+ if (read.error === undefined) return { runId: entry, record: read.record };
64
+ }
65
+ return { error: `${runs} holds no tea-evaluate run` };
66
+ }
67
+
68
+ function elapsedText(ms) {
69
+ if (!Number.isFinite(ms) || ms < 0) return 'unknown';
70
+ const seconds = Math.floor(ms / 1000);
71
+ const hours = Math.floor(seconds / 3600);
72
+ const minutes = Math.floor((seconds % 3600) / 60);
73
+ return `${hours}h ${minutes}m ${seconds % 60}s`;
74
+ }
75
+
76
+ function shortDuration(ms) {
77
+ const seconds = Math.floor(ms / 1000);
78
+ if (seconds < 60) return `${seconds}s`;
79
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
80
+ return elapsedText(ms);
81
+ }
82
+
83
+ function safeText(value) {
84
+ return typeof value === 'string' ? value.replaceAll(/[\r\n\t]+/g, ' ').trim() : '';
85
+ }
86
+
87
+ function costAmount(value) {
88
+ return typeof value === 'string' && /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value) ? value : null;
89
+ }
90
+
91
+ function costText(usage) {
92
+ if (usage === null || typeof usage !== 'object' || usage.scope !== 'trials-only') return 'trial cost unknown (trials only)';
93
+ const missing = Number.isInteger(usage.missingReports) && usage.missingReports >= 0 ? usage.missingReports : null;
94
+ const reported = costAmount(usage.reportedTrialCostUsd);
95
+ const complete = costAmount(usage.trialCostUsd);
96
+ if (complete !== null && missing === 0) return `trial cost $${complete} (trials only)`;
97
+ const parts = ['trial cost unknown'];
98
+ if (reported !== null) parts.push(`reported $${reported}`);
99
+ if (missing !== null) parts.push(`${missing} missing report${missing === 1 ? '' : 's'}`);
100
+ return `${parts[0]} (${[...parts.slice(1), 'trials only'].join('; ')})`;
101
+ }
102
+
103
+ function statusOf(runId, record, now = Date.now()) {
104
+ const progress = record.progress && typeof record.progress === 'object' && !Array.isArray(record.progress) ? record.progress : {};
105
+ const outcome = record.outcome && typeof record.outcome === 'object' ? record.outcome : null;
106
+ const updatedAt = Date.parse(progress.updatedAt);
107
+ const startedAt = Date.parse(progress.startedAt ?? record.startedAt);
108
+ const terminal = record.completed === true || outcome !== null;
109
+ const stale = !terminal && (!Number.isFinite(updatedAt) || now - updatedAt >= STALE_AFTER_MS);
110
+ const state = terminal ? (record.completed === true ? 'complete' : 'stopped') : stale ? 'stale' : 'live';
111
+ const completedTrials = Number.isInteger(progress.completedTrials) && progress.completedTrials >= 0 ? progress.completedTrials : null;
112
+ const totalTrials = Number.isInteger(progress.totalTrials) && progress.totalTrials >= 0 ? progress.totalTrials : null;
113
+ const lastElapsed = Number.isFinite(progress.elapsedMs) && progress.elapsedMs >= 0 ? progress.elapsedMs : null;
114
+ const elapsed =
115
+ lastElapsed === null
116
+ ? terminal && Number.isFinite(record.durationMs)
117
+ ? record.durationMs
118
+ : stale
119
+ ? Number.NaN
120
+ : now - startedAt
121
+ : lastElapsed;
122
+ const parts = [`run ${runId}: ${state}`];
123
+ if (safeText(progress.phase)) parts.push(safeText(progress.phase));
124
+ if (safeText(progress.detail)) parts.push(safeText(progress.detail));
125
+ if (Number.isFinite(progress.phaseElapsedMs) && progress.phaseElapsedMs >= 0) {
126
+ parts.push(`phase elapsed ${elapsedText(progress.phaseElapsedMs)}`);
127
+ }
128
+ if (completedTrials !== null && totalTrials !== null) parts.push(`trials ${completedTrials}/${totalTrials}`);
129
+ parts.push(`elapsed ${elapsedText(elapsed)}`);
130
+ const phaseDurations = progress.phaseDurationsMs;
131
+ if (phaseDurations !== null && typeof phaseDurations === 'object' && !Array.isArray(phaseDurations)) {
132
+ const finishedPhases = Object.entries(phaseDurations)
133
+ .filter(([phase, ms]) => safeText(phase) !== '' && Number.isFinite(ms) && ms >= 0)
134
+ .map(([phase, ms]) => `${safeText(phase)} ${shortDuration(ms)}`);
135
+ if (finishedPhases.length > 0) parts.push(`phase times ${finishedPhases.join(', ')}`);
136
+ }
137
+ parts.push(
138
+ costText(progress.usage),
139
+ Number.isFinite(updatedAt)
140
+ ? `last update ${new Date(updatedAt).toISOString()} (${elapsedText(Math.max(0, now - updatedAt))} ago)`
141
+ : 'last update unknown',
142
+ );
143
+ if (outcome !== null) {
144
+ if (Number.isInteger(outcome.exitCode)) parts.push(`exit ${outcome.exitCode}`);
145
+ if (safeText(outcome.signal)) parts.push(`signal ${safeText(outcome.signal)}`);
146
+ }
147
+ return { runId, state, terminal, stale, line: parts.join(' | '), progress, outcome };
148
+ }
149
+
150
+ function sleep(ms) {
151
+ return new Promise((resolve) => setTimeout(resolve, ms));
152
+ }
153
+
154
+ /** Emit one snapshot, then material changes and periodic heartbeats until the run ends. */
155
+ async function runStatusCommand(folder, { run, follow = false, emit, pollMs = POLL_MS, reportEveryMs = REPORT_EVERY_MS } = {}) {
156
+ const located = locateRun(folder, run);
157
+ if (located.error !== undefined) return { exitCode: 64, message: located.error };
158
+ const { runId } = located;
159
+ let lastIdentity = null;
160
+ let lastReportAt = 0;
161
+ for (;;) {
162
+ const read = readRecord(folder, runId);
163
+ if (read.error !== undefined) return { exitCode: 12, message: read.error };
164
+ const now = Date.now();
165
+ const status = statusOf(runId, read.record, now);
166
+ const identity = JSON.stringify({
167
+ state: status.state,
168
+ phase: status.progress.phase,
169
+ detail: status.progress.detail,
170
+ completedTrials: status.progress.completedTrials,
171
+ totalTrials: status.progress.totalTrials,
172
+ outcome: status.outcome,
173
+ });
174
+ if (lastIdentity !== identity || now - lastReportAt >= reportEveryMs) {
175
+ emit?.(status.line);
176
+ lastIdentity = identity;
177
+ lastReportAt = now;
178
+ }
179
+ if (!follow || status.terminal) return { exitCode: 0, message: status.line, status };
180
+ await sleep(pollMs);
181
+ }
182
+ }
183
+
184
+ module.exports = { STALE_AFTER_MS, locateRun, runStatusCommand, statusOf, validRunId };
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.2",
4
+ "version": "2.0.3-next.0",
5
5
  "description": "Master Test Architect for quality strategy, test automation, and release gates",
6
6
  "keywords": [
7
7
  "bmad",
@@ -89,7 +89,7 @@
89
89
  "release:minor": "gh workflow run publish.yaml -f channel=latest -f bump=minor",
90
90
  "release:next": "gh workflow run publish.yaml -f channel=next",
91
91
  "release:patch": "gh workflow run publish.yaml -f channel=latest -f bump=patch",
92
- "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-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-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:docs-evaluate-sidebar && 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: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: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:trial-set-scoring && 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:install-linux-sandbox && npm run test:shards && npm run test:groups && npm run test: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",
92
+ "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-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:docs-evaluate-sidebar && 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: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: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:trial-set-scoring && 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:install-linux-sandbox && npm run test:shards && npm run test:groups && npm run test: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",
93
93
  "test:atdd-isolation": "node test/test-atdd-isolation.js",
94
94
  "test:atdd-net-guard": "node test/test-atdd-net-guard.js",
95
95
  "test:atdd-scoring": "node test/test-atdd-scoring.js",
@@ -209,6 +209,7 @@
209
209
  "test:evaluate-pr-workflow": "node test/test-evaluate-pr-tier.js workflow",
210
210
  "test:evaluate-preflight": "node test/test-evaluate-preflight.js",
211
211
  "test:evaluate-private": "node test/test-evaluate-evaluators.js --group=private",
212
+ "test:evaluate-progress": "node test/test-evaluate-progress.js",
212
213
  "test:evaluate-promptfoo": "node test/test-evaluate-promptfoo.js",
213
214
  "test:evaluate-records": "node test/test-evaluate-evaluators.js --group=records",
214
215
  "test:evaluate-run": "node test/test-evaluate-run.js --group=run",