@vibe-agent-toolkit/agent-skills 0.2.0-rc.2 → 0.2.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/files-config.d.ts +34 -1
  2. package/dist/files-config.d.ts.map +1 -1
  3. package/dist/files-config.js +280 -36
  4. package/dist/files-config.js.map +1 -1
  5. package/dist/fs-attribution.d.ts +45 -0
  6. package/dist/fs-attribution.d.ts.map +1 -0
  7. package/dist/fs-attribution.js +57 -0
  8. package/dist/fs-attribution.js.map +1 -0
  9. package/dist/index.d.ts +5 -4
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +8 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/inventory/detectors/declared-but-missing.d.ts +1 -1
  14. package/dist/inventory/detectors/declared-but-missing.d.ts.map +1 -1
  15. package/dist/inventory/detectors/marketplace-source-missing.d.ts +1 -1
  16. package/dist/inventory/detectors/marketplace-source-missing.d.ts.map +1 -1
  17. package/dist/inventory/detectors/present-but-undeclared.d.ts +1 -1
  18. package/dist/inventory/detectors/present-but-undeclared.d.ts.map +1 -1
  19. package/dist/inventory/detectors/reference-target-missing.d.ts +1 -1
  20. package/dist/inventory/detectors/reference-target-missing.d.ts.map +1 -1
  21. package/dist/inventory/index.d.ts +1 -1
  22. package/dist/inventory/index.d.ts.map +1 -1
  23. package/dist/inventory/index.js +1 -1
  24. package/dist/inventory/index.js.map +1 -1
  25. package/dist/inventory/serialize.d.ts +4 -4
  26. package/dist/inventory/serialize.d.ts.map +1 -1
  27. package/dist/inventory/serialize.js +6 -6
  28. package/dist/inventory/serialize.js.map +1 -1
  29. package/dist/post-build-checks.d.ts +1 -1
  30. package/dist/post-build-checks.d.ts.map +1 -1
  31. package/dist/post-build-checks.js.map +1 -1
  32. package/dist/projection/skill-extent.d.ts +247 -0
  33. package/dist/projection/skill-extent.d.ts.map +1 -0
  34. package/dist/projection/skill-extent.js +558 -0
  35. package/dist/projection/skill-extent.js.map +1 -0
  36. package/dist/schemas/agent-skill-frontmatter.js +1 -1
  37. package/dist/schemas/agent-skill-frontmatter.js.map +1 -1
  38. package/dist/skill-packager.d.ts +34 -4
  39. package/dist/skill-packager.d.ts.map +1 -1
  40. package/dist/skill-packager.js +113 -23
  41. package/dist/skill-packager.js.map +1 -1
  42. package/dist/skill-source/git-clone.d.ts.map +1 -1
  43. package/dist/skill-source/git-clone.js +9 -8
  44. package/dist/skill-source/git-clone.js.map +1 -1
  45. package/dist/skill-test/baseline-delta.d.ts +1575 -0
  46. package/dist/skill-test/baseline-delta.d.ts.map +1 -0
  47. package/dist/skill-test/baseline-delta.js +272 -0
  48. package/dist/skill-test/baseline-delta.js.map +1 -0
  49. package/dist/skill-test/baseline-integrity.d.ts +944 -0
  50. package/dist/skill-test/baseline-integrity.d.ts.map +1 -0
  51. package/dist/skill-test/baseline-integrity.js +3083 -0
  52. package/dist/skill-test/baseline-integrity.js.map +1 -0
  53. package/dist/skill-test/eval-executor.d.ts +19 -3
  54. package/dist/skill-test/eval-executor.d.ts.map +1 -1
  55. package/dist/skill-test/eval-executor.js +3 -3
  56. package/dist/skill-test/eval-executor.js.map +1 -1
  57. package/dist/skill-test/eval-fragment.d.ts +80 -6
  58. package/dist/skill-test/eval-fragment.d.ts.map +1 -1
  59. package/dist/skill-test/eval-fragment.js +61 -7
  60. package/dist/skill-test/eval-fragment.js.map +1 -1
  61. package/dist/skill-test/eval-grader.d.ts.map +1 -1
  62. package/dist/skill-test/eval-grader.js +143 -7
  63. package/dist/skill-test/eval-grader.js.map +1 -1
  64. package/dist/skill-test/eval-inputs.d.ts +107 -6
  65. package/dist/skill-test/eval-inputs.d.ts.map +1 -1
  66. package/dist/skill-test/eval-inputs.js +173 -16
  67. package/dist/skill-test/eval-inputs.js.map +1 -1
  68. package/dist/skill-test/eval-lint.d.ts +13 -0
  69. package/dist/skill-test/eval-lint.d.ts.map +1 -1
  70. package/dist/skill-test/eval-lint.js +17 -4
  71. package/dist/skill-test/eval-lint.js.map +1 -1
  72. package/dist/skill-test/executor-prompt.d.ts +13 -5
  73. package/dist/skill-test/executor-prompt.d.ts.map +1 -1
  74. package/dist/skill-test/executor-prompt.js +8 -5
  75. package/dist/skill-test/executor-prompt.js.map +1 -1
  76. package/dist/skill-test/exit-codes.d.ts +53 -16
  77. package/dist/skill-test/exit-codes.d.ts.map +1 -1
  78. package/dist/skill-test/exit-codes.js +123 -40
  79. package/dist/skill-test/exit-codes.js.map +1 -1
  80. package/dist/skill-test/fragment-merge.d.ts +1 -18
  81. package/dist/skill-test/fragment-merge.d.ts.map +1 -1
  82. package/dist/skill-test/fragment-merge.js +63 -4
  83. package/dist/skill-test/fragment-merge.js.map +1 -1
  84. package/dist/skill-test/grader-prompt.d.ts +27 -7
  85. package/dist/skill-test/grader-prompt.d.ts.map +1 -1
  86. package/dist/skill-test/grader-prompt.js +253 -61
  87. package/dist/skill-test/grader-prompt.js.map +1 -1
  88. package/dist/skill-test/grader-text.d.ts +145 -0
  89. package/dist/skill-test/grader-text.d.ts.map +1 -0
  90. package/dist/skill-test/grader-text.js +410 -0
  91. package/dist/skill-test/grader-text.js.map +1 -0
  92. package/dist/skill-test/grading-adapter.d.ts +32 -0
  93. package/dist/skill-test/grading-adapter.d.ts.map +1 -1
  94. package/dist/skill-test/grading-adapter.js +50 -3
  95. package/dist/skill-test/grading-adapter.js.map +1 -1
  96. package/dist/skill-test/grading-schema.d.ts +160 -5
  97. package/dist/skill-test/grading-schema.d.ts.map +1 -1
  98. package/dist/skill-test/grading-schema.js +66 -7
  99. package/dist/skill-test/grading-schema.js.map +1 -1
  100. package/dist/skill-test/index.d.ts +1 -1
  101. package/dist/skill-test/index.d.ts.map +1 -1
  102. package/dist/skill-test/index.js +1 -1
  103. package/dist/skill-test/index.js.map +1 -1
  104. package/dist/skill-test/lock.d.ts +56 -0
  105. package/dist/skill-test/lock.d.ts.map +1 -1
  106. package/dist/skill-test/lock.js +75 -2
  107. package/dist/skill-test/lock.js.map +1 -1
  108. package/dist/skill-test/pipeline.d.ts +16 -0
  109. package/dist/skill-test/pipeline.d.ts.map +1 -1
  110. package/dist/skill-test/pipeline.js +16 -9
  111. package/dist/skill-test/pipeline.js.map +1 -1
  112. package/dist/skill-test/preflight.d.ts.map +1 -1
  113. package/dist/skill-test/preflight.js +58 -14
  114. package/dist/skill-test/preflight.js.map +1 -1
  115. package/dist/skill-test/run-harness.d.ts +578 -35
  116. package/dist/skill-test/run-harness.d.ts.map +1 -1
  117. package/dist/skill-test/run-harness.js +1278 -147
  118. package/dist/skill-test/run-harness.js.map +1 -1
  119. package/dist/skill-test/staging.d.ts +8 -2
  120. package/dist/skill-test/staging.d.ts.map +1 -1
  121. package/dist/skill-test/staging.js +8 -4
  122. package/dist/skill-test/staging.js.map +1 -1
  123. package/dist/skill-test/tool-eval-schema.d.ts +6 -6
  124. package/dist/test-input.d.ts +1 -1
  125. package/dist/test-input.d.ts.map +1 -1
  126. package/dist/validators/agent-instruction-presence.d.ts +1 -1
  127. package/dist/validators/agent-instruction-presence.d.ts.map +1 -1
  128. package/dist/validators/bundled-resource-link-detection.d.ts +1 -1
  129. package/dist/validators/bundled-resource-link-detection.d.ts.map +1 -1
  130. package/dist/validators/bundled-resource-link-detection.js +1 -1
  131. package/dist/validators/bundled-resource-link-detection.js.map +1 -1
  132. package/dist/validators/compat-detectors.d.ts +1 -1
  133. package/dist/validators/compat-detectors.d.ts.map +1 -1
  134. package/dist/validators/compat-detectors.js +1 -1
  135. package/dist/validators/compat-detectors.js.map +1 -1
  136. package/dist/validators/cross-skill-dependency-detection.d.ts +1 -1
  137. package/dist/validators/cross-skill-dependency-detection.d.ts.map +1 -1
  138. package/dist/validators/cross-skill-dependency-detection.js +1 -1
  139. package/dist/validators/cross-skill-dependency-detection.js.map +1 -1
  140. package/dist/validators/description-style-detection.d.ts +1 -1
  141. package/dist/validators/description-style-detection.d.ts.map +1 -1
  142. package/dist/validators/description-style-detection.js +1 -1
  143. package/dist/validators/description-style-detection.js.map +1 -1
  144. package/dist/validators/frontmatter-validation.d.ts +1 -1
  145. package/dist/validators/frontmatter-validation.d.ts.map +1 -1
  146. package/dist/validators/frontmatter-validation.js +1 -1
  147. package/dist/validators/frontmatter-validation.js.map +1 -1
  148. package/dist/validators/imperative-body-detection.d.ts +1 -1
  149. package/dist/validators/imperative-body-detection.d.ts.map +1 -1
  150. package/dist/validators/imperative-body-detection.js +1 -1
  151. package/dist/validators/imperative-body-detection.js.map +1 -1
  152. package/dist/validators/index.d.ts +1 -1
  153. package/dist/validators/index.d.ts.map +1 -1
  154. package/dist/validators/kebab-case-detection.d.ts +1 -1
  155. package/dist/validators/kebab-case-detection.d.ts.map +1 -1
  156. package/dist/validators/kebab-case-detection.js +1 -1
  157. package/dist/validators/kebab-case-detection.js.map +1 -1
  158. package/dist/validators/marketplace-validator.js +1 -1
  159. package/dist/validators/marketplace-validator.js.map +1 -1
  160. package/dist/validators/packaging-validator.d.ts +38 -5
  161. package/dist/validators/packaging-validator.d.ts.map +1 -1
  162. package/dist/validators/packaging-validator.js +87 -10
  163. package/dist/validators/packaging-validator.js.map +1 -1
  164. package/dist/validators/plugin-hosted-shape.d.ts +1 -1
  165. package/dist/validators/plugin-hosted-shape.d.ts.map +1 -1
  166. package/dist/validators/plugin-hosted-shape.js +1 -1
  167. package/dist/validators/plugin-hosted-shape.js.map +1 -1
  168. package/dist/validators/plugin-recommended-fields.d.ts +1 -1
  169. package/dist/validators/plugin-recommended-fields.d.ts.map +1 -1
  170. package/dist/validators/plugin-recommended-fields.js +1 -1
  171. package/dist/validators/plugin-recommended-fields.js.map +1 -1
  172. package/dist/validators/registry-validator.js +1 -1
  173. package/dist/validators/registry-validator.js.map +1 -1
  174. package/dist/validators/rule-engine/rule-engine.d.ts +1 -1
  175. package/dist/validators/rule-engine/rule-engine.d.ts.map +1 -1
  176. package/dist/validators/rule-engine/rule-engine.js +20 -9
  177. package/dist/validators/rule-engine/rule-engine.js.map +1 -1
  178. package/dist/validators/skill-validator.d.ts.map +1 -1
  179. package/dist/validators/skill-validator.js +21 -3
  180. package/dist/validators/skill-validator.js.map +1 -1
  181. package/dist/validators/types.d.ts +1 -1
  182. package/dist/validators/types.d.ts.map +1 -1
  183. package/dist/validators/unified-validator.js +1 -1
  184. package/dist/validators/unified-validator.js.map +1 -1
  185. package/dist/validators/validation-rules.d.ts +1 -1
  186. package/dist/validators/validation-rules.d.ts.map +1 -1
  187. package/dist/validators/walker-to-issues.d.ts +7 -3
  188. package/dist/validators/walker-to-issues.d.ts.map +1 -1
  189. package/dist/validators/walker-to-issues.js +37 -8
  190. package/dist/validators/walker-to-issues.js.map +1 -1
  191. package/dist/walk-link-graph.d.ts.map +1 -1
  192. package/dist/walk-link-graph.js +36 -2
  193. package/dist/walk-link-graph.js.map +1 -1
  194. package/package.json +7 -7
@@ -3,6 +3,7 @@ import { mkdirSyncReal, parseStreamJsonTranscript, safePath, spawnHeadlessClaude
3
3
  import { EvalFragmentError, parseEvalFragment } from './eval-fragment.js';
4
4
  import { InternalHarnessError } from './exit-codes.js';
5
5
  import { assertGraderPromptInvariants, buildGraderPrompt } from './grader-prompt.js';
6
+ import { sanitizeGraderText } from './grader-text.js';
6
7
  import { GradingNonceError } from './grading-adapter.js';
7
8
  import { computeToolPassed } from './tool-eval-schema.js';
8
9
  /**
@@ -40,7 +41,16 @@ function fragmentWarnRouter(onProgress) {
40
41
  return (message) => onProgress(`[skill-test] ${message}\n`);
41
42
  }
42
43
  export async function runGraderForEval(input) {
43
- mkdirSyncReal(input.graderOutDir, { recursive: true });
44
+ // 0700, like every other vat-only directory this codebase creates (the grader
45
+ // root in `resolveGraderOutDir`, the workspaces root and every eval workspace in
46
+ // `stageEvalWorkspaces`). This is the per-ARM subdirectory of an already-0700
47
+ // parent, so nothing is exposed today — traversal into it is blocked one level
48
+ // up. It is made explicit because the rule is only worth anything while it is
49
+ // uniform: an unmoded `mkdir` here is the one place a reader would have to prove
50
+ // the parent's mode to know the fragment (which echoes the run's integrity nonce)
51
+ // is unreadable, and the day this dir is created somewhere else that proof is
52
+ // gone. Fragments are consumed-on-read anyway; this is the resting-state half.
53
+ mkdirSyncReal(input.graderOutDir, { recursive: true, mode: 0o700 });
44
54
  // evalId is validated `[A-Za-z0-9_-]+` upstream (eval-inputs.ts) so a plain
45
55
  // join is safe, but `joinUnderRoot` is used defensively — it costs nothing
46
56
  // and closes off any future path where an unvalidated id reaches here.
@@ -58,10 +68,14 @@ export async function runGraderForEval(input) {
58
68
  });
59
69
  // Defense-in-depth (parity with eval-executor.ts's build→assert→use pattern):
60
70
  // re-verify the built grader prompt still carries its required invariants
61
- // (STOP, fragment path, browser/iteration forbids, nonce directive) before
62
- // spawning, so a regressed buildGraderPrompt is caught here, not by a grader
63
- // that silently misbehaves.
64
- assertGraderPromptInvariants(prompt, input.transcript);
71
+ // (STOP, fragment path, browser/iteration forbids, nonce directive, and the
72
+ // untrusted-data fences) before spawning, so a regressed buildGraderPrompt is
73
+ // caught here, not by a grader that silently misbehaves. The nonce is handed
74
+ // over so the assertion can cut out every fenced region — transcript, subject
75
+ // manifest, and the suite's own expectations/expected_output — and none of
76
+ // that attacker-controlled text can satisfy an invariant on the builder's
77
+ // behalf.
78
+ assertGraderPromptInvariants(prompt, input.nonce);
65
79
  const spawn = input.spawn ?? spawnHeadlessClaude;
66
80
  // Accumulate the grader's stream-json stdout so we can read its terminal
67
81
  // `total_cost_usd` for run-wide spend aggregation (adopter follow-up). Held in
@@ -117,8 +131,16 @@ export async function runGraderForEval(input) {
117
131
  if (fragment.runNonce !== input.nonce) {
118
132
  throw new GradingNonceError(`eval "${input.evalId}" fragment \`runNonce\` does not match this run`);
119
133
  }
134
+ assertExpectationCountDeclared(input.evalId, input.expectations, fragment.expectations);
120
135
  assertToolVerdictConsistent(input.evalId, input.toolExpectations, fragment.tool);
121
- return fragment;
136
+ // The grader's only input is the executor transcript, which untrusted skill code
137
+ // controls, and `evalId` is schema-typed as any non-empty string. It is echoed
138
+ // verbatim into the run summary and into `baseline.json` — including the
139
+ // CONTAMINATED banner — so a grader talked into emitting an id containing
140
+ // newlines and ANSI escapes can paint a reassuring "known false positive, the
141
+ // delta is valid" line directly beneath vat's own warning. VAT knows which eval
142
+ // it asked about; take the id from the request, never from the answer.
143
+ return { ...fragment, evalId: input.evalId };
122
144
  }
123
145
  /**
124
146
  * Read the grader's fragment JSON into memory and unlink it immediately —
@@ -146,7 +168,16 @@ function readAndConsumeFragmentFile(fragmentOut, evalId, status) {
146
168
  raw = JSON.parse(readFileSync(fragmentOut, 'utf-8'));
147
169
  }
148
170
  catch (err) {
149
- throw new EvalFragmentError(`grader fragment for eval "${evalId}" at ${fragmentOut} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
171
+ // V8 embeds a VERBATIM slice of the offending bytes in its SyntaxError
172
+ // message, unescaped — and this is one of the few grader-controlled strings
173
+ // that reaches an operator WITHOUT passing `parseEvalFragment`, the
174
+ // documented text boundary, because the parse it would have gone through is
175
+ // the one that just failed. The CLI writes it as `Error: ${err.message}`, so
176
+ // an unsanitized fragment of `ESC[2K CR ESC[32m…` wipes the line vat just
177
+ // printed and continues in vat's own colour (and a <=16-byte file gets its
178
+ // whole `ESC[2J ESC[H` echoed, clearing the screen).
179
+ throw new EvalFragmentError(`grader fragment for eval "${evalId}" at ${fragmentOut} is not valid JSON: ` +
180
+ sanitizeGraderText(err instanceof Error ? err.message : String(err)));
150
181
  }
151
182
  try {
152
183
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- fragmentOut is our own derived path (joinUnderRoot-guarded)
@@ -157,6 +188,57 @@ function readAndConsumeFragmentFile(fragmentOut, evalId, status) {
157
188
  }
158
189
  return raw;
159
190
  }
191
+ /**
192
+ * The OUTPUT channel's declared-vs-reported check — the exact thing
193
+ * {@link assertToolCheckNamesDeclared} does for the TOOL channel, applied to the
194
+ * channel every reported number is computed from.
195
+ *
196
+ * Each arm's `summary`, the pass/fail verdict `reconcileGrading` produces, and
197
+ * the `--baseline` delta between the arms are all computed over
198
+ * `fragment.expectations` — i.e. over however many entries the grader chose to
199
+ * write. The prompt ASKS for "one per expectation above" and, until this check,
200
+ * nothing verified it:
201
+ *
202
+ * - **Omission laundered a pass, on EVERY run.** A grader that skips a FAILING
203
+ * expectation leaves a fragment where the survivors all passed, so
204
+ * `reconcileGrading` reports `allPassed: true` and the run exits 0.
205
+ * - **Invention inflated the delta.** A suite declaring 1 expectation whose two
206
+ * graders each emit 3 of their own yields `Baseline delta: +3` where the
207
+ * maximum legal lift is +1.
208
+ *
209
+ * The existing `armExpectationSkew` cannot see either: it compares the two ARMS
210
+ * to each other, and both graders receive the same declared list from the same
211
+ * template, so they drift together and parity holds while a confident wrong
212
+ * number prints.
213
+ *
214
+ * COUNT, not text. The grader is asked to echo each expectation's `text`, but it
215
+ * routinely reworks the wording, and a reworded text is not a wrong verdict —
216
+ * whereas a missing or extra ENTRY always is, because the entry is the unit
217
+ * every score counts.
218
+ *
219
+ * Fail-closed like its siblings below: this is vat's own grading infrastructure
220
+ * failing its one job, not adopter data being audited.
221
+ *
222
+ * FAIL-CLOSED IS NOT FAIL-THE-RUN, and the distinction is the caller's to make, not
223
+ * this function's. Throwing here is unconditional and stays that way: a fragment
224
+ * whose entry count does not match the declaration is not a verdict, and returning
225
+ * it "leniently" for the control arm would put an unchecked number straight into the
226
+ * delta — which is the hole this check was added to close. What changed is where the
227
+ * throw lands. `runEvalWorker` (run-harness.ts) catches a CONTROL-arm throw, records
228
+ * it as a control-arm failure, and lets the run finish; a TREATMENT-arm throw still
229
+ * propagates and fails the run. So a control-arm grader miscount now withholds the
230
+ * delta as `null` with the reason attached, instead of destroying fully-billed
231
+ * treatment work that was never in doubt. Do not soften this assert to get that
232
+ * behaviour — it is already there, one level up, where the arm is known.
233
+ */
234
+ function assertExpectationCountDeclared(evalId, declared, reported) {
235
+ if (reported.length === declared.length)
236
+ return;
237
+ throw new InternalHarnessError(`Grader for eval "${evalId}" returned ${reported.length} expectation entr${reported.length === 1 ? 'y' : 'ies'} ` +
238
+ `for an eval that declares ${declared.length}. Every score vat reports — each arm's summary and the ` +
239
+ `--baseline delta between them — is computed over the entries the grader emitted, so an invented or ` +
240
+ `omitted entry silently changes it.`);
241
+ }
160
242
  /**
161
243
  * Fail-open guards on the grader's tool verdict, split out of
162
244
  * {@link runGraderForEval} to keep its cognitive complexity within budget:
@@ -171,6 +253,8 @@ function readAndConsumeFragmentFile(fragmentOut, evalId, status) {
171
253
  * that disagrees with its own sub-checks is grader malfunction (a false green
172
254
  * or a false red), surfaced loudly rather than merged. Mirrors
173
255
  * `reconcileGrading`'s summary/expectations reconciliation (grading-adapter.ts).
256
+ * - Fix #4: the check NAMES must be exactly the eval's declared ones. See
257
+ * {@link assertToolCheckNamesDeclared}.
174
258
  */
175
259
  function assertToolVerdictConsistent(evalId, toolExpectations, tool) {
176
260
  if (toolExpectations !== undefined && tool === undefined) {
@@ -180,5 +264,57 @@ function assertToolVerdictConsistent(evalId, toolExpectations, tool) {
180
264
  throw new InternalHarnessError(`Grader for eval "${evalId}" emitted a tool verdict whose \`passed\` (${tool.passed}) ` +
181
265
  `disagrees with its own sub-checks (recomputed ${computeToolPassed(tool)}).`);
182
266
  }
267
+ assertToolCheckNamesDeclared(evalId, toolExpectations, tool);
268
+ }
269
+ /** Render grader-supplied names for an error message, already sanitized by the fragment parse. */
270
+ function quoteNames(names) {
271
+ return names.map((name) => `"${name}"`).join(', ');
272
+ }
273
+ /**
274
+ * One channel's check names must be EXACTLY the declared ones — no invention,
275
+ * no omission.
276
+ *
277
+ * `computeToolPassed` iterates the checks the GRADER emitted, not the ones the
278
+ * eval declared, so both directions are load-bearing and neither is cosmetic:
279
+ *
280
+ * - **Omission laundered a pass.** Drop the `mustRun` entry for an executable
281
+ * that never ran and the channel is vacuously true — the verdict reads
282
+ * `passed: true` for an expectation nobody checked.
283
+ * - **Invention gates on a name vat never asked about.** A grader talked into
284
+ * emitting `{name: "totally-fine", ran: true}` adds a check the eval does not
285
+ * declare, and it counts toward the verdict exactly like a real one.
286
+ *
287
+ * Names are compared AFTER {@link sanitizeGraderText} on both sides. The
288
+ * reported side is already sanitized (parseEvalFragment); normalizing the
289
+ * declared side too means the comparison happens in one space, so an adopter
290
+ * who declared a name containing a tab does not get an unexplainable mismatch.
291
+ *
292
+ * Fail-closed like its siblings above: this is vat's own grading infrastructure
293
+ * failing to do its one job, not adopter data being audited.
294
+ */
295
+ function assertToolCheckNamesDeclared(evalId, toolExpectations, tool) {
296
+ if (tool === undefined)
297
+ return;
298
+ const declared = toolExpectations ?? {};
299
+ assertChannelNames(evalId, 'mustRun', declared.mustRun, (tool.mustRun ?? []).map((c) => c.name));
300
+ assertChannelNames(evalId, 'mustNotRun', declared.mustNotRun, (tool.mustNotRun ?? []).map((c) => c.name));
301
+ assertChannelNames(evalId, 'mustSucceed', declared.mustSucceed, (tool.mustSucceed ?? []).map((c) => c.name));
302
+ assertChannelNames(evalId, 'sequence', declared.sequence, (tool.sequence ?? []).flatMap((c) => c.steps));
303
+ }
304
+ function assertChannelNames(evalId, channel, declared, reported) {
305
+ const declaredSet = new Set((declared ?? []).map(sanitizeGraderText));
306
+ const reportedSet = new Set(reported);
307
+ const invented = [...reportedSet].filter((name) => !declaredSet.has(name));
308
+ const missing = [...declaredSet].filter((name) => !reportedSet.has(name));
309
+ if (invented.length === 0 && missing.length === 0)
310
+ return;
311
+ const problems = [];
312
+ if (invented.length > 0)
313
+ problems.push(`invented ${quoteNames(invented)}`);
314
+ if (missing.length > 0)
315
+ problems.push(`omitted ${quoteNames(missing)}`);
316
+ throw new InternalHarnessError(`Grader for eval "${evalId}" emitted \`tool.${channel}\` checks that do not match the eval's ` +
317
+ `declared toolExpectations: ${problems.join('; ')}. The verdict is computed from the checks the ` +
318
+ `grader emitted, so an invented or omitted name silently changes it.`);
183
319
  }
184
320
  //# sourceMappingURL=eval-grader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"eval-grader.js","sourceRoot":"","sources":["../../src/skill-test/eval-grader.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"eval-grader.js","sourceRoot":"","sources":["../../src/skill-test/eval-grader.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,48 @@
1
1
  import { z } from 'zod';
2
- /** Raised for any eval-input problem (bad JSON, schema failure, missing input file). Maps to exit 2. */
2
+ /**
3
+ * Raised for any eval-input problem (bad JSON, schema failure, missing input file).
4
+ * Maps to exit 2.
5
+ *
6
+ * The FIELD is what makes that sentence true, and until it was added only one of this
7
+ * class's two routes delivered it. `attemptStageWorkspaces` catches the staging
8
+ * instance and maps it explicitly; the instance thrown from {@link armDirSegment}
9
+ * fires deep in the eval loop, outside that handler, and reached
10
+ * `mapErrorToExitCode`'s Internal default — reporting 1 for a preflight-class
11
+ * problem while the docblock above claimed 2. Exit 2 for the internal-invariant case
12
+ * too is consistent with every sibling in this feature.
13
+ */
3
14
  export declare class EvalInputError extends Error {
15
+ readonly exitCode: 2;
4
16
  constructor(message: string);
5
17
  }
18
+ /**
19
+ * Neutralize a SUITE-AUTHORED string before it is quoted into an
20
+ * {@link EvalInputError}, whose message reaches `process.stdout` on the
21
+ * `Summary:` line — the one channel deliberately kept machine-readable — and
22
+ * renders twice on the way there.
23
+ *
24
+ * The suite is nominally adopter-authored, but `resolveEvalSuitePath` will
25
+ * harvest one out of a FETCHED npm/url artifact, i.e. out of the skill under
26
+ * test, so treat it as untrusted. `files[]` is the sharp edge: it is
27
+ * `z.array(z.string().min(1))` with NO charset constraint, in deliberate
28
+ * contrast to `id`, which is regex-pinned precisely because it names a
29
+ * directory. A derived path or an OS error text built from such an entry
30
+ * carries the same bytes, so those are sanitized here too, not just the entry.
31
+ *
32
+ * Reuses {@link sanitizeGraderText} rather than growing a second neutralizer:
33
+ * the threat and the remedy (drop escape sequences whole, fold every remaining
34
+ * control code to a space, collapse, cap) are identical — only the source of
35
+ * the untrusted text differs.
36
+ *
37
+ * EXPORTED because the suite reaches an operator surface from more than this
38
+ * module: `eval-lint.ts` quotes `toolExpectations.*` entries — the same
39
+ * unconstrained `z.array(z.string().min(1))` shape as `files[]` — into an advisory
40
+ * that `run-harness.ts` writes to stderr verbatim, and does it at Step 5.5, ahead
41
+ * of both the security-ack gate and the `--dry-run` short-circuit. Every new
42
+ * suite-authored string bound for a terminal goes through HERE, so the trust model
43
+ * above stays stated in one place.
44
+ */
45
+ export declare function quoteSuiteText(value: string): string;
6
46
  /**
7
47
  * True when `key` is exactly one edit (insert/delete/substitute one char) from
8
48
  * `target`. Used to catch typos of recognized fields (here) and of declared
@@ -496,19 +536,80 @@ export type EvalSuite = z.infer<typeof EvalSuiteSchema>;
496
536
  export type ToolExpectations = NonNullable<EvalEntry['toolExpectations']>;
497
537
  /** Parse + validate a skill-test eval suite. Throws {@link EvalInputError} on any problem. */
498
538
  export declare function parseEvalSuite(jsonText: string): EvalSuite;
539
+ /**
540
+ * One side of a `--baseline` A/B: `'with'` runs the skill staged and declared,
541
+ * `'without'` is the control. Declared here rather than in the run orchestrator
542
+ * because the staging layer needs it to give each arm its own workspace tree.
543
+ */
544
+ export type EvalArm = 'with' | 'without';
545
+ /**
546
+ * The opaque directory segment each arm's workspaces live under, minted fresh per
547
+ * run. `with` is required and `without` present only under `--baseline`, so the
548
+ * set of arms is the keys — a shape that cannot express "stage nothing", unlike
549
+ * the `readonly EvalArm[]` this replaced (an empty array was a legal input that
550
+ * silently left every executor with a non-existent cwd).
551
+ *
552
+ * ⚠️ The VALUES must not be the literal arm names, and must not be derivable from
553
+ * each other. See {@link stageEvalWorkspaces}.
554
+ */
555
+ export interface ArmWorkspaceDirs {
556
+ with: string;
557
+ without?: string;
558
+ }
559
+ /** The arms an {@link ArmWorkspaceDirs} covers, in dispatch order. */
560
+ export declare function armsOf(dirs: ArmWorkspaceDirs): readonly EvalArm[];
561
+ /**
562
+ * The directory segment for one arm. Throws rather than falling back to the arm
563
+ * NAME: a silent fallback would reinstate the unblinding this indirection exists
564
+ * to remove, and would do it invisibly.
565
+ */
566
+ export declare function armDirSegment(dirs: ArmWorkspaceDirs, arm: EvalArm): string;
499
567
  export interface StageEvalWorkspacesInput {
500
568
  /** Parsed suite (Task 1). */
501
569
  suite: EvalSuite;
502
570
  /** Directory containing evals.json — the base for each eval's relative `files`. */
503
571
  evalsDir: string;
504
- /** `<harnessRoot>/workspaces` — per-eval dirs are created beneath it. */
572
+ /** Per-run workspaces root — per-arm, per-eval dirs are created beneath it. */
505
573
  workspacesRoot: string;
574
+ /**
575
+ * Opaque per-arm directory segments. Each arm gets its OWN copy of every
576
+ * workspace; see the note on the layout below for why that is not an
577
+ * optimization to undo.
578
+ */
579
+ armDirs: ArmWorkspaceDirs;
506
580
  }
507
581
  /**
508
- * Materialize each eval's declared input `files` into `<workspacesRoot>/<id>/<relpath>`,
509
- * preserving relative structure. Evals without `files` are skipped. Throws
510
- * {@link EvalInputError} if a listed file does not exist (the eval cannot run without it).
511
- * Returns `workspacesRoot`.
582
+ * Materialize each eval's declared input `files` into
583
+ * `<workspacesRoot>/<armDir>/<id>/<relpath>`, preserving relative structure. Throws
584
+ * {@link EvalInputError} if a listed file does not exist (the eval cannot run
585
+ * without it). Returns `workspacesRoot`.
586
+ *
587
+ * EVERY eval gets a directory, including one that declares no `files` — it is
588
+ * simply left empty. An eval without a workspace used to leave the executor with
589
+ * no cwd of its own, so it ran inside the staged subject dir instead; for the
590
+ * skill-absent arm of a `--baseline` run that placed the control's cwd inside the
591
+ * very skill the arm exists to withhold. An empty dir costs nothing and keeps cwd
592
+ * shaped identically across both arms, which is what makes the A/B a comparison.
593
+ *
594
+ * WHY THE `<arm>` SEGMENT. The two arms of one eval are dispatched ADJACENTLY into
595
+ * a bounded-parallel pool, so they run at the same time. When they shared one
596
+ * directory, the control arm could simply `ls` and read whatever the treatment arm
597
+ * had just written — recovering the treatment's answer without ever touching the
598
+ * skill, and without producing a single detectable harness path. That is a channel
599
+ * BETWEEN the arms rather than into one of them, which is why auditing the four
600
+ * channels into a process (prompt, argv, cwd, env) did not surface it: each arm's
601
+ * own cwd was correct in isolation, and wrong as a pair. The arms must be
602
+ * byte-identical at start and unable to observe each other after it.
603
+ *
604
+ * WHY THE SEGMENT IS AN OPAQUE TOKEN and not the arm's name. It used to be the
605
+ * literal `with`/`without`, which made the executor's own working directory the
606
+ * loudest possible unblinding: the prompt tells each arm `Your working directory
607
+ * is …/vat-skill-test-ws-<token>/without/e1`, so the skill-absent arm was told, in
608
+ * the one string it cannot avoid reading, which side of the experiment it was on —
609
+ * while `buildExecutorPrompt`'s docblock promises it "must NEVER be told (directly
610
+ * or by implication) that it is being tested, evaluated, or graded". It also named
611
+ * the sibling: `../with/<id>/` was a guessable path to the treatment arm's live
612
+ * output. Independent random tokens fix both, and cost nothing.
512
613
  */
513
614
  export declare function stageEvalWorkspaces(input: StageEvalWorkspacesInput): string;
514
615
  //# sourceMappingURL=eval-inputs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"eval-inputs.d.ts","sourceRoot":"","sources":["../../src/skill-test/eval-inputs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wGAAwG;AACxG,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAyBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAUrE;AAWD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAsDxB,CAAC;AAEL,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAMZ,CAAC;AAEjB,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AACxD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAE1E,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAmB1D;AAED,MAAM,WAAW,wBAAwB;IACvC,6BAA6B;IAC7B,KAAK,EAAE,SAAS,CAAC;IACjB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM,CAoC3E"}
1
+ {"version":3,"file":"eval-inputs.d.ts","sourceRoot":"","sources":["../../src/skill-test/eval-inputs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;GAWG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,QAAQ,EAAG,CAAC,CAAU;gBACnB,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AA2CD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAUrE;AAiDD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAyDxB,CAAC;AAEL,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAMZ,CAAC;AAEjB,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AACxD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAE1E,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAsB1D;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAEzC;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,sEAAsE;AACtE,wBAAgB,MAAM,CAAC,IAAI,EAAE,gBAAgB,GAAG,SAAS,OAAO,EAAE,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAU1E;AAED,MAAM,WAAW,wBAAwB;IACvC,6BAA6B;IAC7B,KAAK,EAAE,SAAS,CAAC;IACjB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM,CAK3E"}
@@ -1,13 +1,73 @@
1
1
  import { cpSync, existsSync } from 'node:fs';
2
2
  import { mkdirSyncReal, safePath } from '@vibe-agent-toolkit/utils';
3
3
  import { z } from 'zod';
4
- /** Raised for any eval-input problem (bad JSON, schema failure, missing input file). Maps to exit 2. */
4
+ import { sanitizeGraderText, sanitizeTextPreservingLines } from './grader-text.js';
5
+ /**
6
+ * Raised for any eval-input problem (bad JSON, schema failure, missing input file).
7
+ * Maps to exit 2.
8
+ *
9
+ * The FIELD is what makes that sentence true, and until it was added only one of this
10
+ * class's two routes delivered it. `attemptStageWorkspaces` catches the staging
11
+ * instance and maps it explicitly; the instance thrown from {@link armDirSegment}
12
+ * fires deep in the eval loop, outside that handler, and reached
13
+ * `mapErrorToExitCode`'s Internal default — reporting 1 for a preflight-class
14
+ * problem while the docblock above claimed 2. Exit 2 for the internal-invariant case
15
+ * too is consistent with every sibling in this feature.
16
+ */
5
17
  export class EvalInputError extends Error {
18
+ exitCode = 2;
6
19
  constructor(message) {
7
20
  super(message);
8
21
  this.name = 'EvalInputError';
9
22
  }
10
23
  }
24
+ /**
25
+ * Neutralize a SUITE-AUTHORED string before it is quoted into an
26
+ * {@link EvalInputError}, whose message reaches `process.stdout` on the
27
+ * `Summary:` line — the one channel deliberately kept machine-readable — and
28
+ * renders twice on the way there.
29
+ *
30
+ * The suite is nominally adopter-authored, but `resolveEvalSuitePath` will
31
+ * harvest one out of a FETCHED npm/url artifact, i.e. out of the skill under
32
+ * test, so treat it as untrusted. `files[]` is the sharp edge: it is
33
+ * `z.array(z.string().min(1))` with NO charset constraint, in deliberate
34
+ * contrast to `id`, which is regex-pinned precisely because it names a
35
+ * directory. A derived path or an OS error text built from such an entry
36
+ * carries the same bytes, so those are sanitized here too, not just the entry.
37
+ *
38
+ * Reuses {@link sanitizeGraderText} rather than growing a second neutralizer:
39
+ * the threat and the remedy (drop escape sequences whole, fold every remaining
40
+ * control code to a space, collapse, cap) are identical — only the source of
41
+ * the untrusted text differs.
42
+ *
43
+ * EXPORTED because the suite reaches an operator surface from more than this
44
+ * module: `eval-lint.ts` quotes `toolExpectations.*` entries — the same
45
+ * unconstrained `z.array(z.string().min(1))` shape as `files[]` — into an advisory
46
+ * that `run-harness.ts` writes to stderr verbatim, and does it at Step 5.5, ahead
47
+ * of both the security-ack gate and the `--dry-run` short-circuit. Every new
48
+ * suite-authored string bound for a terminal goes through HERE, so the trust model
49
+ * above stays stated in one place.
50
+ */
51
+ export function quoteSuiteText(value) {
52
+ return sanitizeGraderText(value);
53
+ }
54
+ /**
55
+ * The same neutralization for a suite-derived string that is ALREADY multi-line
56
+ * and whose lines carry the meaning — today, exactly one: zod's schema-failure
57
+ * text.
58
+ *
59
+ * That message is a list of issues, one per offending path, and every one of the
60
+ * paths in it is a SUITE-AUTHORED KEY (`evals[17].toolExpecations`) reaching
61
+ * stderr unsanitized. It cannot go through {@link quoteSuiteText}: that collapses
62
+ * the list onto one capped line, which is the difference between "your suite has
63
+ * a typo, here is where" and "your suite has a typo". Degrading the common case to
64
+ * neutralize the rare one is a bad trade, so the sanitizer that keeps lines is
65
+ * used instead. See {@link sanitizeTextPreservingLines} for why it is not the
66
+ * default anywhere else.
67
+ */
68
+ function quoteSuiteBlock(value) {
69
+ return sanitizeTextPreservingLines(value);
70
+ }
11
71
  /**
12
72
  * String eval ids name a per-eval working directory ({@link stageEvalWorkspaces}),
13
73
  * so they must be safe path segments on every platform. Letters, digits, hyphen,
@@ -52,6 +112,42 @@ export function isSingleEditAway(key, target) {
52
112
  return key.slice(i + 1) === target.slice(i); // deletion from key
53
113
  return key.slice(i) === target.slice(i + 1); // insertion into key
54
114
  }
115
+ /**
116
+ * Reject an expectation string an eval declares TWICE.
117
+ *
118
+ * The entry, not the text, is the unit every reported score counts:
119
+ * `runGraderForEval`'s declared-vs-graded check compares `fragment.expectations`
120
+ * against `expectations.length`, and each arm's summary and the `--baseline` delta
121
+ * are computed over those entries. A suite that declares the same string twice
122
+ * hands the grader "1. x / 2. x" and gets ONE entry back for it — a perfectly
123
+ * reasonable grader response, and one that fails the count check mid-run with an
124
+ * `InternalHarnessError`, destroying a fully-billed treatment run over an input
125
+ * defect nothing looked at.
126
+ *
127
+ * So it is caught at PARSE, exactly like the duplicate-id rule in
128
+ * {@link parseEvalSuite}: exit 2, user-correctable, before anything is spawned.
129
+ * Scoped to one eval — two different evals may legitimately share expectation
130
+ * text, since each is graded from its own transcript.
131
+ *
132
+ * `expectations` is `z.array(z.string().min(1))` with no charset constraint and
133
+ * the message reaches stdout, so the offending string is sanitized on the way in.
134
+ */
135
+ function addDuplicateExpectationIssues(expectations, ctx) {
136
+ const seen = new Set();
137
+ for (const [index, expectation] of expectations.entries()) {
138
+ if (!seen.has(expectation)) {
139
+ seen.add(expectation);
140
+ continue;
141
+ }
142
+ ctx.addIssue({
143
+ code: z.ZodIssueCode.custom,
144
+ message: `duplicate expectation "${quoteSuiteText(expectation)}" — an eval must declare each expectation ` +
145
+ 'once. Every reported score counts fragment ENTRIES, and a grader handed the same text twice ' +
146
+ 'returns one entry for it, which fails the declared-count check mid-run.',
147
+ path: ['expectations', index],
148
+ });
149
+ }
150
+ }
55
151
  // evals.json is adopter-authored input that VAT *reads* — so per the project's
56
152
  // Postel's Law (read the outside world liberally), we validate only the fields
57
153
  // VAT actually consumes and pass everything else through untouched. `id` accepts
@@ -101,6 +197,7 @@ export const EvalEntrySchema = z
101
197
  })
102
198
  .passthrough()
103
199
  .superRefine((entry, ctx) => {
200
+ addDuplicateExpectationIssues(entry.expectations, ctx);
104
201
  for (const key of Object.keys(entry)) {
105
202
  if (RECOGNIZED_EVAL_FIELDS.includes(key))
106
203
  continue;
@@ -108,7 +205,9 @@ export const EvalEntrySchema = z
108
205
  if (near !== undefined) {
109
206
  ctx.addIssue({
110
207
  code: z.ZodIssueCode.custom,
111
- message: `unknown eval field "${key}" — did you mean "${near}"? (other custom fields are allowed and ignored)`,
208
+ // `key` is a suite-authored object key with no charset constraint, and
209
+ // this message is surfaced through `result.error.message` below.
210
+ message: `unknown eval field "${quoteSuiteText(key)}" — did you mean "${near}"? (other custom fields are allowed and ignored)`,
112
211
  path: [key],
113
212
  });
114
213
  }
@@ -128,11 +227,14 @@ export function parseEvalSuite(jsonText) {
128
227
  raw = JSON.parse(jsonText);
129
228
  }
130
229
  catch (e) {
131
- throw new EvalInputError(`evals.json is not valid JSON: ${e.message}`);
230
+ // V8 quotes a verbatim, unescaped slice of the offending bytes into its
231
+ // SyntaxError message — a suite file that is nothing but ANSI escapes would
232
+ // otherwise repaint the operator's terminal from vat's own error line.
233
+ throw new EvalInputError(`evals.json is not valid JSON: ${quoteSuiteText(e.message)}`);
132
234
  }
133
235
  const result = EvalSuiteSchema.safeParse(raw);
134
236
  if (!result.success) {
135
- throw new EvalInputError(`evals.json failed schema validation: ${result.error.message}`);
237
+ throw new EvalInputError(`evals.json failed schema validation: ${quoteSuiteBlock(result.error.message)}`);
136
238
  }
137
239
  // Compare ids as strings: each id names a working directory via String(id),
138
240
  // so a numeric `1` and a string `"1"` would collide on disk even though they
@@ -143,18 +245,72 @@ export function parseEvalSuite(jsonText) {
143
245
  }
144
246
  return result.data;
145
247
  }
248
+ /** The arms an {@link ArmWorkspaceDirs} covers, in dispatch order. */
249
+ export function armsOf(dirs) {
250
+ return dirs.without === undefined ? ['with'] : ['with', 'without'];
251
+ }
252
+ /**
253
+ * The directory segment for one arm. Throws rather than falling back to the arm
254
+ * NAME: a silent fallback would reinstate the unblinding this indirection exists
255
+ * to remove, and would do it invisibly.
256
+ */
257
+ export function armDirSegment(dirs, arm) {
258
+ const segment = arm === 'with' ? dirs.with : dirs.without;
259
+ if (segment === undefined || segment === '') {
260
+ throw new EvalInputError(`Internal: no workspace directory was minted for the "${arm}" arm. ` +
261
+ 'Every arm this run dispatches must have one; falling back to the arm name would ' +
262
+ 'tell the executor which side of the A/B it is on.');
263
+ }
264
+ return segment;
265
+ }
146
266
  /**
147
- * Materialize each eval's declared input `files` into `<workspacesRoot>/<id>/<relpath>`,
148
- * preserving relative structure. Evals without `files` are skipped. Throws
149
- * {@link EvalInputError} if a listed file does not exist (the eval cannot run without it).
150
- * Returns `workspacesRoot`.
267
+ * Materialize each eval's declared input `files` into
268
+ * `<workspacesRoot>/<armDir>/<id>/<relpath>`, preserving relative structure. Throws
269
+ * {@link EvalInputError} if a listed file does not exist (the eval cannot run
270
+ * without it). Returns `workspacesRoot`.
271
+ *
272
+ * EVERY eval gets a directory, including one that declares no `files` — it is
273
+ * simply left empty. An eval without a workspace used to leave the executor with
274
+ * no cwd of its own, so it ran inside the staged subject dir instead; for the
275
+ * skill-absent arm of a `--baseline` run that placed the control's cwd inside the
276
+ * very skill the arm exists to withhold. An empty dir costs nothing and keeps cwd
277
+ * shaped identically across both arms, which is what makes the A/B a comparison.
278
+ *
279
+ * WHY THE `<arm>` SEGMENT. The two arms of one eval are dispatched ADJACENTLY into
280
+ * a bounded-parallel pool, so they run at the same time. When they shared one
281
+ * directory, the control arm could simply `ls` and read whatever the treatment arm
282
+ * had just written — recovering the treatment's answer without ever touching the
283
+ * skill, and without producing a single detectable harness path. That is a channel
284
+ * BETWEEN the arms rather than into one of them, which is why auditing the four
285
+ * channels into a process (prompt, argv, cwd, env) did not surface it: each arm's
286
+ * own cwd was correct in isolation, and wrong as a pair. The arms must be
287
+ * byte-identical at start and unable to observe each other after it.
288
+ *
289
+ * WHY THE SEGMENT IS AN OPAQUE TOKEN and not the arm's name. It used to be the
290
+ * literal `with`/`without`, which made the executor's own working directory the
291
+ * loudest possible unblinding: the prompt tells each arm `Your working directory
292
+ * is …/vat-skill-test-ws-<token>/without/e1`, so the skill-absent arm was told, in
293
+ * the one string it cannot avoid reading, which side of the experiment it was on —
294
+ * while `buildExecutorPrompt`'s docblock promises it "must NEVER be told (directly
295
+ * or by implication) that it is being tested, evaluated, or graded". It also named
296
+ * the sibling: `../with/<id>/` was a guessable path to the treatment arm's live
297
+ * output. Independent random tokens fix both, and cost nothing.
151
298
  */
152
299
  export function stageEvalWorkspaces(input) {
300
+ for (const arm of armsOf(input.armDirs)) {
301
+ stageEvalWorkspacesForArm(input, arm);
302
+ }
303
+ return input.workspacesRoot;
304
+ }
305
+ function stageEvalWorkspacesForArm(input, arm) {
306
+ const armRoot = safePath.joinUnderRoot(input.workspacesRoot, armDirSegment(input.armDirs, arm));
307
+ mkdirSyncReal(armRoot, { recursive: true, mode: 0o700 });
153
308
  for (const entry of input.suite.evals) {
154
- if (entry.files === undefined || entry.files.length === 0)
155
- continue;
156
- const evalWorkspace = safePath.joinUnderRoot(input.workspacesRoot, String(entry.id));
157
- for (const rel of entry.files) {
309
+ const evalWorkspace = safePath.joinUnderRoot(armRoot, String(entry.id));
310
+ // 0700 like the root above it — created for every eval, populated only by
311
+ // those declaring `files`.
312
+ mkdirSyncReal(evalWorkspace, { recursive: true, mode: 0o700 });
313
+ for (const rel of entry.files ?? []) {
158
314
  // Containment first: a `rel` that escapes evalsDir or the workspace is a
159
315
  // genuine "escapes the eval directory" problem and is reported as such.
160
316
  let src;
@@ -164,11 +320,12 @@ export function stageEvalWorkspaces(input) {
164
320
  dest = safePath.joinUnderRoot(evalWorkspace, rel);
165
321
  }
166
322
  catch (err) {
167
- throw new EvalInputError(`eval ${entry.id} declares input file "${rel}" that escapes the eval directory: ${err.message}`);
323
+ throw new EvalInputError(`eval ${entry.id} declares input file "${quoteSuiteText(rel)}" that escapes the eval directory: ` +
324
+ quoteSuiteText(err.message));
168
325
  }
169
326
  // eslint-disable-next-line security/detect-non-literal-fs-filename -- src is contained under evalsDir via joinUnderRoot; suite is developer-authored
170
327
  if (!existsSync(src)) {
171
- throw new EvalInputError(`eval ${entry.id} declares input file "${rel}" but it is absent at ${src}`);
328
+ throw new EvalInputError(`eval ${entry.id} declares input file "${quoteSuiteText(rel)}" but it is absent at ${quoteSuiteText(src)}`);
172
329
  }
173
330
  // Copy failures (permissions, illegal filename on the host, disk) are
174
331
  // reported accurately rather than mislabeled as a containment escape.
@@ -179,10 +336,10 @@ export function stageEvalWorkspaces(input) {
179
336
  cpSync(src, dest, { recursive: true });
180
337
  }
181
338
  catch (err) {
182
- throw new EvalInputError(`eval ${entry.id} failed to stage input file "${rel}" into the workspace: ${err.message}`);
339
+ throw new EvalInputError(`eval ${entry.id} failed to stage input file "${quoteSuiteText(rel)}" into the workspace: ` +
340
+ quoteSuiteText(err.message));
183
341
  }
184
342
  }
185
343
  }
186
- return input.workspacesRoot;
187
344
  }
188
345
  //# sourceMappingURL=eval-inputs.js.map