@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
@@ -11,13 +11,16 @@
11
11
  * → release lock → return result
12
12
  */
13
13
  import type { SkillSourceDescriptor } from '@vibe-agent-toolkit/resources';
14
- import { type spawnHeadlessClaude } from '@vibe-agent-toolkit/utils';
14
+ import { type spawnHeadlessClaude, type ResolvedAuth } from '@vibe-agent-toolkit/utils';
15
15
  import type { ResolveSkillSourceContext, SkillSource } from '../skill-source/types.js';
16
+ import { type BaselineDelta } from './baseline-delta.js';
17
+ import { type ArmEvalGrade, type BaselineControlArmFailure, type BaselineIntegrity, type DetectBaselineContaminationInput } from './baseline-integrity.js';
16
18
  import type { EvalFragment } from './eval-fragment.js';
17
- import { type EvalEntry, type EvalSuite } from './eval-inputs.js';
19
+ import { type ArmWorkspaceDirs, type EvalArm, type EvalEntry, type EvalSuite } from './eval-inputs.js';
18
20
  import { type SkillTestExitCodeValue } from './exit-codes.js';
19
21
  import { type FrictionItem } from './friction-schema.js';
20
22
  import { type GradingVerdict } from './grading-adapter.js';
23
+ import { RateLimitSignal } from './pipeline.js';
21
24
  import { type PreflightInput } from './preflight.js';
22
25
  import { type StageItem } from './staging.js';
23
26
  import { type SkippedEvalsSummary } from './tier-plan.js';
@@ -86,6 +89,15 @@ export interface RunHarnessOptions {
86
89
  * install. Production callers leave it undefined (the real spawn is used).
87
90
  */
88
91
  spawn?: typeof spawnHeadlessClaude;
92
+ /**
93
+ * Injectable rate-limit backoff (tests only) — the pipeline's `onRateLimit`,
94
+ * given the 1-based retry attempt and returning milliseconds. Returning `0`
95
+ * collapses the retry budget to nothing, which is what makes the EXHAUSTED
96
+ * control-arm path testable at all: the real backoff is exponential and spends
97
+ * 31 seconds per item before the budget runs out. Production callers leave it
98
+ * undefined (the real exponential backoff is used).
99
+ */
100
+ rateLimitBackoffMs?: (attempt: number) => number;
89
101
  /** Enable A/B baseline run (with/without skill). */
90
102
  baseline?: boolean;
91
103
  /** Allow unverified skill source (skip manifest check). */
@@ -144,11 +156,18 @@ export interface RunHarnessOptions {
144
156
  * does not — issue #145 Phase T). Passed to the grader on the WITH arm ONLY as
145
157
  * a recognition aid alongside each eval's `toolExpectations`. Absent → the
146
158
  * grader still matches tools by the commands it sees in the transcript.
159
+ *
160
+ * `path` serves a SECOND consumer with a different need: `detectBaselineContamination`
161
+ * matches an escaping reach against the skill-relative PATH, because an ambient copy
162
+ * of the skill reproduces `…/scripts/csvsum.py` under whatever root it sits at, while
163
+ * the control arm's own `/tmp/summary.txt` does not. Matching the extension-stripped
164
+ * `name` instead convicted clean control arms of reading their own scratch files.
147
165
  */
148
166
  declaredExecutables?: Array<{
149
167
  name: string;
150
168
  howInvoked: string;
151
169
  kind: string;
170
+ path: string;
152
171
  }>;
153
172
  /**
154
173
  * Opt-OUT of eval gating (for interactive use). By DEFAULT (false/absent) a
@@ -163,6 +182,27 @@ export interface RunHarnessResult {
163
182
  harnessPath: string;
164
183
  exitCode: number;
165
184
  summary: string;
185
+ /**
186
+ * Where the executor's per-eval working directories were materialized. Lives
187
+ * OUTSIDE `harnessPath` on purpose (see {@link resolveWorkspacesRoot}), so it
188
+ * cannot be derived from the harness path and has to be reported.
189
+ *
190
+ * Present ONLY when those directories SURVIVE the run — i.e. under `--keep`,
191
+ * the sole thing that retains them (NOT `--out`/`--workdir`, unlike the harness
192
+ * root). Cleanup runs in the harness's own `finally`, before this result reaches
193
+ * any caller, so a populated value is a promise that the path is still there:
194
+ * anything that reports it (the CLI's `Workspaces:` line) may print it as-is.
195
+ * Also absent when the run ended before workspaces were staged.
196
+ */
197
+ workspacesPath?: string;
198
+ /**
199
+ * Where this run's artifacts (`grading.json`, `friction.json`, `tool-eval.json`,
200
+ * and `baseline.json` on a `--baseline` run) were written. Reported rather than
201
+ * left to be derived from `harnessPath`, because it is the ONLY part of the
202
+ * harness root that survives cleanup on a default run — see
203
+ * {@link cleanupHarness}. Absent when the run ended before Step 7 created it.
204
+ */
205
+ resultsPath?: string;
166
206
  }
167
207
  /**
168
208
  * Built-in cost/runtime safety ceilings applied PER executor/grader spawn. These
@@ -231,10 +271,61 @@ export declare function makeStageItem(name: string, source: SkillSource, repoRoo
231
271
  export declare function buildStageItems(opts: RunHarnessOptions, repoRoot: string): StageItem[];
232
272
  export declare function buildResolveCtx(harnessRoot: string, repoRoot: string): ResolveSkillSourceContext;
233
273
  /**
234
- * Return a sensible dummy value for a given CLI flag so that value-validation
235
- * doesn't reject the argument before `--help` can short-circuit the session.
274
+ * Match a flag name as a whole token in `claude --help` output.
275
+ *
276
+ * The boundary matters: a bare `includes('--plugin-dir')` also matches
277
+ * `--plugin-dirs` or `--plugin-dir-cache`, and vat's own flag names are prefixes
278
+ * of each other (`--max-turns` / `--max-turns-per-eval` would collide). Neither
279
+ * side of the token may be a letter or a dash.
280
+ */
281
+ export declare function helpTextDeclaresFlag(helpText: string, flag: string): boolean;
282
+ /**
283
+ * A flag name no `claude` will ever accept. It is probed alongside the real ones
284
+ * as a NEGATIVE CONTROL: if the probe reports this as supported, the probe is
285
+ * broken and its answer about every other flag is worthless.
286
+ */
287
+ export declare const FLAG_PROBE_SENTINEL = "--vat-probe-flag-that-cannot-exist";
288
+ /**
289
+ * Build a token-free flag-support probe from `claude --help`.
290
+ *
291
+ * ⚠️ This REPLACES an exit-code probe (`claude <flag> <dummy> --help`, exit 0 ⇒
292
+ * supported) that could not discriminate at all. Verified against claude 2.x:
293
+ * `claude --no-such-flag-xyz 1 --help` exits **0**, because `--help` short-circuits
294
+ * before argument validation. Every one of preflight's `flag <name>` checks
295
+ * therefore reported "supported" unconditionally — including for flags that do not
296
+ * exist — so the gate that is supposed to stop vat spawning with an unsupported
297
+ * flag was decorative. (The same command without `--help` exits 1, but running it
298
+ * would start a real session and bill tokens, which is why `--help` was there.)
299
+ *
300
+ * Help-text matching does discriminate, and it is still token-free: one `--help`
301
+ * invocation for the whole run instead of one spawn per flag.
302
+ *
303
+ * The probe carries its own negative control ({@link FLAG_PROBE_SENTINEL}). If a
304
+ * future `claude` prints something that matches the sentinel — or `--help` fails
305
+ * outright — the probe reports EVERY flag unsupported rather than every flag
306
+ * supported. A probe that cannot tell must fail closed and be seen; the defect it
307
+ * replaces failed open and was invisible.
308
+ */
309
+ export declare function buildFlagParseProbe(runHelp?: () => string | null): (flag: string) => boolean;
310
+ /**
311
+ * ⚠️ KNOWN OPEN — `costEstimate` here is DEAD, in three independent ways, and none
312
+ * of them is fixable from inside this function:
313
+ *
314
+ * 1. `evalCount: 1` is a placeholder. The suite is not parsed until Step 5.5, which
315
+ * runs AFTER preflight, so the real count does not exist yet. Making it real means
316
+ * either moving preflight after the suite parse (it currently gates that parse) or
317
+ * adding a second reader of `evals.json` — and two readers of the answer key is
318
+ * precisely the drift this module refuses elsewhere.
319
+ * 2. `renderPreflightSummary` returns only FAILING checks, and it is called only
320
+ * inside the `if (!preflightResult.passed)` branch — so on a passing run the
321
+ * estimate never reaches a terminal at all.
322
+ * 3. Preflight runs after the `--dry-run` short-circuit, so a dry run never gets here.
323
+ *
324
+ * The consequence is that {@link buildDryRunSummary}'s spawn-count line is the ONLY
325
+ * pre-spend number an operator ever sees. Fix the count before making the check
326
+ * reachable: a reachable estimate that says "1 evals × 2 config" is worse than a
327
+ * silent one, because it is quotable.
236
328
  */
237
- export declare function flagDummyValueFor(flag: string): string;
238
329
  export declare function buildPreflightInput(evalsPath: string, pluginDirs: string[], opts: RunHarnessOptions, knobs: {
239
330
  maxBudgetUsd: number;
240
331
  }): PreflightInput;
@@ -255,6 +346,13 @@ export declare function isAcknowledged(opts: Pick<RunHarnessOptions, 'dryRun' |
255
346
  * Format a friction report for human consumption — one line per entry as
256
347
  * `[<severity>] <category>: <message>`. Pure; returns the empty string for no
257
348
  * entries so the caller can skip emitting anything.
349
+ *
350
+ * `severity` and `category` are closed enums vat owns, so they are safe to
351
+ * interpolate. `message` is GRADER text and is sanitized here even though
352
+ * {@link parseEvalFragment} already sanitized it on the way in — this is a
353
+ * second, independent boundary, not a redundant one: {@link emitFrictionReport}
354
+ * re-reads `friction.json` from the harness results dir, which same-uid skill
355
+ * code can reach and rewrite after vat wrote it.
258
356
  */
259
357
  export declare function formatFrictionReport(items: readonly FrictionItem[]): string;
260
358
  /**
@@ -322,17 +420,65 @@ export declare function resolveEvalSuitePath(input: {
322
420
  holdDir: string;
323
421
  subjectEvalSuiteHeld: boolean;
324
422
  }): string | undefined;
423
+ /**
424
+ * The per-run executor workspace root: `<tmp>/vat-skill-test-ws-<dirToken>/`.
425
+ *
426
+ * Deliberately OUTSIDE `harnessRoot`, alongside {@link resolveGraderOutDir} and
427
+ * {@link resolveEvalSuiteHoldDir}. The executor's cwd lives under here, and the
428
+ * harness root is where vat stages runnable copies of the skill (`staged/` and
429
+ * the assembled plugin dir). While the two were parent and child, the skill-absent
430
+ * arm of a `--baseline` run sat one `ls ..` away from the treatment it was
431
+ * supposed to be denied — the control could reach vat's own copy without ever
432
+ * leaving its working directory.
433
+ *
434
+ * Named by an unpredictable token, like the other vat-only dirs, and removed by
435
+ * the same cleanup. Pure (derives a path only).
436
+ */
437
+ export declare function resolveWorkspacesRoot(dirToken: string): string;
325
438
  /** Parse the staged eval suite and materialize each eval's input `files` into
326
- * `<harnessRoot>/workspaces/<id>/`. Returns the workspaces root, the parsed
327
- * {@link EvalSuite} (so the eval loop has the entries without re-reading), and the
328
- * declared eval count (derived from the suite). The dir is wiped first so a reused
329
- * harness root cannot leak a prior run's inputs. Throws {@link EvalInputError}
330
- * (mapped by the caller to exit 2) on a bad suite or a missing input file. */
331
- export declare function stageWorkspacesForRun(evalsPath: string, harnessRoot: string): {
439
+ * `<workspacesRoot>/<arm>/<id>/`, once per arm this run will execute. Returns the
440
+ * workspaces root, the parsed {@link EvalSuite} (so the eval loop has the entries
441
+ * without re-reading), and the declared eval count (derived from the suite). The
442
+ * dir is wiped first so a reused root cannot leak a prior run's inputs. Throws
443
+ * {@link EvalInputError} (mapped by the caller to exit 2) on a bad suite or a
444
+ * missing input file. */
445
+ /**
446
+ * Mint this run's opaque per-arm workspace directory segments.
447
+ *
448
+ * Independent random tokens, NOT the arm names and not derived from one another:
449
+ * the segment ends up in the executor's cwd, which the prompt states verbatim, so
450
+ * a name like `without` unblinds the control arm outright — and a name like `with`
451
+ * hands it a guessable path to the treatment arm's live output one directory over.
452
+ * 8 bytes is ample for "not guessable within a run that lasts minutes"; these are
453
+ * blinding/anti-enumeration tokens, not secrets protecting data at rest.
454
+ */
455
+ export declare function mintArmWorkspaceDirs(baseline: boolean): ArmWorkspaceDirs;
456
+ export declare function stageWorkspacesForRun(evalsPath: string, workspacesRoot: string, armDirs: ArmWorkspaceDirs): {
332
457
  workspacesRoot: string;
333
458
  declaredEvalCount: number;
334
459
  suite: EvalSuite;
335
460
  };
461
+ interface ResolveDeclaredChildEnvInput {
462
+ opts: RunHarnessOptions;
463
+ resolvedAuth: ResolvedAuth;
464
+ subjectStagedDir: string;
465
+ harnessRoot: string;
466
+ resultsDir: string;
467
+ evalsSubpath: string;
468
+ subjectPluginRoot: string | null;
469
+ /** The eval's staged input workspace — what `${fixturesDir}` resolves under. */
470
+ workspaceDir?: string;
471
+ /** Suppress the stderr transparency line (emitted once, by the first eval). */
472
+ quiet?: boolean;
473
+ }
474
+ /** Everything needed to re-assemble the executor env once an eval's workspace is known. */
475
+ interface PerEvalEnvAssembly {
476
+ input: Omit<ResolveDeclaredChildEnvInput, 'workspaceDir' | 'quiet'>;
477
+ /** Mutable latch so exactly one eval emits the stderr transparency line. */
478
+ logged: {
479
+ done: boolean;
480
+ };
481
+ }
336
482
  /** Inputs for the dry-run summary string. */
337
483
  export interface DryRunSummaryInput {
338
484
  /** True when the resolved subject is buildable (a real run would build + stage it). */
@@ -342,7 +488,14 @@ export interface DryRunSummaryInput {
342
488
  * rebuilding (may be stale); false = no dist existed, fell back to source dir.
343
489
  */
344
490
  dryRunStagedExistingDist?: boolean;
345
- /** Absolute path to the written provenance.json (already on disk). */
491
+ /**
492
+ * Absolute path where a REAL run would write provenance.json.
493
+ *
494
+ * Not written by a dry run, and the summary line says so. It used to be written —
495
+ * from the same block that wiped the previous run's grading/friction/baseline
496
+ * artifacts — which made "preview the cost" a destructive operation against the
497
+ * results the operator was about to read.
498
+ */
346
499
  provenancePath: string;
347
500
  /** Content fingerprint from the staged manifest. */
348
501
  provenanceFingerprint: string;
@@ -352,10 +505,29 @@ export interface DryRunSummaryInput {
352
505
  modelFlag: string;
353
506
  /** Number of declared evals a real run would execute. */
354
507
  evalCount: number;
508
+ /**
509
+ * Whether `--baseline` is set, i.e. whether every eval runs TWICE.
510
+ *
511
+ * Without this the preview reported the SUITE SIZE and called it the spawn count,
512
+ * which under `--baseline` under-reports the run by exactly 2x: a 3-eval suite
513
+ * previewed as "3 evals as executor→grader spawn pairs" and then launched 6 pairs
514
+ * — twelve `claude` sessions. This is the ONLY pre-spend number an operator ever
515
+ * sees (see {@link buildPreflightInput} for why the other one never reaches a
516
+ * terminal), so a 2x understatement here is the whole cost estimate being wrong.
517
+ */
518
+ baseline: boolean;
355
519
  /** Bounded-parallel executor→grader pipeline width a real run would use. */
356
520
  concurrency: number;
357
521
  /** Pinned grader/judge model a real run would grade with. */
358
522
  graderModel: string;
523
+ /**
524
+ * The PER-SPAWN budget ceiling (`--max-budget-usd`), not a run total. Reported
525
+ * because the two facts only mean something together: the worst case is
526
+ * `pairs × 2 × maxBudgetUsd`, and an operator who reads the flag name as a run
527
+ * budget under-provisions by the session count — which under `--baseline` is
528
+ * four times the eval count.
529
+ */
530
+ maxBudgetUsd: number;
359
531
  }
360
532
  /**
361
533
  * Build the "stale dist" warning lines for a `--dry-run` preview that staged an
@@ -372,19 +544,6 @@ export interface DryRunSummaryInput {
372
544
  * than copying the string.
373
545
  */
374
546
  export declare function buildStaleDistWarningLines(roleLabel?: string): string[];
375
- /**
376
- * Build the dry-run summary string. Pure function so it can be unit-tested
377
- * without running the full harness.
378
- *
379
- * The summary covers three scenarios:
380
- * 1. Declared (buildable) subject — no dist existed, fell back to source dir.
381
- * 2. Declared (buildable) subject — existing dist was staged WITHOUT rebuilding
382
- * (potentially stale).
383
- * 3. Plain source subject — staged as-is, no build step.
384
- *
385
- * Always includes the assembled spawn command, the staged-manifest entry count +
386
- * fingerprint, and the provenance.json path so a stale tree is visible at a glance.
387
- */
388
547
  export declare function buildDryRunSummary(input: DryRunSummaryInput): string;
389
548
  export interface CleanupHarnessOptions {
390
549
  /** User asked to retain the harness dir (`--keep`) — never remove. */
@@ -397,11 +556,32 @@ export interface CleanupHarnessOptions {
397
556
  created: boolean;
398
557
  }
399
558
  /**
400
- * Remove the harness directory after a run so staged untrusted skill bytes and
401
- * prompts do not accumulate in OS tmp. No-op when the user asked to keep it, when
402
- * the dir is a user-supplied location (`--out`/`--workdir`), or when it is already
403
- * gone. Idempotent and never throws — it runs from a `finally`, so it must not
404
- * mask the run's real outcome.
559
+ * The one child of the harness root that cleanup NEVER removes. Cleanup exists to
560
+ * evict staged untrusted skill bytes and prompts from OS tmp; `results/` is the
561
+ * opposite — it is the run's product, written solely by vat.
562
+ */
563
+ export declare const RETAINED_RESULTS_DIRNAME = "results";
564
+ /**
565
+ * Remove the harness directory's staged contents after a run so untrusted skill
566
+ * bytes and prompts do not accumulate in OS tmp. No-op when the user asked to keep
567
+ * it, when the dir is a user-supplied location (`--out`/`--workdir`), or when it is
568
+ * already gone. Idempotent and never throws — it runs from a `finally`, so it must
569
+ * not mask the run's real outcome.
570
+ *
571
+ * `results/` SURVIVES. It holds `grading.json`, `friction.json`, `tool-eval.json`
572
+ * and — for a `--baseline` run — `baseline.json`, i.e. every artifact the command's
573
+ * own help text tells the operator to go read. Removing the whole root on the
574
+ * DEFAULT invocation (no `--out`, no `--workdir`, no `--keep`) deleted those files
575
+ * inside this function's own `finally`, before `run.ts` had printed a single line:
576
+ * the operator was handed a `Harness:` path to a directory that no longer existed.
577
+ * That is the invocation `vat-skill-testing.md`'s copy-paste example uses.
578
+ *
579
+ * The retention is deliberately UNCONDITIONAL rather than `--baseline`-only: every
580
+ * run's verdict detail lives in the same directory, and a rule that depends on a
581
+ * flag is a rule that will drift away from the flag. The cost is bounded — the
582
+ * harness root is a deterministic function of the subject set, so a re-run reuses
583
+ * (and {@link wipeStaleArtifacts} clears) the same `results/`, rather than
584
+ * accumulating one per run.
405
585
  *
406
586
  * SAFETY: re-asserts the root is not a symlink immediately before removal (via
407
587
  * `lstat`, which does NOT follow the link). A root swapped to a symlink between
@@ -412,7 +592,7 @@ export declare function cleanupHarness(harnessRoot: string, opts: CleanupHarness
412
592
  /** One unit of executor→grader work: an eval + which arm (with/without skill). */
413
593
  export interface EvalWorkItem {
414
594
  entry: EvalEntry;
415
- arm: 'with' | 'without';
595
+ arm: EvalArm;
416
596
  }
417
597
  /**
418
598
  * Build the per-eval work items for a set of evals (one tier's worth, or a whole
@@ -455,18 +635,264 @@ export declare function partitionFragmentsByArm(fragments: EvalFragment[]): {
455
635
  */
456
636
  export declare function resolveGraderOutDir(dirToken: string): string;
457
637
  /**
458
- * The executor working directory for one eval: its staged input workspace
459
- * `<workspacesRoot>/<id>` when the eval declares input `files`, else undefined
460
- * (the executor then defaults to the staged subject dir). Pure + unit-testable.
638
+ * The executor working directory for one eval ON ONE ARM: its staged input
639
+ * workspace `<workspacesRoot>/<arm>/<id>`, ALWAYS — empty when the eval declares
640
+ * no input `files` (see {@link stageEvalWorkspaces}).
641
+ *
642
+ * This used to return undefined for a file-less eval, which made the executor
643
+ * fall back to running inside the staged subject dir. That fallback was the
644
+ * quiet half of the `--baseline` control defect: the skill-absent arm's cwd
645
+ * was the skill.
646
+ *
647
+ * The per-arm segment closes the OTHER half, found a round later: without it both
648
+ * arms of one eval shared a single writable directory AND ran concurrently, so
649
+ * the control arm could read the treatment arm's output files and answer from
650
+ * them. Keyed on the eval id alone this is a pure function of the eval; the arm
651
+ * is what makes the two runs independent.
652
+ *
653
+ * The segment is an OPAQUE per-run token, never the arm's name — this path is
654
+ * quoted verbatim to the executor as its working directory, so `…/without/e1`
655
+ * told the control arm it was the control, and `../with/e1` told it where to look.
656
+ * See {@link mintArmWorkspaceDirs}. Pure + unit-testable.
461
657
  */
462
- export declare function resolvePerEvalWorkspaceDir(entry: EvalEntry, workspacesRoot: string): string | undefined;
658
+ export declare function resolvePerEvalWorkspaceDir(entry: EvalEntry, workspacesRoot: string, arm: EvalArm, armDirs: ArmWorkspaceDirs): string;
463
659
  /**
464
660
  * Best-effort removal of a vat-only tmp dir that lives OUTSIDE the harness root —
465
661
  * the grader fragment dir and the held eval suite. Never throws (it runs from
466
662
  * cleanup, where masking the run's real outcome would be worse than a leftover 0700
467
663
  * tmp dir) and refuses to follow a symlinked root.
468
664
  */
665
+ /**
666
+ * Run one cleanup step, swallowing whatever it throws.
667
+ *
668
+ * The discipline every other step in the harness `finally` already follows, stated
669
+ * once: a cleanup failure is not a run failure, and letting one out REPLACES an
670
+ * already-good result — verdict computed, artifacts written, summary composed — with
671
+ * an error and exit 1.
672
+ *
673
+ * ⚠️ A CALL-SITE GUARD IS THE WEAK FORM, and this helper is a last resort rather
674
+ * than the pattern to copy. It is invisible to the compiler and to the test suite:
675
+ * reverting either of its two original call sites to a bare call left the unit suite
676
+ * AND the integration suite green, so nothing but review stood between the fix and
677
+ * its own silent removal. `lock.release()` was therefore made non-throwing at its own
678
+ * definition instead (see {@link acquireHarnessLock}), which is a contract the type
679
+ * carries and no edit here can undo. That leaves ONE caller —
680
+ * `killAllActiveClaudeChildren`, which lives in `@vibe-agent-toolkit/utils` and is
681
+ * only contractually non-throwing as long as that package says so.
682
+ *
683
+ * The failure is reported on stderr rather than silently dropped: a lockfile that
684
+ * could not be removed will break the NEXT run with a "busy" error, and an operator
685
+ * who saw nothing here has no way to connect the two.
686
+ */
687
+ export declare function swallowCleanupFailure(step: () => void): void;
469
688
  export declare function removeVatOnlyDir(dir: string | undefined): void;
689
+ /** Everything one executor→grader pipeline worker needs, built once per run. */
690
+ interface EvalRunContext {
691
+ subjectStagedDir: string;
692
+ workspacesRoot: string;
693
+ /** This run's opaque per-arm workspace segments (see {@link mintArmWorkspaceDirs}). */
694
+ armDirs: ArmWorkspaceDirs;
695
+ pluginDirs: string[];
696
+ graderOutDir: string;
697
+ /**
698
+ * The vat-only dir a harvested eval suite is held in — the ANSWER KEY for a
699
+ * fetched-artifact subject. Carried on the context solely so the WITHOUT arm's
700
+ * contamination scan can look for it; nothing in the eval loop reads from it.
701
+ */
702
+ evalSuiteHoldDir: string;
703
+ /**
704
+ * Verbatim lines of the staged SKILL.md, for the skill-absent arm's scan. The
705
+ * only contamination signal that does not need the arm to name a path or an
706
+ * executable, and so the only cover an instruction-only skill has.
707
+ */
708
+ skillContentNeedles: readonly string[];
709
+ runNonce: string;
710
+ graderModel: string;
711
+ model?: string;
712
+ maxTurns: number;
713
+ maxBudgetUsd: number;
714
+ timeoutMs: number;
715
+ stallMs?: number;
716
+ /**
717
+ * Full assembled env (skill secrets included) for the executor spawn, WITHOUT
718
+ * the declared `env` injections — those carry `${fixturesDir}`, which names the
719
+ * eval's own workspace and so can only be resolved per eval (see `perEvalEnv`).
720
+ */
721
+ executorEnv: NodeJS.ProcessEnv;
722
+ /**
723
+ * Present only when the subject declares an `env` block. Re-assembles the child
724
+ * env per eval so `${fixturesDir}` resolves under THAT eval's workspace. The
725
+ * transparency line is emitted once, by whichever eval assembles first.
726
+ */
727
+ perEvalEnv: PerEvalEnvAssembly | undefined;
728
+ /** AUTH-ONLY env for the grader spawn (trusted vat infra loads no skill). */
729
+ graderEnv: NodeJS.ProcessEnv;
730
+ /** Declared executables (WITH-arm grader aid + the detector's path needles); absent when unreachable. */
731
+ declaredExecutables?: Array<{
732
+ name: string;
733
+ howInvoked: string;
734
+ kind: string;
735
+ path: string;
736
+ }>;
737
+ spawn?: typeof spawnHeadlessClaude;
738
+ /** Shared spend accumulator — each worker folds in its executor + grader session cost. */
739
+ costAccumulator: RunCostSummary;
740
+ /**
741
+ * The harness root, where vat stages runnable copies of the subject. Used ONLY
742
+ * as a contamination needle for the skill-absent arm (see baseline-integrity.ts):
743
+ * nothing points the control arm here any more, so a mention of this path in its
744
+ * transcript means it went looking and found vat's own copy.
745
+ */
746
+ harnessRoot: string;
747
+ }
748
+ /**
749
+ * The control arm ran out of rate-limit retries — record it and let the run live.
750
+ *
751
+ * The retryable case is handled correctly by {@link runEvalWorker} and must stay that
752
+ * way; this is the EXHAUSTED case, which used to be indistinguishable from it. A
753
+ * control executor rate-limited on every attempt threw after five retries, taking
754
+ * down a run in which both treatment executors and both treatment graders had already
755
+ * completed and been billed, leaving `results/` holding `provenance.json` and nothing
756
+ * else — the exact failure {@link runEvalWorker} exists to prevent, reached by the one
757
+ * error class it had to let through.
758
+ *
759
+ * The treatment arm still hard-fails: there is no treatment result to salvage, which
760
+ * is the same asymmetry {@link EvalWorkOutcome} documents.
761
+ *
762
+ * Wired as the pipeline's `onRetriesExhausted`, and `--baseline` — which doubles the
763
+ * spawn count — is the run most likely to need it.
764
+ */
765
+ export declare function rateLimitExhaustedOutcome(item: EvalWorkItem, error: RateLimitSignal): EvalWorkOutcome;
766
+ /**
767
+ * Strip the fields VAT attaches itself, before VAT attaches them.
768
+ *
769
+ * The grader's only input is the executor transcript, which untrusted skill code
770
+ * controls — so a prompt injection there could otherwise fabricate a
771
+ * `BASELINE CONTAMINATED` verdict carrying attacker-chosen strings into an
772
+ * operator-facing artifact, or (via `degraded`) claim a scan degraded that did not.
773
+ * Spreading VAT's value last is NOT sufficient: it is absent on a clean run, so it
774
+ * overwrites nothing — which is exactly why the omission direction matters more
775
+ * than the invention one for `degraded`. A grader that omits it cannot re-hide a
776
+ * degradation VAT detected, because VAT's value is spread after this strip; a
777
+ * grader that INVENTS it would otherwise stamp a blind-scan warning on a run that
778
+ * scanned properly, and an operator who learns to ignore that warning stops reading
779
+ * the real ones.
780
+ */
781
+ export declare function withoutGraderContamination(fragment: EvalFragment): EvalFragment;
782
+ /**
783
+ * What one work item produced: a graded fragment, or — CONTROL ARM ONLY — a
784
+ * recorded failure that the run carries on past.
785
+ *
786
+ * The asymmetry is the whole point and it is not a subjective call. A treatment-arm
787
+ * failure leaves nothing to salvage: `grading.json` IS the treatment arm, so a run
788
+ * that cannot produce it has produced nothing and may as well stop. A control-arm
789
+ * failure destroys only the second half of a comparison whose first half is already
790
+ * bought and paid for — and the artifact that reports that comparison already has a
791
+ * way to say "I do not exist here" (`delta: null`). Failing the whole run on a
792
+ * control-arm timeout threw away every treatment executor and grader spawn that had
793
+ * already completed, wrote no `grading.json` at all, and left `results/` holding
794
+ * only `provenance.json`.
795
+ */
796
+ type EvalWorkOutcome = {
797
+ readonly fragment: EvalFragment;
798
+ readonly controlFailure?: undefined;
799
+ } | {
800
+ readonly fragment?: undefined;
801
+ readonly controlFailure: BaselineControlArmFailure;
802
+ };
803
+ /**
804
+ * Lift the run's content needles off the STAGED SKILL.md.
805
+ *
806
+ * Staged, not source: the staged copy is byte-identical to what the treatment arm
807
+ * is given, so a needle taken from it is exactly the text an ambient copy of the
808
+ * same build would carry.
809
+ *
810
+ * The exclusion set is EVERY channel through which vat itself hands the arm text:
811
+ * the suite's prompts, its `expected_output`, its expectations, and the CONTENTS
812
+ * of the input `files` fixtures vat stages into the arm's workspace and instructs
813
+ * it to operate on. An adopter who quotes a sentence of their SKILL.md through any
814
+ * of the four would otherwise stamp every run contaminated, including the arm that
815
+ * merely read the input vat gave it — the skill's words arriving THROUGH vat are
816
+ * not the arm reaching the skill. The fixture channel is the one that was missed,
817
+ * and it is the worst of the four to get wrong: the summary attached to that
818
+ * verdict tells the operator to go uninstall an ambient copy of the plugin that
819
+ * does not exist.
820
+ *
821
+ * Fixtures are read from the STAGED workspace rather than the suite directory, for
822
+ * the same reason the SKILL.md is: that is byte-for-byte what the arm was handed.
823
+ *
824
+ * Returns `[]` when the file cannot be read. A missing SKILL.md is not this
825
+ * function's error to raise (staging has already validated the subject), and an
826
+ * unarmed signal is reported as unarmed rather than claimed as clean. An
827
+ * unreadable FIXTURE is skipped for the same reason, and errs the safe way: a
828
+ * fixture we could not exclude can only make the signal noisier, never blinder.
829
+ */
830
+ export declare function resolveSkillContentNeedles(subjectStagedDir: string, evals: readonly EvalEntry[],
831
+ /**
832
+ * Where the staged fixtures live, as the two inputs an arm root is DERIVED from
833
+ * rather than as the pre-joined path itself — see {@link armWorkspaceRoot}. Taking
834
+ * a bare `string` here let the call site pass `workspacesRoot` (one level too high,
835
+ * so every `<id>/` fixture lookup missed) with the whole unit suite still green.
836
+ *
837
+ * The `with` arm, because the fixture BYTES are what matter and both arms are
838
+ * staged from the same source — and `with` is the only arm every run mints.
839
+ */
840
+ workspaces: {
841
+ workspacesRoot: string;
842
+ armDirs: ArmWorkspaceDirs;
843
+ }): string[];
844
+ /**
845
+ * Baseline integrity for ONE skill-absent eval: vat has removed its OWN copies
846
+ * from the control arm's reach, but an ambient copy in the adopter's repo or
847
+ * installed plugin cache is not vat's to remove — so the control arm can still,
848
+ * in principle, find and run the skill. Detect it from the transcript and return
849
+ * the fragment patch that carries the evidence to the merge step, which turns it
850
+ * into a loud caveat on baseline.json. A wrong number that announces itself is
851
+ * recoverable; a silent one gets believed.
852
+ *
853
+ * Returns an empty patch when clean, so the caller spreads it unconditionally.
854
+ */
855
+ /**
856
+ * Exactly the per-run values {@link buildContaminationInput} reads. A `Pick`, not a
857
+ * fresh interface, so it cannot drift from the context the run actually assembles.
858
+ */
859
+ export type ContaminationCtx = Pick<EvalRunContext, 'armDirs' | 'workspacesRoot' | 'harnessRoot' | 'evalSuiteHoldDir' | 'graderOutDir' | 'skillContentNeedles' | 'declaredExecutables'>;
860
+ /**
861
+ * Build the detector input for the skill-absent arm. ONE builder, so the hits
862
+ * written per eval and the `signals` list stamped on the run-level integrity
863
+ * block are derived from the same values — a second construction site is how the
864
+ * block would come to claim a detector the scan never actually ran.
865
+ *
866
+ * `transcript` and `evalId` are supplied per call; everything else is per-run.
867
+ *
868
+ * Takes a NARROWED context rather than the whole {@link EvalRunContext} so the
869
+ * builder is directly unit-testable — the detector honours `armCwd` (dropping it
870
+ * there kills three tests) while the wiring that SUPPLIES it was droppable green,
871
+ * and a dropped `armCwd` anchors every relative climb one directory too high, where
872
+ * no needle can see it.
873
+ *
874
+ * `evalId` is what turns the arm's workspace ROOT into the arm's actual starting
875
+ * CWD (`<armWorkspaceDir>/<evalId>` — see {@link resolvePerEvalWorkspaceDir}).
876
+ * The detector needs both and they are one directory apart: containment is judged
877
+ * against the root (the arm's whole tree is its own), while the cwd walk must
878
+ * start exactly where the executor did, or every relative climb in the transcript
879
+ * resolves one level too high and lands where no needle can see it.
880
+ *
881
+ * `evalId` is REQUIRED here, and the signals-only caller gets its own entry point
882
+ * ({@link buildContaminationSignalsInput}) rather than sharing an optional third
883
+ * argument. An optional parameter is a pin the compiler cannot hold: dropping the
884
+ * argument at the SCAN call site left all 2270 unit tests green and was caught only
885
+ * by the integration suite — which this repo's fast implementer loop explicitly says
886
+ * not to run. That is the same shape as the dropped `armCwd` that anchored the whole
887
+ * walk one directory too high, also green.
888
+ */
889
+ export declare function buildContaminationInput(transcript: string, ctx: ContaminationCtx, evalId: string): DetectBaselineContaminationInput;
890
+ /**
891
+ * The same input built for the "which detectors are ARMED?" question, which is asked
892
+ * once per run with no transcript and no eval — so it can honestly supply neither,
893
+ * and `armCwd` is correctly absent rather than accidentally dropped.
894
+ */
895
+ export declare function buildContaminationSignalsInput(ctx: ContaminationCtx): DetectBaselineContaminationInput;
470
896
  /** The results/ artifacts vat is the SOLE writer of, resolved for one run. */
471
897
  export interface ArtifactPaths {
472
898
  gradingOut: string;
@@ -488,6 +914,85 @@ export declare function resolveArtifactPaths(resultsDir: string): ArtifactPaths;
488
914
  * (`force: true`) — a missing file is fine.
489
915
  */
490
916
  export declare function wipeStaleArtifacts(paths: ArtifactPaths): void;
917
+ /**
918
+ * Where a results artifact goes when it fails the fail-closed gate.
919
+ *
920
+ * The gate throws and `results/` is deliberately RETAINED, so the file that failed
921
+ * used to sit there under its authoritative name — the one artifact whose schema
922
+ * violation is BY DEFINITION a harness bug, indistinguishable in a CI archive from a
923
+ * good one until the next run's wipe. Renamed rather than deleted: the bytes are what
924
+ * a maintainer needs in order to see which refine tripped, and a name ending in
925
+ * `.rejected` cannot be mistaken for the artifact a reader came for.
926
+ */
927
+ export declare function rejectedArtifactPath(path: string): string;
928
+ /**
929
+ * Merge the run's grader fragments and WRITE the run artifacts (vat is the SOLE
930
+ * writer): grading.json from the WITH-arm fragments, friction.json from ALL
931
+ * fragments, tool-eval.json from the WITH-arm fragments' `tool` verdicts (a
932
+ * SEPARATE channel — C2; the WITHOUT arm carries no toolExpectations), and — only
933
+ * when a WITHOUT arm ran (baseline) — baseline.json from the WITHOUT-arm fragments
934
+ * (informational A/B, never part of the verdict). Stale copies from a reused harness
935
+ * were already removed by {@link wipeStaleArtifacts} pre-pipeline. Returns the
936
+ * reconciled prose-expectation verdict AND the merged tool-eval report so the caller
937
+ * can compute the COMPOSITE verdict. Every fragment's per-run nonce is re-verified
938
+ * inside {@link mergeFragmentsToGrading}.
939
+ */
940
+ /**
941
+ * Collect per-eval baseline-integrity findings from the WITHOUT-arm fragments.
942
+ * Fragments with no `contamination` are clean and contribute nothing. Pure.
943
+ */
944
+ /**
945
+ * What one arm graded, per eval — the SINGLE derivation behind both the parity check
946
+ * ({@link armExpectationSkew}, which reads `total`) and the A/B delta (which reads
947
+ * `passed`). Deriving those separately would let the two blocks in `baseline.json`
948
+ * disagree about the same arm. Pure.
949
+ *
950
+ * Exported for its own unit test, not for reuse. Setting `passed` to `total` here
951
+ * failed zero unit tests and three integration ones: every unit fixture graded
952
+ * everything it declared, so the two fields were numerically identical in all of
953
+ * them and the ONE thing this function exists to keep distinct was unpinned.
954
+ */
955
+ export declare function gradedCounts(fragments: EvalFragment[]): ArmEvalGrade[];
956
+ /**
957
+ * The stderr report for a `--baseline` run: the delta line, then the ⚠️ banner when
958
+ * the run is unusable. Pure — the caller decides WHEN it goes out, which is the
959
+ * whole point (see the call site).
960
+ *
961
+ * Composed on EVERY baseline run, including a withheld one: "the delta is
962
+ * unavailable, and here is why" is the report an operator needs, and a run that
963
+ * silently prints nothing is indistinguishable from a vat that still does not
964
+ * compute a delta at all. The delta line comes FIRST so an unusable run reads as the
965
+ * number followed by the caveat that explains it — the banner is the only line that
966
+ * says the number means nothing, so it must never be the one scrolled away.
967
+ *
968
+ * ⚠️ IT IS NOT LITERALLY LAST ON THE SCREEN, and this docblock used to say it was.
969
+ * The CLI prints `Harness:`, `Results:`, `Workspaces:` and `Summary:` AFTER the
970
+ * harness returns, so 2–4 lines always follow the banner. What this ordering buys is
971
+ * that the banner is not separated from its delta by up to ~1250 rows of friction —
972
+ * a bounded, short tail after it, not none. Moving those CLI lines above the harness
973
+ * report is the CLI's call, not this module's.
974
+ *
975
+ * THREE conditions have to hold before the caveat is dropped, not two. A DEGRADED
976
+ * scan is neither contaminated nor incomparable, so a two-clause gate printed
977
+ * `Baseline delta: +0 (with skill: 2/2, without skill: 2/2).` and exited 0 while
978
+ * `baseline.json` carried `degraded: [...]` and an `integrity.summary` ending in a
979
+ * `⚠️ DEGRADED SCAN: …` sentence that was composed, written to the artifact, and
980
+ * never emitted anywhere. Everything this module says about telling "checked and clean"
981
+ * apart from "checked with the blunt instrument" was true of the artifact and false
982
+ * of the terminal — and the terminal is the only surface an operator reads.
983
+ *
984
+ * A CONTAMINATED run still reports its number, deliberately. Contamination does not
985
+ * make the subtraction illegal — both arms were graded to the same depth, so the
986
+ * arithmetic is sound; what it invalidates is INTERPRETING that number as skill
987
+ * lift, which is precisely what the ⚠️ line says ("The reported delta is NOT a
988
+ * measure of skill lift"), and what `vat-skill-testing.md` turns into the
989
+ * instruction "discard the delta". Withholding it here would (a) leave that sentence
990
+ * referring to a number that was never reported, and (b) spend `null` — which means
991
+ * "these arms cannot be subtracted", a different failure with a different remedy —
992
+ * on a run where they can be. A contaminated delta is also diagnostic in its own
993
+ * right: it collapses toward zero, which is corroboration, not noise.
994
+ */
995
+ export declare function formatBaselineReport(delta: BaselineDelta, integrity: BaselineIntegrity): string;
491
996
  /**
492
997
  * The COMPOSITE run verdict (issue #145 Phase T): the run passes only when BOTH
493
998
  * the prose-expectation grading passed AND every tool-expectation verdict passed.
@@ -540,9 +1045,47 @@ export declare function buildRunSummary(verdict: GradingVerdict, toolEval: ToolE
540
1045
  * the call site), so the base line is already FAIL when the note is present.
541
1046
  */
542
1047
  export declare function buildRunSummaryWithSkips(verdict: GradingVerdict, toolEval: ToolEvalReport, compositeAllPassed: boolean, skipped: SkippedEvalsSummary | undefined, cost?: RunCostSummary): string;
1048
+ /**
1049
+ * Post-merge D2 gate over the vat-written results artifacts (vat is SOLE writer):
1050
+ * grading.json, friction.json, tool-eval.json — and, on a baseline run, baseline.json
1051
+ * — must each exist + parse + validate. A separate, explicit check (NOT folded into
1052
+ * any prompt-invariant linter) — fail-closed and safe because vat wrote every one of
1053
+ * these itself just above.
1054
+ *
1055
+ * `baseline.json` was EXEMPT from this gate for as long as the gate has existed, and
1056
+ * it is the artifact the exemption fits worst: on a `--baseline` run it is the only
1057
+ * durable record of the thing the command sells, and this branch has already been
1058
+ * bitten once by a default run deleting `results/` out from under it. Its validation
1059
+ * is also the strictest of the four — {@link BaselineArtifactSchema} carries the
1060
+ * `passed <= total` and `delta === with − without` refines, so a merge that produced
1061
+ * a self-contradicting number fails the run here instead of being written down.
1062
+ *
1063
+ * `baselineRun` gates it rather than "does the file exist", deliberately: an
1064
+ * existence check would pass on the one failure that matters (vat asked for a
1065
+ * baseline and wrote nothing), which is exactly what a fail-CLOSED gate must not do.
1066
+ * A non-baseline run writes no baseline.json at all and is not asked about one.
1067
+ */
1068
+ export declare function assertVatWroteArtifacts(paths: ArtifactPaths, baselineRun: boolean): void;
543
1069
  /**
544
1070
  * Domain orchestrator for `vat skill test run`. Pure of process.exit — all
545
1071
  * exit-code decisions live in the caller (run.ts).
546
1072
  */
1073
+ /**
1074
+ * Resolve WHERE this run lives, and whether the harness owns that location.
1075
+ *
1076
+ * `--out` names the harness root exactly; `--workdir` names the BASE the root is
1077
+ * derived under. They are mutually exclusive: passing both used to silently
1078
+ * discard `--workdir` — the run went entirely to `--out` and the `--workdir` path
1079
+ * was never even created — so an operator using the pair to separate the
1080
+ * executor's cwd from the staged trees got no separation and no warning.
1081
+ *
1082
+ * `harnessCreated` is true only for the derived-under-OS-tmp case; an explicit
1083
+ * `--out` or `--workdir` is a location the user owns and is never auto-removed.
1084
+ */
1085
+ export declare function resolveHarnessLocation(opts: Pick<RunHarnessOptions, 'out' | 'workdir' | 'subject'>): {
1086
+ harnessRoot: string;
1087
+ harnessCreated: boolean;
1088
+ };
547
1089
  export declare function runSkillTestHarness(opts: RunHarnessOptions): Promise<RunHarnessResult>;
1090
+ export {};
548
1091
  //# sourceMappingURL=run-harness.d.ts.map