@skill-harness/core 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,7 +37,18 @@ export function withExecutionFailure(transcript, failure) {
37
37
  return failure ? `${EXECUTION_FAILURE_MARKER} ${failure}\n\n${transcript}` : transcript;
38
38
  }
39
39
  export function executionFailureFromTranscript(transcript) {
40
- return failureFromPreamble(transcript, EXECUTION_FAILURE_MARKER);
40
+ const failure = failureFromPreamble(transcript, EXECUTION_FAILURE_MARKER);
41
+ if (failure !== null)
42
+ return failure;
43
+ // Before 0.25.1, the runner saved thrown errors as this entire transcript,
44
+ // without a turn header. Match only its exact opening preamble: a model quote
45
+ // after a USER header (or elsewhere in the transcript) carries no authority.
46
+ const legacyPrefix = "[adapter failure] ";
47
+ if (transcript.startsWith(legacyPrefix)) {
48
+ const detail = transcript.slice(legacyPrefix.length).split("\n", 1)[0].trim();
49
+ return `adapter failure — ${detail || "adapter threw without a message"}`;
50
+ }
51
+ return null;
41
52
  }
42
53
  /**
43
54
  * The adapters' shared turn-header prefix (`>>> USER:` / `>>> USER (turn n/m):`),
package/dist/run.js CHANGED
@@ -16,7 +16,7 @@ import { judgeOneRep } from "./regrade.js";
16
16
  import { runDeliveryCanary, canaryFailure } from "./canary.js";
17
17
  import { boundaryCells, stabilityNote } from "./stability.js";
18
18
  import { aggregateMetrics } from "./metrics.js";
19
- import { providerFailureFromTranscript, executionFailureFromTranscript } from "./provider-failure.js";
19
+ import { providerFailureFromTranscript, executionFailureFromTranscript, withExecutionFailure } from "./provider-failure.js";
20
20
  /**
21
21
  * The pi-daddy arm's delivery proof, by convention: `arms.yaml` points
22
22
  * `PI_GRANTS_LEDGER` at exactly this filename inside the run directory. A fixed
@@ -372,7 +372,7 @@ async function runRep(scenario, rep, repCount, ctx) {
372
372
  // Configuration errors do not arrive here: the two that used to be
373
373
  // thrown from inside this block (adapter cannot produce traces for a
374
374
  // gated scenario) are decided above, before the loop.
375
- adapterFailure = e instanceof Error ? e.message : String(e);
375
+ adapterFailure = (e instanceof Error ? e.message : String(e)) || "adapter threw without a message";
376
376
  transcript = `[adapter failure] ${adapterFailure}`;
377
377
  // Nothing partial from the failed attempt may be read as evidence: an
378
378
  // empty trace list satisfies a `forbid_calls` gate, and a stale diff
@@ -384,7 +384,7 @@ async function runRep(scenario, rep, repCount, ctx) {
384
384
  // A provider outage is infrastructure, never a model verdict. Checked on
385
385
  // every path: the text path carries the marker in the transcript, the
386
386
  // structured path sets `providerFailure` above and exits 0 while doing it.
387
- if (!infrastructureFailure) {
387
+ if (!infrastructureFailure && adapterFailure === null) {
388
388
  const provider = providerFailureFromTranscript(transcript);
389
389
  if (provider)
390
390
  infrastructureFailure = `provider failure — ${provider}`;
@@ -394,7 +394,8 @@ async function runRep(scenario, rep, repCount, ctx) {
394
394
  infrastructureFailure = `execution failure — ${execution}`;
395
395
  }
396
396
  }
397
- executionUnavailable ||= executionFailureFromTranscript(transcript) !== null;
397
+ // Thrown errors keep their one fresh retry, unlike a returned unavailable final.
398
+ executionUnavailable ||= adapterFailure === null && executionFailureFromTranscript(transcript) !== null;
398
399
  // Qualified complete traces prove delivered bytes even if those bytes
399
400
  // are only whitespace; the legacy text-only timeout heuristic cannot erase them.
400
401
  const deliveredText = traces.length > 0 && traces.every((trace) => trace.final_status === "complete" && trace.final_text.length > 0 && !trace.capture_errors?.length);
@@ -405,8 +406,11 @@ async function runRep(scenario, rep, repCount, ctx) {
405
406
  }
406
407
  // Survived the retry: ERROR for this rep, inside a run that still completes
407
408
  // and still writes every other scenario's verdict.
408
- if (adapterFailure && !infrastructureFailure) {
409
- infrastructureFailure = `adapter failure — ${adapterFailure}`;
409
+ if (adapterFailure) {
410
+ infrastructureFailure ??= `adapter failure — ${adapterFailure}`;
411
+ // Persist only the exhausted failure. A recovered retry must retain its
412
+ // clean transcript; later grade/regate must never judge an unavailable run.
413
+ transcript = withExecutionFailure(transcript, `adapter failure — ${adapterFailure}`);
410
414
  }
411
415
  }
412
416
  const repSuffix = repCount > 1 ? rep : undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@skill-harness/core",
3
- "version": "0.25.0",
4
- "description": "skill-harness engine — spec, discover, run, LLM-judge grade, score, results (internal API)",
3
+ "version": "0.26.0",
4
+ "description": "skill-harness engine \u2014 spec, discover, run, LLM-judge grade, score, results (internal API)",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "./dist/index.js",