bmad-method-test-architecture-enterprise 1.26.0 → 1.26.1-next.1

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 (94) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.github/workflows/quality.yaml +9 -0
  3. package/CHANGELOG.md +74 -0
  4. package/README.md +14 -13
  5. package/cli/lib/parse-routing.js +67 -0
  6. package/cli/routing-runner.js +197 -0
  7. package/docs/explanation/eval-quality-adoption-guide.md +1 -1
  8. package/docs/explanation/eval-quality-command-adapter.md +25 -20
  9. package/docs/explanation/eval-quality-roadmap.md +6 -6
  10. package/package.json +8 -3
  11. package/test/README.md +70 -11
  12. package/test/contracts/README.md +31 -3
  13. package/test/contracts/expected-status.json +6 -0
  14. package/test/contracts/fragment-selection/bmad-testarch-atdd.contract.json +1 -1
  15. package/test/contracts/fragment-selection/bmad-testarch-automate.contract.json +1 -1
  16. package/test/contracts/fragment-selection/bmad-testarch-ci.contract.json +1 -1
  17. package/test/contracts/fragment-selection/bmad-testarch-framework.contract.json +1 -1
  18. package/test/contracts/fragment-selection/bmad-testarch-nfr.contract.json +1 -1
  19. package/test/contracts/fragment-selection/bmad-testarch-test-design.contract.json +1 -1
  20. package/test/contracts/fragment-selection/bmad-testarch-test-review.contract.json +1 -1
  21. package/test/contracts/fragment-selection/bmad-testarch-trace.contract.json +1 -1
  22. package/test/contracts/tea-routing-controls.contract.json +1633 -0
  23. package/test/contracts/tea-routing-intents.contract.json +2468 -0
  24. package/test/contracts/test-review.contract.json +1 -1
  25. package/test/contracts/trace.contract.json +1 -1
  26. package/test/eval-bmad-tea-routing.js +1273 -0
  27. package/test/eval-contract-strength.js +25 -6
  28. package/test/eval-fragment-selection.js +8 -2
  29. package/test/eval-test-review.js +5 -2
  30. package/test/eval-trace.js +4 -1
  31. package/test/evals/suite-manifest.json +48 -7
  32. package/test/fixtures/tea-routing-eval/ground-truth.json +204 -0
  33. package/test/fixtures/tea-routing-eval/intents.json +94 -0
  34. package/test/lib/eval-quality-inputs.js +19 -10
  35. package/test/lib/probe-scoring.js +149 -0
  36. package/test/lib/probe-targets.js +170 -29
  37. package/test/probes/README.md +23 -3
  38. package/test/probes/expected-strength.json +116 -0
  39. package/test/probes/fragment-selection/bmad-testarch-atdd.probes.json +4 -3
  40. package/test/probes/fragment-selection/bmad-testarch-automate.probes.json +4 -3
  41. package/test/probes/fragment-selection/bmad-testarch-ci.probes.json +4 -3
  42. package/test/probes/fragment-selection/bmad-testarch-framework.probes.json +4 -3
  43. package/test/probes/fragment-selection/bmad-testarch-nfr.probes.json +4 -3
  44. package/test/probes/fragment-selection/bmad-testarch-test-design.probes.json +4 -3
  45. package/test/probes/fragment-selection/bmad-testarch-test-review.probes.json +4 -3
  46. package/test/probes/fragment-selection/bmad-testarch-trace.probes.json +4 -3
  47. package/test/probes/tea-routing-controls.probes.json +118 -0
  48. package/test/probes/tea-routing-intents.probes.json +106 -0
  49. package/test/probes/test-review.probes.json +31 -21
  50. package/test/probes/trace.probes.json +10 -7
  51. package/test/replay/bmad-tea-routing/clarification-correct/expected.json +146 -0
  52. package/test/replay/bmad-tea-routing/clarification-correct/stdout.txt +1 -0
  53. package/test/replay/bmad-tea-routing/clarification-names-both-by-label/expected.json +144 -0
  54. package/test/replay/bmad-tea-routing/clarification-names-both-by-label/stdout.txt +1 -0
  55. package/test/replay/bmad-tea-routing/clarification-names-one-side/expected.json +144 -0
  56. package/test/replay/bmad-tea-routing/clarification-names-one-side/stdout.txt +1 -0
  57. package/test/replay/bmad-tea-routing/clear-intent-left-unrouted/expected.json +147 -0
  58. package/test/replay/bmad-tea-routing/clear-intent-left-unrouted/stdout.txt +1 -0
  59. package/test/replay/bmad-tea-routing/confident-route-on-ambiguous/expected.json +152 -0
  60. package/test/replay/bmad-tea-routing/confident-route-on-ambiguous/stdout.txt +1 -0
  61. package/test/replay/bmad-tea-routing/confident-route-on-unservable/expected.json +145 -0
  62. package/test/replay/bmad-tea-routing/confident-route-on-unservable/stdout.txt +1 -0
  63. package/test/replay/bmad-tea-routing/correct-route/expected.json +146 -0
  64. package/test/replay/bmad-tea-routing/correct-route/stdout.txt +1 -0
  65. package/test/replay/bmad-tea-routing/decline-correct/expected.json +143 -0
  66. package/test/replay/bmad-tea-routing/decline-correct/stdout.txt +1 -0
  67. package/test/replay/bmad-tea-routing/decline-without-saying-what-is-missing/expected.json +142 -0
  68. package/test/replay/bmad-tea-routing/decline-without-saying-what-is-missing/stdout.txt +1 -0
  69. package/test/replay/bmad-tea-routing/fenced-json/expected.json +144 -0
  70. package/test/replay/bmad-tea-routing/fenced-json/stdout.txt +13 -0
  71. package/test/replay/bmad-tea-routing/gate-routed-as-a-workflow/expected.json +146 -0
  72. package/test/replay/bmad-tea-routing/gate-routed-as-a-workflow/stdout.txt +1 -0
  73. package/test/replay/bmad-tea-routing/prose-no-json/expected.json +109 -0
  74. package/test/replay/bmad-tea-routing/prose-no-json/stdout.txt +1 -0
  75. package/test/replay/bmad-tea-routing/reason-not-grounded/expected.json +144 -0
  76. package/test/replay/bmad-tea-routing/reason-not-grounded/stdout.txt +1 -0
  77. package/test/replay/bmad-tea-routing/route-in-the-messages-own-words/expected.json +142 -0
  78. package/test/replay/bmad-tea-routing/route-in-the-messages-own-words/stdout.txt +1 -0
  79. package/test/replay/bmad-tea-routing/scope-dropped/expected.json +143 -0
  80. package/test/replay/bmad-tea-routing/scope-dropped/stdout.txt +1 -0
  81. package/test/replay/bmad-tea-routing/scope-echoes-the-message/expected.json +144 -0
  82. package/test/replay/bmad-tea-routing/scope-echoes-the-message/stdout.txt +1 -0
  83. package/test/replay/bmad-tea-routing/wrong-menu-code/expected.json +148 -0
  84. package/test/replay/bmad-tea-routing/wrong-menu-code/stdout.txt +1 -0
  85. package/test/test-contract-oracles.js +204 -1
  86. package/test/test-eval-quality-corpus.js +301 -0
  87. package/test/test-eval-replay.js +99 -6
  88. package/test/test-port-totality.js +297 -0
  89. package/test/test-probe-conformance.js +21 -4
  90. package/test/test-probe-targets.js +120 -2
  91. package/test/test-test-review-cli.js +10 -3
  92. package/tools/generate-contracts.js +480 -3
  93. package/tools/generate-probes.js +133 -2
  94. package/tools/validate-ci-coverage.js +67 -2
@@ -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 automation, CI/CD quality gates, and structured testing education. Part of the BMad Method ecosystem.",
34
- "version": "1.26.0",
34
+ "version": "1.26.1-next.1",
35
35
  "author": {
36
36
  "name": "Murat K Ozcan (TEA Creator) & Brian (BMad) Madison"
37
37
  },
@@ -148,12 +148,18 @@ jobs:
148
148
  - name: Validate trace eval corpus
149
149
  run: npm run test:eval-trace-data
150
150
 
151
+ - name: Validate the bmad-tea routing eval corpus
152
+ run: npm run test:eval-routing-data
153
+
151
154
  - name: Validate the eval suite manifest and result schema
152
155
  run: npm run test:eval-schemas
153
156
 
154
157
  - name: Replay stored eval outputs against the scorers
155
158
  run: npm run test:eval-replay
156
159
 
160
+ - name: Compile eval-quality's own published corpus against the pinned release
161
+ run: npm run test:eval-quality-corpus
162
+
157
163
  - name: Check the eval contracts against the sources they are generated from
158
164
  run: npm run test:contract-sources
159
165
 
@@ -175,6 +181,9 @@ jobs:
175
181
  - name: Probe every TEA command target through the eval-quality adapter
176
182
  run: npm run test:probe-targets
177
183
 
184
+ - name: Check TEA's branches are total over the published port vocabularies
185
+ run: npm run test:port-totality
186
+
178
187
  - name: Test changelog stamping
179
188
  run: npm run test:changelog
180
189
 
package/CHANGELOG.md CHANGED
@@ -7,6 +7,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - `npm run test:eval-quality-corpus` compiles `eval-quality`'s own published corpus against the pinned release, before any TEA artifact is asked to run on it.
13
+ TEA's every other check feeds the package TEA's own bytes, so a package regression and a bad migration read identically, and this upgrade moved 31 probes, 10 contracts and both command policies at once.
14
+ This check feeds the package nothing of TEA's. The `./corpus/*` subpath ships `corpus/dev/index.json`, which names 27 entries with a `sha256:` digest each: 24 contracts under `contracts/`, the compile-and-seal example's contract and its sealed brief, and the corpus README. The index also records the failure code each of the three deliberately-refused contracts raises, and it carries no digest of itself, which is the one corpus file it cannot list.
15
+ So the corpus states the expected result and the package computes the observed one. Measured against 3.0.0: 27 entry digests match the shipped bytes, 22 contracts compile, 3 are refused with the code the index records, and the sealed brief the example produces is byte for byte the shipped `brief.json`.
16
+ It also holds the resolved version against the exact pin in `package.json`, so a tree that is not the tree the repository declares fails here rather than somewhere downstream.
17
+ Exit 1 is a moved status, a moved digest or a moved brief; exit 2 is a package, subpath or index that could not be read, an index naming no contract, or a tree resolving a version other than the pin. So an unresolvable install, an empty corpus and a tree nobody installed are each reported as nothing measured rather than as a package regression.
18
+ - `npm run test:port-totality` holds TEA's branches against the port vocabularies the installed package declares.
19
+ `ProbeRequest` and `ProbeObservation` are three-member unions tagged by `kind` and the conformance surface publishes six arms, and TEA is CommonJS with no typechecker, so no compiler and no reviewer will notice the day a fourth member arrives: the failure mode is code that keeps working on the members it knows.
20
+ Every member is now handled or declined with the reason recorded, every arm is run or recorded as not yet run, and a member or arm in neither fails the check by name.
21
+ `probeCommand` narrows every observation to the `cli` member and raises a named error otherwise, which the check proves by calling the narrowing with the two members TEA declines.
22
+ Three of the four harnesses reach the port through `probeCommand`; `test/eval-contract-strength.js` caches every observation itself, so it narrows at its own boundary, on the live answer and on the cached one.
23
+ The narrowing throws outside the fault handler on purpose: a member TEA cannot read is a defect in TEA, and classifying it as an environment failure would file that defect as a lost run.
24
+
25
+ - `bmad-tea` has a behavioral eval suite and is no longer declared `deferred` in `test/evals/suite-manifest.json`. It was the only TEA skill with no eval of any kind, and the one thing it does is the step every workflow runs after: turn a sentence a user wrote into one item on its ten-item menu. A wrong route is the most expensive mistake the module can make and the cheapest one to miss, because every artifact downstream of it is well-formed, cites the repository, and answers a question nobody asked. `test/eval-bmad-tea-routing.js` puts eighteen intents to the skill through `cli/routing-runner.js`, a new `read-only` command registered as `tea-routing-runner`: ten with a single right menu item, four where two or more items are genuinely close and the right answer is to ask, and four nothing on the menu serves. Each ambiguous intent is ambiguous from what the agent is actually shown, which is the message plus `SKILL.md` and `customize.toml`; an intent whose second reading only opens once you have read a workflow's frontmatter is not ambiguous to the agent, and scoring it as a clarification would measure a fact it was never given.
26
+ - Two of the three answers are Step 8 of `src/agents/bmad-tea/SKILL.md` verbatim and the third is not, which is worth stating rather than glossing. Step 8 says to dispatch a clear match directly and to pause only when two or more items are genuinely close, so `route` and `clarify` are the skill's own rules. Its third clause says that when nothing on the menu fits the agent should just continue the conversation, and names chat, clarifying questions and `bmad-help` as fair game: it forbids a dispatch and asks for no refusal. The requirement that an unservable intent be declined and that the answer say what is missing is the suite's own exit condition, and the prompt states it outright, so the skill is scored against a rule it was handed rather than one inferred from text that does not carry it. The skill itself is not edited.
27
+ - Five metrics and four counts, and three of the counts are the controls. `routeAccuracy`, `reasonTokenRecall`, `scopeFidelity`, `clarifyRecall` and `declineRecall` measure the decision, the stated reason, the scope the user named, the clarification and the decline. `maxConfidentRoutesOnUnservable`, `maxConfidentRoutesOnAmbiguous` and `maxUnroutedClearIntents` are all zero and they close the cells recall leaves open: a skill that guesses on everything fails the first two, and one that asks about everything fails the third, which Step 8 rules out by name as a confirmation ritual. `maxUnstableCases` is two of eighteen at two repetitions. Every threshold sits on a value its denominator can actually produce, because a threshold between two reachable values is not the gate it reads as, and every one of them except stability has a stored replay case that fails it and nothing else. Stability cannot have one, since it is a property of a pair of repetitions and a stored case is one reply; what is pinned instead is the signature the live run measures it with.
28
+ - The stated reason is scored by token containment and never by judgment, and every token has to be a word of the user's own message. The harness refuses one that is not, and that rule is what makes the number mean anything: the prompt asks the agent to name the deciding feature in the message's own words, so a token reachable only from the menu row it is being scored for choosing could be satisfied by echoing that row without reading the message, and a token in neither place would make the threshold unreachable for a skill doing exactly what it was told. `test/eval-bmad-tea-routing.js` owns the one pattern rule and `tools/generate-contracts.js` writes the same pattern sources into the contracts, so `npm run test:contract-oracles` compares an oracle and a scorer that cannot spell it two ways. Scope is held to containment and to a length bound together, because the same prompt asks for the scope in the message's own words and echoing the whole message would otherwise satisfy any token drawn from it.
29
+ - The answers never reach the agent. `test/fixtures/tea-routing-eval/intents.json` holds the eighteen messages and is the only file the prompt carries; `ground-truth.json` holds the expected action, menu code, workflow, deciding tokens, scope tokens and clarification candidates. Every assembled prompt is searched for each ground-truth-only key and for the file's own bytes before a model call is spent, and that check runs under `--validate-only`, so it is in the pull-request gate rather than only in the runs that cost money. Every expected menu code is checked against the live `src/agents/bmad-tea/customize.toml`, so editing the menu fails the corpus instead of quietly invalidating it.
30
+ - Seventeen stored replies under `test/replay/bmad-tea-routing/` score with no model call, and they are the only part of the suite the deterministic gate can run end to end. They cover a correct route, a wrong menu code, a route whose scope is the whole message copied back, a grounded route with an ungrounded reason, a route that dropped the epic the user named, a `GATE` dispatch that named a workflow behind a menu item whose target is a prompt, a route whose reason is written the way a person writes rather than as a list of the fixture's tokens, a clarification that names nothing to choose between, one that names all three candidates, one that names both by menu label and never by code, a confident route on a genuinely ambiguous intent, a confident route on an unservable one, a decline that says what is missing and one that does not, a clear intent left unrouted, an answer inside a fenced block, and a reply carrying no answer at all. Every stored result was derived by hand.
31
+ - Two eval contracts and two probe corpora, generated. The suite is expressed as `tea-routing-intents.contract.json` and `tea-routing-controls.contract.json` because `eval-quality`'s AD-39 scripting bound caps an interaction plan at sixteen steps and the corpus is eighteen intents, one step each; the manifest's `contracts` field is an array for exactly this. Both bind standard input as a literal prompt per step, which is what tells eighteen steps declaring one operation apart, and what the pair costs in bytes. Seventy-two oracles between them read the action, the menu code, the workflow behind it, the stated reason, the scope and the clarifying question, and every one of them was seen resolving false on some answer as well as true on another. The probe corpora carry a gameability probe and a clean control each and no defect probe: a defect probe needs a controlled mutation of the system under test, and the system here is the skill, which this suite must not edit.
32
+
33
+ ### Changed
34
+
35
+ - `eval-quality` moves from 1.4.0 to 3.0.0.
36
+ The pin alone left four checks red, which is why every migration below lands with it.
37
+ - Every `CommandTargetPolicy` TEA builds declares `permittedEnvironmentKeys`, required on 3.0.0 with no default.
38
+ The environment channel was the one command channel with no operator bound: the contract author declared it and everything declared reached the process.
39
+ The list is stated per target rather than once for every command TEA ships, because a key a command never reads has no business reaching it. All three permit exactly what their own contracts declare, the vendor names plus `HOME` and `USER`, read from `vendorEnvironmentNames()` so the contract and the policy cannot drift.
40
+ `PATH` is on no list, refused where the widening happens as well as by the schema and by the adapter, because `target` may name a bare command and a declared `PATH` would then choose which binary runs.
41
+ A per-interface override naming an interface the policy does not authorize is refused too: it widens nothing, and unnoticed it leaves every request of that harness carrying names the authorization never permitted.
42
+ A caller widens an authorization one call at a time through `environmentKeys`, which is how an operator's `--env-pass` names and this repository's two stub variables reach a process.
43
+ - `CI` reaches no measured run, and its absence is what makes a review reproducible.
44
+ `cli/test-review.js` reads it to decide filesystem isolation when `--isolate` is not stated, and no contract declares it, so passing it through let the host decide how the measured run executed: isolation on in GitHub Actions, off on a laptop, with the two sealed records indistinguishable afterwards.
45
+ TEA's harness had been sending it since before the environment channel had an operator bound, and `test/eval-contract-strength.js` had been filtering it back out against the contract, so the two halves of the same suite were already measuring different commands.
46
+ A command run through the port now sees no `CI` at all and isolates the same way on every host; `test/eval-test-review.js` states `--isolate` explicitly and never depended on the variable.
47
+ `test/test-probe-targets.js` holds each contract's declared keys equal to its authorization's permitted keys, in both directions, so neither side can drift from the other again.
48
+ - All 31 probes carry `schemaVersion: 5` and all 10 contracts carry `5`, written by their generators rather than by hand.
49
+ On 3.0.0 a stale stamp is a `schema-version-mismatch` runtime fault at exit 5 in both `preflight` and `score`, so every probe corpus and every contract compile was failing on the version alone.
50
+ The load-bearing edit is the ninth input channel: each of the 21 `inputBinding` selectors gains `"arguments": null`, which version 5 added so a signature against a tool call can filter on what the call supplied.
51
+ `tools/generate-contracts.js` now names its stamp `EVAL_CONTRACT_SCHEMA_VERSION` instead of repeating the literal in three places.
52
+ - TEA's sealed run records carry `schemaVersion: 6` and no `invalidReason`, and their observations carry the ninth `arguments` call-input channel.
53
+ Version 6 drops `invalidReason` because nothing in the package ever read it, so a caller attesting that a run was invalid was ignored by every stage while the field looked like a supported channel.
54
+ `SCHEMA_VERSIONS` in `test/lib/eval-quality-inputs.js` read 3 for both the probe and the record; the record entry is the one with a reader, and it was emitting version-3 records against a version-6 parser.
55
+ - The command-probe conformance arm reports 16 outcomes where it reported 15. The count is the package's and moved with the version; what TEA supplies is `unauthorizedEnvironmentKeyRequest`, which is what makes the sixteenth assertion pass rather than fail.
56
+ That request is authorized in every other respect and declares exactly one environment key its authorization omits, and the suite checks that the refusal costs zero calls into the underlying mechanism, so no process is spawned.
57
+ TEA reads the expected count from `CONFORMANCE_OUTCOME_COUNTS` rather than stating it, so the next assertion the package adds fails this check until TEA answers it.
58
+ The conformance policy permits no environment key at all, which is both the honest allowlist for a fixture command that reads none and AD-35's default-deny base case.
59
+
60
+ - Scores computed before and after this release are comparable on every input, with one exception that nothing recorded depends on.
61
+ Every replay-scored result is comparable across this change, and none of them moved.
62
+ `npm run test:probe-corpus` recomputes each probe's pre-flight, verdict, exit code, basis, qualification and strength vector from the stored evidence under 3.0.0, against `test/probes/expected-strength.json` unchanged by this release.
63
+ The probe and contract bytes moved, so `corpusDigest` and `contractDigest` move with them at scoring time, and nothing under `test/replay/` pins either or pins a scoring version, which is why no stored score moved.
64
+ No digest stored inside a probe or a contract moved either: those are taken over the harness implementation and its artifacts rather than over the files themselves.
65
+ The live `eval:test-review` numbers in `docs/explanation/eval-quality-roadmap.md` are comparable.
66
+ That harness states `--isolate` explicitly on every run, so its filesystem isolation never depended on `CI`, before or after.
67
+ A live `eval:preflight` or `eval:contract-strength` measurement of `tea-test-review` taken on a host where `CI` was set is not comparable with one taken after this release, because such a run isolated where this one does not.
68
+ No such measurement is recorded here: no workflow invokes any `eval:` script, the pre-flight cache under `test/eval-artifacts/` is not committed, and every contract-strength result this repository records is a finding rather than a figure.
69
+ So nothing recorded is invalidated; what changes is that the next contract-strength run and the next eval run measure the same command on every host.
70
+ `tea-fragment-selection-runner` and `tea-trace-runner` stop carrying `CI` as well, and nothing observable moves.
71
+ Only `cli/test-review.js` reads it, and `buildMinimalEnv` in `cli/lib/run-agent.js` never passes it to a vendor, so no measured run of those two could have read it.
72
+
73
+ ### Fixed
74
+
75
+ - Two statements in `docs/explanation/eval-quality-roadmap.md` and `docs/explanation/eval-quality-command-adapter.md` that this upgrade falsified or that were already false.
76
+ The roadmap counted the published conformance suite at fifteen assertions, which is sixteen now.
77
+ The adapter page said `tea-test-review` permits neither `HOME` nor `USER`, so its live pre-flight must use an API key. That stopped being true one release earlier when its request shape moved to `vendorEnvironmentNames()`, and the same page recorded the correction two hundred lines further down while the first statement stayed, so the page contradicted itself.
78
+ The page's adoption inventory also listed `serializeArtifact` and `digestBytes` as unused, which the new corpus check falsifies, and its "done, and covered by npm test" section named none of the three checks this change adds.
79
+
80
+ - `npm run test:ci-coverage` now reconciles the CLI job's shard partition against the suites it splits, and a deliberately dropped suite number proves it fires. That job runs `test/test-test-review-cli.js` across three parallel shards by suite number, and the three lists were hardcoded in `.github/workflows/quality.yaml` with nothing holding their union to the suites the file defines. A suite added to the file and to no list is enabled in none of the three: every shard prints "skipped", all three jobs go green, and the suite runs nowhere at all. The file's own 1-to-13 bound catches a number that is not a suite and cannot catch a suite that is in no list. The check reads the suite numbers off the file's own `suiteEnabled()` call sites and fails in both directions. Found incidentally while the positional assertion below was being fixed; it is the same defect one layer up, with a worse blast radius.
81
+ - `test/test-test-review-cli.js` asserted `eval:all`'s invocation list by position, indexing `[0]` and `[1]` for the two suites that declare repetition overrides. Nothing held that order. It passed for three suites by accident of the order they were added in, and the first suite whose id sorted ahead of `fragment-selection` broke an assertion that is not about order at all. Both now look the suite up by id. Found while adding the routing suite, whose id does sort ahead of it.
82
+ - Seventeen stale counts and claims in prose, every one of which this change would have made staler. `npm test` chains twenty-five checks and the README said twenty-one; `test/contracts/` holds twelve contracts and three files said ten; `test/probes/` holds thirty-five probes across twelve corpora and two files said thirty-one across ten; the replay corpus holds forty-four cases and three files said twenty-seven; TEA ships four commands and four harnesses and five statements said three; and the README, the adoption guide and the roadmap each said that every suite had been measured live, which stops being true the moment a suite is added. These are hand-maintained counts and they will go stale again. FR23 and Story 4.1 are what end that; this is not it.
83
+
10
84
  ## [1.26.0] - 2026-09-09
11
85
 
12
86
  ### Changed
package/README.md CHANGED
@@ -362,7 +362,7 @@ cli/ # tea-test-review: the headless CI gate
362
362
  docs/ # source of truth for the docs site
363
363
  website/ # Astro + Starlight, consumes docs/ through a symlink
364
364
  tools/ # validators, doc build, changelog stamping
365
- test/ # quality gate suites and the three eval harnesses
365
+ test/ # quality gate suites and the four eval harnesses
366
366
  ```
367
367
 
368
368
  ## How TEA Keeps Itself Honest
@@ -392,7 +392,7 @@ A passing fragment-selection eval means the workflow loaded the right knowledge.
392
392
 
393
393
  ### Deterministic Checks
394
394
 
395
- `npm test` chains twenty-one checks. That count covers the whole chain: every entry in it is deterministic and credential-free, so the chain and its credential-free subset are the same list. `npm run test:ci-coverage` derives the count from `package.json` and prints it. Five of the twenty-one keep the rules, guidance, hook, eval data, and eval contracts aligned:
395
+ `npm test` chains twenty-five checks. That count covers the whole chain: every entry in it is deterministic and credential-free, so the chain and its credential-free subset are the same list. `npm run test:ci-coverage` derives the count from `package.json` and prints it. Five of the twenty-five keep the rules, guidance, hook, eval data, and eval contracts aligned:
396
396
 
397
397
  - `test:criteria-fragments` fails when a registry row is neither mapped to a knowledge fragment nor declared a known gap. A rule the reviewer scores but no fragment teaches is a rule TEA punishes without ever having explained it. All 36 rows are currently mapped across 50 anchors. Because the declared-gap list is empty, the validator feeds itself a synthetic unmapped row on every run to prove that path still works.
398
398
  - `test:enforce-hook` fails when a new Absolute registry row appears in neither the hook's enforced list nor its deferred list. This prevents a rule from being added without an explicit write-time enforcement decision.
@@ -430,7 +430,7 @@ npm run eval:all -- --agent claude --preflight-only
430
430
  npm run eval:all -- --agent agy --preflight-only
431
431
  ```
432
432
 
433
- Output ending with `nothing measured` is expected in preflight mode. It means the static eval data is valid and, for every one of the three suites, the selected executable is on `PATH`, answers `--version`, and a built-in vendor has a credential. Some runners cannot expose session authentication to this probe, so a preflight pass does not guarantee that the later live call will authenticate. The flag intentionally exits before launching the agent. `npm run test:eval-schemas` runs each suite's preflight with a missing runner and with a present one, so a preflight that stopped probing the runner would fail `npm test`.
433
+ Output ending with `nothing measured` is expected in preflight mode. It means the static eval data is valid and, for every one of the four suites, the selected executable is on `PATH`, answers `--version`, and a built-in vendor has a credential. Some runners cannot expose session authentication to this probe, so a preflight pass does not guarantee that the later live call will authenticate. The flag intentionally exits before launching the agent. `npm run test:eval-schemas` runs each suite's preflight with a missing runner and with a present one, so a preflight that stopped probing the runner would fail `npm test`.
434
434
 
435
435
  ### A La Carte Live Evals
436
436
 
@@ -542,16 +542,17 @@ npm run eval:fragment-selection -- \
542
542
 
543
543
  ### What Has to Pass
544
544
 
545
- Every row below is a declared gate. All three suites were measured live for the first time on 2026-09-08 against `claude`/`sonnet`, and `docs/explanation/eval-quality-roadmap.md` records what each one returned; no result artifact is committed, so quote that section rather than a number from memory. The live thresholds are the ones `test/evals/suite-manifest.json` declares, and `npm run test:eval-schemas` fails when a harness constant and the manifest disagree.
545
+ Every row below is a declared gate. The three suites that existed then were measured live for the first time on 2026-09-08 against `claude`/`sonnet`, and `docs/explanation/eval-quality-roadmap.md` records what each one returned; `bmad-tea-routing` has been added since and has never been run live, so its row is a declared target and nothing more; no result artifact is committed, so quote that section rather than a number from memory. The live thresholds are the ones `test/evals/suite-manifest.json` declares, and `npm run test:eval-schemas` fails when a harness constant and the manifest disagree.
546
546
 
547
- | Eval | Declared threshold | Declared volume |
548
- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
549
- | `npm run eval:all -- --agent ...` | All three live suites below meet their thresholds for the selected runner | 48 selections, 3 reviews, and 4 traces |
550
- | Fragment selection | At least 90% required-fragment recall, at most 10% forbidden-fragment selection, and stable choices across repeated cases | 24 cases twice: 48 calls |
551
- | Test review | At least 70% overall recall, 100% CRITICAL recall, an 80% non-false-positive rate, score standard deviation no higher than 3, and a stable verdict | Three complete reviews |
552
- | Trace | At least 90% criterion-status accuracy and 90% evidence-citation precision; 100% on both discriminating criteria, the gate decision, the gate criteria, the coverage arithmetic, the oracle resolution, the rejected evidence, the waiver validity, and the live evidence; no clean-set false positive, no unstable case, and no fixture mutation | Two fixture sets twice: 4 calls |
553
- | `npm run test:eval-data` | Every case references valid workflow files and indexed fragments; required and forbidden sets do not overlap | No agent calls |
554
- | `npm run test:eval-schemas` | The suite manifest matches its schema, declares the thresholds the harnesses apply, and accounts for every TEA skill | No agent calls |
547
+ | Eval | Declared threshold | Declared volume |
548
+ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
549
+ | `npm run eval:all -- --agent ...` | All four live suites below meet their thresholds for the selected runner | 48 selections, 3 reviews, 4 traces, 36 routing calls |
550
+ | Fragment selection | At least 90% required-fragment recall, at most 10% forbidden-fragment selection, and stable choices across repeated cases | 24 cases twice: 48 calls |
551
+ | Test review | At least 70% overall recall, 100% CRITICAL recall, an 80% non-false-positive rate, score standard deviation no higher than 3, and a stable verdict | Three complete reviews |
552
+ | Trace | At least 90% criterion-status accuracy and 90% evidence-citation precision; 100% on both discriminating criteria, the gate decision, the gate criteria, the coverage arithmetic, the oracle resolution, the rejected evidence, the waiver validity, and the live evidence; no clean-set false positive, no unstable case, and no fixture mutation | Two fixture sets twice: 4 calls |
553
+ | bmad-tea routing | At least 90% route accuracy, 80% reason-token recall, 87.5% scope fidelity, and 75% on both clarification and decline recall; zero confident routes on an intent nothing on the menu serves, zero on one whose menu items are genuinely close, zero clear intents left unrouted, and at most two intents whose answer changes between repetitions | 18 intents twice: 36 calls |
554
+ | `npm run test:eval-data` | Every case references valid workflow files and indexed fragments; required and forbidden sets do not overlap | No agent calls |
555
+ | `npm run test:eval-schemas` | The suite manifest matches its schema, declares the thresholds the harnesses apply, and accounts for every TEA skill | No agent calls |
555
556
 
556
557
  Every declared repetition has to complete. A run that loses one to a timeout, a transport error, or an unparseable reply cannot measure variance or stability, so it exits `2` rather than reporting a lower score.
557
558
 
@@ -561,7 +562,7 @@ The non-false-positive rate is the share of reported findings that are not defin
561
562
 
562
563
  `test/evals/suite-manifest.json` registers every suite `eval:all` runs, with its skills, fixtures, ground truth, contract, thresholds, repetition count, CI tier, and the capabilities its runner needs. It also carries a `deferred` list: one entry per skill with no behavioral suite, naming the owner, the missing evidence, and the condition that retires the entry. `eval:all` reads the suite list from that file and refuses to run when a TEA skill appears in neither list.
563
564
 
564
- Add `--json <path>` to any of the three harnesses to write a result record alongside the console output:
565
+ Add `--json <path>` to any of the four harnesses to write a result record alongside the console output:
565
566
 
566
567
  ```bash
567
568
  npm run eval:all -- --agent codex --json results/eval-all.json
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The routing answer out of a runner reply.
3
+ *
4
+ * Lives here rather than inside the harness for the reason cli/lib/parse-selection.js
5
+ * records: `cli/routing-runner.js` has to accept exactly the spellings
6
+ * `test/eval-bmad-tea-routing.js` and `test/test-eval-replay.js` score, and two
7
+ * copies of that rule would let a fenced-block reply be accepted by the runner
8
+ * and rejected by the scorer. This file owns how a reply is parsed and nothing
9
+ * about how one is scored; the pattern rules the oracles share live in
10
+ * `test/eval-bmad-tea-routing.js`.
11
+ *
12
+ * Returns null when nothing parseable came back, so "the reply was prose" never
13
+ * reports as "the agent declined", which is a real and very different answer.
14
+ * Everything past `action` is normalized rather than rejected: a reply that names
15
+ * an action and forgets to say why is a measurement of a bad routing answer, and
16
+ * a reply that is not a routing answer at all is a broken pipe. Collapsing the
17
+ * two would let a parser change show up as a quality score.
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ /** The three answers Step 8 of src/agents/bmad-tea/SKILL.md allows. */
23
+ const ROUTING_ACTIONS = ['route', 'clarify', 'decline'];
24
+
25
+ /** @returns {string|null} A trimmed non-empty string, or null for anything else. */
26
+ function text(value) {
27
+ if (typeof value !== 'string') return null;
28
+ const trimmed = value.trim();
29
+ return trimmed.length === 0 ? null : trimmed;
30
+ }
31
+
32
+ /**
33
+ * @param {string} stdout - The runner reply, in full.
34
+ * @returns {{action: string, menuCode: string|null, workflow: string|null, scope: string|null, reason: string|null, question: string|null, missing: string|null}|null}
35
+ */
36
+ function parseRouting(stdout) {
37
+ const source = String(stdout || '');
38
+ const candidates = [];
39
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/g;
40
+ for (const match of source.matchAll(fenced)) candidates.push(match[1]);
41
+ const braced = /\{[\s\S]*"action"[\s\S]*\}/.exec(source);
42
+ if (braced) candidates.push(braced[0]);
43
+ candidates.push(source);
44
+
45
+ for (const candidate of candidates) {
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(candidate.trim());
49
+ } catch {
50
+ continue;
51
+ }
52
+ const action = text(parsed?.action)?.toLowerCase();
53
+ if (action === undefined || !ROUTING_ACTIONS.includes(action)) continue;
54
+ return {
55
+ action,
56
+ menuCode: text(parsed?.menuCode)?.toUpperCase() ?? null,
57
+ workflow: text(parsed?.workflow),
58
+ scope: text(parsed?.scope),
59
+ reason: text(parsed?.reason),
60
+ question: text(parsed?.question),
61
+ missing: text(parsed?.missing),
62
+ };
63
+ }
64
+ return null;
65
+ }
66
+
67
+ module.exports = { ROUTING_ACTIONS, parseRouting };
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tea-routing-runner — the command the `bmad-tea` routing contract names.
4
+ *
5
+ * `bmad-tea` is the agent a user meets first, and until this suite existed
6
+ * nothing measured the only thing it does: turn a sentence a person wrote into
7
+ * one item on its menu. The whole surface is one turn:
8
+ *
9
+ * prompt on standard input -> {"action": "...", "menuCode": "...", ...} on standard output
10
+ *
11
+ * which is what the contract's operation declares: `stdin` carries the one
12
+ * required key, `descriptorChannel` is the stdout stream, and the response
13
+ * descriptor requires `action` and `reason` and permits the five optional keys
14
+ * beside them. The prompt stays the harness's to assemble, because it is built
15
+ * from the skill's own SKILL.md and customize.toml plus one corpus intent, and
16
+ * those belong to the eval corpus rather than to this command.
17
+ *
18
+ * The agent call is TEA's own (cli/lib/run-agent.js), so vendor argv, the
19
+ * minimal child environment, the model pin, and the credential shape are decided
20
+ * in the one place that already decides them for the other three commands. This
21
+ * file adds no vendor knowledge. It declares the one capability a routing
22
+ * decision needs, `read-only`: the answer is a JSON object on standard output
23
+ * and nothing about it requires a file, so claude runs with no write tool and
24
+ * codex under its read-only sandbox, which is what the suite declares in
25
+ * test/evals/suite-manifest.json.
26
+ *
27
+ * Two nested wall clocks are in play when this runs behind eval-quality's
28
+ * command-line adapter, and the inner one has to be the shorter of the two:
29
+ * --timeout-ms bounds the vendor call and reports a timeout as exit 5, while the
30
+ * adapter's own maxElapsedMs SIGKILLs this process and reports `budget-exhausted`
31
+ * with no exit code at all. A run that hits the outer bound first loses the
32
+ * classification the inner one would have produced. test/lib/probe-targets.js
33
+ * sets the outer bound a minute above the harness's own for that reason.
34
+ *
35
+ * Usage:
36
+ * tea-routing-runner --agent codex < prompt.txt
37
+ * tea-routing-runner --agent custom --agent-cmd ./my-runner < prompt.txt
38
+ *
39
+ * Exit codes: 0 a routing answer was produced, and the classes in
40
+ * cli/lib/runner-exit-codes.js otherwise. They are the exit codes because the
41
+ * caller is a probe: the adapter records an exit code as an observation and never
42
+ * as a fault, so the code is the only channel that survives the boundary intact.
43
+ * The table is shared with the other runner commands so all of them spell one
44
+ * class with one number.
45
+ */
46
+
47
+ 'use strict';
48
+
49
+ const fs = require('node:fs');
50
+ const { Command } = require('commander');
51
+
52
+ const { AGENT_ADAPTERS } = require('./lib/agent-adapters');
53
+ const { parseRouting } = require('./lib/parse-routing');
54
+ const { runAgent } = require('./lib/run-agent');
55
+ const { EXIT_CODES, classOfAgentError, failureClassForExit, vendorEnvironmentNames } = require('./lib/runner-exit-codes');
56
+
57
+ /** The same default the other three commands declare, so this command changes no run that omits `--agent`. */
58
+ const DEFAULT_AGENT = 'claude';
59
+
60
+ /** Five minutes, matching RUN_TIMEOUT_MS in test/eval-bmad-tea-routing.js, which is the only caller that measures. */
61
+ const DEFAULT_TIMEOUT_MS = 5 * 60_000;
62
+
63
+ /** A routing decision is a reply, so the runner needs to write nothing; see the header. */
64
+ const RUNNER_CAPABILITIES = ['read-only'];
65
+
66
+ /**
67
+ * The request shape this command accepts, as the contract generator needs to
68
+ * declare it.
69
+ *
70
+ * Read by tools/generate-contracts.js rather than transcribed there, which is
71
+ * the rule test/contracts/README.md records for every declaration a command
72
+ * owns. The permitted environment names are the vendor variables plus HOME and
73
+ * USER; cli/lib/runner-exit-codes.js states why those two are load-bearing.
74
+ */
75
+ const ROUTING_REQUEST_KEYS = {
76
+ argument: { required: [], permitted: [] },
77
+ option: {
78
+ required: ['agent'],
79
+ permitted: ['agent', 'agent-cmd', 'agent-arg', 'env-pass', 'model', 'timeout-ms'],
80
+ },
81
+ environment: { required: [], permitted: vendorEnvironmentNames() },
82
+ stdin: { required: ['prompt'], permitted: ['prompt'] },
83
+ };
84
+
85
+ /**
86
+ * The response shape this command prints, as the contract generator needs to
87
+ * declare it.
88
+ *
89
+ * `action` and `reason` are required because an answer with neither is not a
90
+ * routing decision. The other five are permitted and not required because which
91
+ * of them carries the answer depends on the action: a route names a menu code, a
92
+ * clarification names a question, a decline names what is missing, and forcing
93
+ * all three onto every reply would make an empty string the normal way to say
94
+ * "not applicable".
95
+ */
96
+ const ROUTING_RESPONSE_KEYS = {
97
+ required: ['action', 'reason'],
98
+ permitted: ['action', 'menuCode', 'workflow', 'scope', 'reason', 'question', 'missing'],
99
+ };
100
+
101
+ function collect(value, previous) {
102
+ return [...previous, value];
103
+ }
104
+
105
+ /** Everything this process prints on stderr is diagnostic; stdout carries the routing answer alone. */
106
+ function fail(failureClass, message) {
107
+ process.stderr.write(`tea-routing-runner: ${message}\n`);
108
+ process.exit(EXIT_CODES[failureClass]);
109
+ }
110
+
111
+ function readPrompt() {
112
+ try {
113
+ return fs.readFileSync(0, 'utf8');
114
+ } catch (error) {
115
+ fail('usage', `could not read the prompt from standard input: ${error.message}`);
116
+ return '';
117
+ }
118
+ }
119
+
120
+ function main(argv) {
121
+ const program = new Command();
122
+ program
123
+ .name('tea-routing-runner')
124
+ .description("Ask a headless agent which bmad-tea menu item a user's intent belongs to, and print the answer as JSON.")
125
+ .option('--agent <name>', `agent adapter (${Object.keys(AGENT_ADAPTERS).join('|')})`, DEFAULT_AGENT)
126
+ .option('--agent-cmd <path>', 'executable override for the selected adapter')
127
+ .option('--agent-arg <arg>', 'extra argument appended to the agent CLI argv (repeatable)', collect, [])
128
+ .option('--env-pass <NAME>', 'environment variable name allowed through to the agent (repeatable)', collect, [])
129
+ .option('--model <name>', 'model to pin for this run; defaults to the adapter default')
130
+ .option('--timeout-ms <n>', 'agent wall-clock timeout in milliseconds', String(DEFAULT_TIMEOUT_MS));
131
+
132
+ program.exitOverride();
133
+ try {
134
+ program.parse(argv);
135
+ } catch (error) {
136
+ // commander's own --help and --version exits are not usage errors.
137
+ if (error.exitCode === 0) return;
138
+ fail('usage', error.message);
139
+ }
140
+
141
+ const options = program.opts();
142
+ if (!Object.hasOwn(AGENT_ADAPTERS, options.agent)) {
143
+ fail('environment-configuration', `unknown agent "${options.agent}"; expected one of ${Object.keys(AGENT_ADAPTERS).join(', ')}`);
144
+ }
145
+ const timeout = Number.parseInt(options.timeoutMs, 10);
146
+ if (!Number.isInteger(timeout) || timeout <= 0) {
147
+ fail('usage', `--timeout-ms must be a positive integer; got ${JSON.stringify(options.timeoutMs)}`);
148
+ }
149
+
150
+ const prompt = readPrompt();
151
+ if (prompt.trim().length === 0) {
152
+ fail('usage', 'the prompt is empty; this command reads the complete prompt from standard input');
153
+ }
154
+
155
+ let stdout = '';
156
+ try {
157
+ ({ stdout } = runAgent(prompt, {
158
+ agent: options.agent,
159
+ agentCommand: options.agentCmd,
160
+ agentArgs: options.agentArg,
161
+ envPass: options.envPass,
162
+ model: options.model,
163
+ timeout,
164
+ cwd: process.cwd(),
165
+ capabilities: RUNNER_CAPABILITIES,
166
+ }));
167
+ } catch (error) {
168
+ fail(classOfAgentError(error), error.message);
169
+ }
170
+
171
+ const routing = parseRouting(stdout);
172
+ if (routing === null) {
173
+ fail('environment-parser', 'the agent reply carries no routing answer');
174
+ }
175
+
176
+ // Exactly the response descriptor's key set, and nothing beside it, for the
177
+ // reason tea-fragment-selection-runner gives: a diagnostic field added here
178
+ // would fail the contract it exists to satisfy.
179
+ process.stdout.write(`${JSON.stringify(routing)}\n`);
180
+ }
181
+
182
+ // Guarded so tools/generate-contracts.js can read the declarations above without
183
+ // running a command, the same way it reads VERDICT_KEYS out of cli/test-review.js.
184
+ if (require.main === module) {
185
+ main(process.argv);
186
+ }
187
+
188
+ module.exports = {
189
+ DEFAULT_AGENT,
190
+ DEFAULT_TIMEOUT_MS,
191
+ EXIT_CODES,
192
+ ROUTING_REQUEST_KEYS,
193
+ ROUTING_RESPONSE_KEYS,
194
+ RUNNER_CAPABILITIES,
195
+ classOfAgentError,
196
+ failureClassForExit,
197
+ };
@@ -26,7 +26,7 @@ TEA's state today, as `test/evals/suite-manifest.json` registers it:
26
26
 
27
27
  Two of the ten skills, `bmad-tea` and `bmad-teach-me-testing`, have no suite of any kind. They carry deferred entries in the manifest naming their owner, their missing evidence, and the condition that retires the entry, because a suite list with a skill quietly missing from it reads as coverage.
28
28
 
29
- All three suites were measured live for the first time on 2026-09-08 and all three were green. No result artifact is committed, so the numbers live in `docs/explanation/eval-quality-roadmap.md` and any claim about them needs a fresh measurement.
29
+ The three suites that existed then were measured live for the first time on 2026-09-08 and all three were green. No result artifact is committed, so the numbers live in `docs/explanation/eval-quality-roadmap.md` and any claim about them needs a fresh measurement. The `bmad-tea-routing` suite was added afterwards and has never been run live, so it has a declared threshold and no measurement behind it.
30
30
 
31
31
  ## 1. What you need before you start
32
32
 
@@ -7,7 +7,7 @@ description: 'Why TEA probes every measured command through eval-quality, what t
7
7
 
8
8
  TEA measures a skill by running a command and reading what it wrote. Every harness owned that mechanism itself: its own `spawnSync`, argv, timeout, and `existsSync` plus `JSON.parse` over the file the run produced. Three copies, disagreeing about what a command may do, none capping output.
9
9
 
10
- `eval-quality` 1.0.0 ships that mechanism as `createCommandLineAdapter`, a real `EnvironmentProbePort` over a child process with 15 conformance outcomes behind it. TEA uses it and deletes what it invented. See the [roadmap](./eval-quality-roadmap.md) for the surrounding plan and `test/contracts/README.md` for the contracts.
10
+ `eval-quality` 1.0.0 ships that mechanism as `createCommandLineAdapter`, a real `EnvironmentProbePort` over a child process with a conformance arm behind it, 16 outcomes on the 3.0.0 TEA now runs. TEA uses it and deletes what it invented. See the [roadmap](./eval-quality-roadmap.md) for the surrounding plan and `test/contracts/README.md` for the contracts.
11
11
 
12
12
  "Runner" has meant three processes here, which is most of why the boundary was unclear: the vendor agent doing the skill's work, the TEA command under evaluation, and `eval-all.js` orchestrating one child per suite. `eval-quality` has an opinion about the middle one only. It probes a declared interface and returns an observation, never launching an agent and holding no view on vendor routing or credential shape.
13
13
 
@@ -36,11 +36,11 @@ A contract names a logical executable and `ProbeRequest` enforces it: `executabl
36
36
  Two couplings the seam does not remove, both found by running it:
37
37
 
38
38
  - **`--json` and `--output` resolve against `--project-root`; the artifact map resolves against the policy `cwd`.** `test-review.contract.json`'s witness legs pass a bare `verdict.json` and name no project root, so its live pre-flight writes into whichever directory also has to satisfy its repository-relative `--files`. Those legs need an explicit project root and run-scoped artifact paths.
39
- - **The child environment is closed** to `PATH` plus what the request declares. Both vendors resolve a stored login through `HOME`, so the selection runner permits `HOME` and `USER`. `tea-test-review` does not, so its live pre-flight must use an API key.
39
+ - **The child environment is closed** to `PATH` plus what the request declares, and from `eval-quality` 3.0.0 the authorization declares which of those keys a request may carry at all. All three commands permit the vendor names plus `HOME` and `USER`, because both vendors resolve a stored login through `HOME`; `tea-test-review` permits `CI` on top of them, which it reads to decide filesystem isolation. A key outside a command's list is refused before a process spawns, and `PATH` is on no list: `target` may name a bare command, so a declared `PATH` would choose which binary runs. The adapter supplies its own.
40
40
 
41
41
  ## Reaching more than one skill
42
42
 
43
- Ten contracts declare three logical executables, each a command TEA ships. When there were nine they declared two, and one did not exist: `tea-fragment-selection-runner`, named by eight of them, was fiction, which is worse than a declared gap: the contract compiles, pre-flight schedules a leg against it, and the gate stays green over a command nobody can run.
43
+ Twelve contracts declare four logical executables, each a command TEA ships. When there were nine they declared two, and one did not exist: `tea-fragment-selection-runner`, named by eight of them, was fiction, which is worse than a declared gap: the contract compiles, pre-flight schedules a leg against it, and the gate stays green over a command nobody can run.
44
44
 
45
45
  `cli/fragment-selection-runner.js` is that command now. Its whole surface is the one turn those contracts declared: a prompt on standard input, `{"fragments": [...]}` on standard output. It builds no prompt, because a prompt belongs to the eval corpus, and knows no vendor, because it calls `runAgent`. `test/test-probe-targets.js` keeps the fiction from returning: every declared interface, executable, and subcommand path must be one the registry carries, and every registered command must be named by some contract.
46
46
 
@@ -64,22 +64,23 @@ The package has three stages: compile a contract, probe an environment, then sco
64
64
  TEA used the first two and none of the third. It uses all three now, and this section is the
65
65
  inventory, kept honest by being a list of what is still unused rather than a list of what is.
66
66
 
67
- | Published surface | TEA's use |
68
- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
- | `compile`, through the `eval-quality` binary | `npm run test:contracts` compiles all ten contracts against `test/contracts/expected-status.json` |
70
- | `createCommandLineAdapter`, `nodeCommandMechanism`, `CommandTargetPolicy` | `test/lib/probe-targets.js` maps three logical executables to three real commands |
71
- | `runPreflight` | `npm run eval:preflight` drives every contract's witness legs through the adapter for real |
72
- | `runScore` | `npm run test:probe-corpus` scores 31 probes across ten corpora; `npm run eval:contract-strength` scores them under a live pre-flight verdict |
73
- | `seal` | one sealed evaluator brief per contract, written by the same script |
74
- | `digestArtifact` | every artifact digest the run record and the isolation manifest declare |
75
- | the published JSON Schemas | `test/lib/eval-quality-inputs.js` validates every artifact TEA builds or receives against `eval-quality/schemas/*` |
76
- | `preflightFromObservations` | unused, and it cannot be used: a caller has to key its observations by leg identifier, and the leg identifiers are minted by the plan `runPreflight` builds. TEA holds a port for both halves, so the port entry point answers the same question with no ordering problem |
77
- | `validateLineageChain`, `INTERCHANGE_ARTIFACT_KEYS`, `serializeArtifact`, `digestBytes`, `digestComposite` | unused. Every TEA artifact is `revisionCount: 0` with a null parent, so there is no chain to validate, and the digest helpers TEA needs are the artifact one and its own file digest |
78
- | `eval-quality/conformance` | `npm run test:probe-conformance` runs the published command-line arm against a fixture command |
67
+ | Published surface | TEA's use |
68
+ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | `compile`, through the `eval-quality` binary | `npm run test:contracts` compiles all twelve contracts against `test/contracts/expected-status.json` |
70
+ | `createCommandLineAdapter`, `nodeCommandMechanism`, `CommandTargetPolicy` | `test/lib/probe-targets.js` maps four logical executables to four real commands |
71
+ | `runPreflight` | `npm run eval:preflight` drives every contract's witness legs through the adapter for real |
72
+ | `runScore` | `npm run test:probe-corpus` scores 35 probes across twelve corpora; `npm run eval:contract-strength` scores them under a live pre-flight verdict |
73
+ | `seal` | one sealed evaluator brief per contract, written by the same script |
74
+ | `digestArtifact` | every artifact digest the run record and the isolation manifest declare |
75
+ | the published JSON Schemas | `test/lib/eval-quality-inputs.js` validates every artifact TEA builds or receives against `eval-quality/schemas/*` |
76
+ | `preflightFromObservations` | unused, and it cannot be used: a caller has to key its observations by leg identifier, and the leg identifiers are minted by the plan `runPreflight` builds. TEA holds a port for both halves, so the port entry point answers the same question with no ordering problem |
77
+ | `validateLineageChain`, `INTERCHANGE_ARTIFACT_KEYS`, `digestComposite` | unused. Every TEA artifact is `revisionCount: 0` with a null parent, so there is no chain to validate, and the digest helper TEA needs is the artifact one |
78
+ | `serializeArtifact`, `digestBytes`, `StructuralFailure`, the `./corpus/*` subpath | `npm run test:eval-quality-corpus` seals the package's own compile-and-seal example and compares the serialized bytes with the shipped brief, digests every corpus file against the digest `corpus/dev/index.json` records, and reads a refused compile by its failure class rather than by duck-typing |
79
+ | `eval-quality/conformance` | `npm run test:probe-conformance` runs the published command-line arm against a fixture command |
79
80
 
80
81
  ### What the corpus is
81
82
 
82
- `tools/generate-probes.js` writes 31 probes from the ground truth this repository already keeps.
83
+ `tools/generate-probes.js` writes 35 probes from the ground truth this repository already keeps.
83
84
  Nine defect probes for `test-review`, one per planted registry row, each a controlled mutation whose
84
85
  target artifact, baseline-pass evidence and mutated-fail evidence are files on disk. Three for
85
86
  `trace`, one per criterion the seeded set deliberately leaves short. A clean control for every
@@ -126,8 +127,9 @@ been zero by construction whatever the reviewer did.
126
127
  `test-review.contract.json` grouped its nine plant oracles into three behaviors by severity, and each
127
128
  fragment-selection contract grouped its two oracles per case into one behavior. Both are one behavior
128
129
  per oracle now. The demand is unchanged: the same oracles, all required, at the same severities, with
129
- all ten contracts still compiling and all 611 oracle checks still agreeing with their scorers. Only
130
- the grouping moved.
130
+ all contracts still compiling and all 611 oracle checks of the day still agreeing with their scorers.
131
+ Only the grouping moved. Two routing contracts and their oracles have joined the corpus since, so the
132
+ count is a record of that change rather than a running total.
131
133
 
132
134
  `trace.contract.json` is split the same way, twenty-six behaviors for twenty-six oracles, minted from
133
135
  the oracle identifiers so `B-00n` and `O-00n` are one thing. Its authored groups still carry the
@@ -379,11 +381,14 @@ for the same correctness.
379
381
  The adapter closes the child environment to `PATH` plus what the request declares, and
380
382
  `cli/test-review.js` resolves a stored login through `HOME`, so a machine with a keychain login could
381
383
  not run that contract's own pre-flight. The shape is read from `vendorEnvironmentNames()` now, the
382
- same source the other two commands use.
384
+ same source the other two commands use, and on 3.0.0 the authorization permits exactly that list,
385
+ asserted equal in both directions by `npm run test:probe-targets`.
383
386
 
384
387
  ## Done, and owed
385
388
 
386
- Done, and covered by `npm test`: the registry, policy, port, and fault-to-failure-class mapping in `test/lib/probe-targets.js`; the runner, whose request shape and default agent `tools/generate-contracts.js` reads rather than transcribes; and `npm run test:probe-targets`, which drives all three real commands through the real adapter against checked-in fixtures with a stub vendor. It asserts default-deny, the observation shape, artifact read-back, an absent artifact, a real budget kill classified as a timeout, and contract-to-registry agreement both ways, with no model call and no credential.
389
+ Done, and covered by `npm test`: the registry, policy, port, and fault-to-failure-class mapping in `test/lib/probe-targets.js`; the runner, whose request shape and default agent `tools/generate-contracts.js` reads rather than transcribes; and `npm run test:probe-targets`, which drives every real command through the real adapter against checked-in fixtures with a stub vendor. It asserts default-deny, the observation shape, artifact read-back, an absent artifact, a real budget kill classified as a timeout, and contract-to-registry agreement both ways, with no model call and no credential.
390
+
391
+ Three more checks joined that list with the move to 3.0.0. `npm run test:probe-targets` now also holds each contract's declared environment keys equal to its authorization's permitted keys in both directions, and asserts that an unpermitted key is denied before a process spawns and that a malformed key fails at the port parse. `npm run test:eval-quality-corpus` compiles the package's own published corpus, which is the one check here that feeds the package nothing of TEA's. `npm run test:port-totality` holds TEA's branches total over both probe unions and all six published conformance arms.
387
392
 
388
393
  Also done: `eval-test-review.js`'s `runReview` and `promptDigestFromCli` drive `tea-test-review` through the port, and `eval-fragment-selection.js` runs `cli/fragment-selection-runner.js` rather than calling `runAgent` in process, so its live path and its contract describe one thing. Both were verified end to end against their stub agents, with no model call: the review harness reads a real verdict artifact back as JSON and scores it, and the selection harness scores a real reply off stdout.
389
394