@skill-harness/adapters 0.9.0 → 0.11.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.
package/dist/pi-json.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
- import { parseTrace } from "@skill-harness/core";
3
+ import { parseTrace, providerFailureFromJsonLine } from "@skill-harness/core";
4
4
  /**
5
5
  * Run `pi --mode json` and build an execution trace, **streaming**.
6
6
  *
@@ -28,12 +28,14 @@ export function runPiJson(opts) {
28
28
  return new Promise((resolve, reject) => {
29
29
  const child = spawn("pi", opts.args, {
30
30
  cwd: opts.cwd,
31
+ env: opts.env,
31
32
  // stdin from /dev/null: pi hangs waiting on it otherwise, and a hang in a
32
33
  // wave is indistinguishable from a slow model until the timeout fires.
33
34
  stdio: ["ignore", "pipe", "pipe"],
34
35
  });
35
36
  const kept = [];
36
37
  let stderr = "";
38
+ let providerFailure = null;
37
39
  let settled = false;
38
40
  const timer = setTimeout(() => {
39
41
  if (settled)
@@ -58,6 +60,8 @@ export function runPiJson(opts) {
58
60
  if (SKIPPED_TYPE_RE.test(line))
59
61
  return;
60
62
  kept.push(line);
63
+ if (providerFailure === null)
64
+ providerFailure = providerFailureFromJsonLine(line);
61
65
  });
62
66
  child.stderr.on("data", (chunk) => {
63
67
  if (stderr.length < MAX_STDERR_CHARS)
@@ -85,7 +89,7 @@ export function runPiJson(opts) {
85
89
  changedPaths: opts.changedPaths,
86
90
  homeDir: opts.homeDir,
87
91
  });
88
- resolve({ ...parsed, code, stderr: stderr.slice(0, MAX_STDERR_CHARS) });
92
+ resolve({ ...parsed, code, stderr: stderr.slice(0, MAX_STDERR_CHARS), providerFailure });
89
93
  });
90
94
  });
91
95
  }
package/dist/pi.js CHANGED
@@ -3,8 +3,24 @@ import { tmpdir, homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { runPiJson } from "./pi-json.js";
5
5
  import { collectTrajectorySources, normalizePiTraces, resequence } from "./trajectory.js";
6
- import { exec, onPath, envNum, traceSha256 } from "@skill-harness/core";
6
+ import { exec, onPath, envNum, traceSha256, withProviderFailure } from "@skill-harness/core";
7
7
  const PI_TIMEOUT_MS = envNum("PI_TIMEOUT_MS", 300_000);
8
+ /**
9
+ * stderr fragments that mean the provider refused the request, so the run measured
10
+ * nothing about the model. Substring matching on a message pi passes through from
11
+ * the provider — deliberately narrow: a stderr line we cannot classify stays an
12
+ * ordinary non-zero exit, because calling a real model failure "infrastructure"
13
+ * would hide a regression.
14
+ */
15
+ const PROVIDER_STDERR_SIGNATURES = [
16
+ "invalidated oauth token",
17
+ "invalid_api_key",
18
+ "insufficient_quota",
19
+ ];
20
+ function providerStderr(stderr) {
21
+ const hay = stderr.toLowerCase();
22
+ return PROVIDER_STDERR_SIGNATURES.some((sig) => hay.includes(sig)) ? stderr.trim() : null;
23
+ }
8
24
  /**
9
25
  * Refuse to hand pi a skill dir it will silently ignore.
10
26
  *
@@ -127,26 +143,46 @@ export const piAdapter = {
127
143
  : skillFlags(req.mode, req.skillDir);
128
144
  const total = req.turns.length;
129
145
  const parts = [];
146
+ // The arm's env, merged over the harness's own — undefined (not `process.env`)
147
+ // when there is none, so `exec`'s `env: opts.env ?? process.env` inherits
148
+ // normally and the control arm is unaffected.
149
+ const env = req.armEnv ? { ...process.env, ...req.armEnv } : undefined;
150
+ // Collected across turns and written by `withProviderFailure` into the
151
+ // transcript PREAMBLE at the end, never inline after an assistant turn: the
152
+ // preamble is the only region of the transcript the model provably cannot
153
+ // reach, and a marker anywhere else is forgeable by a model that types the
154
+ // words (which would convert a FAIL into ERROR and mute the judge forever).
155
+ let providerFailure = null;
130
156
  if (total === 1) {
131
157
  const args = [...flags, ...common, "--no-session", "-p", req.turns[0]];
132
- const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS });
158
+ const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS, env });
133
159
  parts.push(header(1, 1, req.turns[0]));
134
160
  parts.push(`<<< ASSISTANT:\n${r.stdout.trim()}\n`);
135
- if (r.code !== 0)
136
- parts.push(`[pi exited ${r.code}]\n${r.stderr.trim()}\n`);
137
- return parts.join("\n");
161
+ if (r.code !== 0) {
162
+ providerFailure = providerStderr(r.stderr);
163
+ if (!providerFailure)
164
+ parts.push(`[pi exited ${r.code}]\n${r.stderr.trim()}\n`);
165
+ }
166
+ return withProviderFailure(parts.join("\n"), providerFailure);
138
167
  }
139
168
  const session = mkdtempSync(join(tmpdir(), "sc-pi-session-"));
140
169
  for (let i = 0; i < total; i++) {
141
170
  const turnFlags = i === 0 ? ["--session-dir", session] : ["--session-dir", session, "-c"];
142
171
  const args = [...flags, ...common, ...turnFlags, "-p", req.turns[i]];
143
- const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS });
172
+ const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS, env });
144
173
  parts.push(header(i + 1, total, req.turns[i]));
145
174
  parts.push(`<<< ASSISTANT:\n${r.stdout.trim()}\n`);
146
- if (r.code !== 0)
147
- parts.push(`[pi exited ${r.code} on turn ${i + 1}]\n${r.stderr.trim()}\n`);
175
+ if (r.code !== 0) {
176
+ const provider = providerStderr(r.stderr);
177
+ // First failure wins, same as the structured path: the turns after an
178
+ // outage are downstream of it, not independent evidence.
179
+ if (provider && providerFailure === null)
180
+ providerFailure = provider;
181
+ if (!provider)
182
+ parts.push(`[pi exited ${r.code} on turn ${i + 1}]\n${r.stderr.trim()}\n`);
183
+ }
148
184
  }
149
- return parts.join("\n");
185
+ return withProviderFailure(parts.join("\n"), providerFailure);
150
186
  },
151
187
  /**
152
188
  * Structured run: same flags, same turn loop, plus `--mode json` and a trace
@@ -178,6 +214,10 @@ export const piAdapter = {
178
214
  const traces = [];
179
215
  const parts = [];
180
216
  const session = total === 1 ? null : mkdtempSync(join(tmpdir(), "sc-pi-session-"));
217
+ let providerFailure = null;
218
+ // Same merge as `run()`: undefined when the arm carries no env, so `spawn`
219
+ // (which treats `undefined` as "inherit") leaves the control arm untouched.
220
+ const env = req.armEnv ? { ...process.env, ...req.armEnv } : undefined;
181
221
  for (let i = 0; i < total; i++) {
182
222
  const turnFlags = session === null
183
223
  ? ["--no-session"]
@@ -196,6 +236,7 @@ export const piAdapter = {
196
236
  rep: req.rep ?? 0,
197
237
  turn: i,
198
238
  homeDir: homedir(),
239
+ env,
199
240
  });
200
241
  // A stream with no terminal events at all is not evidence of a clean run.
201
242
  // Fail loudly here rather than let an empty trace satisfy a `forbid_calls`
@@ -210,6 +251,14 @@ export const piAdapter = {
210
251
  r.trace.capture_errors = [`pi JSONL contained ${r.malformedLines} malformed line(s); absence-based trace assertions are unsafe`];
211
252
  r.trace.trace_sha256 = traceSha256(r.trace);
212
253
  }
254
+ // Recorded here, written into the transcript preamble by
255
+ // `withProviderFailure` below — not just returned on `providerFailure`: the
256
+ // artifact on disk is the only thing a later `grade`/`regrade` call ever
257
+ // reads (see `judgeOneRep` in core/regrade.ts), and the structured path
258
+ // exits 0 while carrying the evidence, so a field a re-judge never sees
259
+ // leaves it unrecoverable from the saved transcript.
260
+ if (providerFailure === null && r.providerFailure)
261
+ providerFailure = r.providerFailure;
213
262
  traces.push(r.trace);
214
263
  parts.push(header(i + 1, total, req.turns[i]));
215
264
  parts.push(`<<< ASSISTANT:\n${r.trace.final_text.trim()}\n`);
@@ -235,10 +284,11 @@ export const piAdapter = {
235
284
  }
236
285
  const eventErrors = [...native.errors, ...chronologyErrors];
237
286
  return {
238
- transcript: parts.join("\n"),
287
+ transcript: withProviderFailure(parts.join("\n"), providerFailure),
239
288
  traces,
240
289
  events: resequence(combined),
241
290
  ...(eventErrors.length ? { eventErrors } : {}),
291
+ ...(providerFailure ? { providerFailure } : {}),
242
292
  };
243
293
  },
244
294
  /**
@@ -12,8 +12,45 @@ export declare function normalizePiTraces(traces: ExecutionTraceV1[]): Trajector
12
12
  /** Normalize principal-pi-skills' immutable assurance event schema v1.0. */
13
13
  export declare function normalizePrincipalAssuranceLedger(text: string): TrajectoryEventV1[];
14
14
  /**
15
- * Normalize both the current unversioned pi-daddy 0.17 grant ledger and the
16
- * explicit v1 governance supplement. Legacy omissions remain omissions: no
17
- * task/workspace/expiry field is ever inferred as successful governance.
15
+ * Normalize pi-daddy's public ledgers: unversioned 0.17 GrantRecord lines and
16
+ * ledgerVersion 2 runtime events emitted by 0.18.0. Version detection precedes
17
+ * the legacy fallback so a new event can never be misdiagnosed as an old grant.
18
18
  */
19
19
  export declare function normalizePiDaddyLedger(text: string): TrajectoryEventV1[];
20
+ /**
21
+ * pi-daddy's canonical refusal vocabulary, in the pinned contract's own order.
22
+ *
23
+ * This is a copy of `#/$defs/refusalCode` from the pinned schema and is exported
24
+ * so `pi-daddy-contract.test.ts` can assert set equality against the producer
25
+ * artifact — a hand-maintained second vocabulary without that drift assertion is
26
+ * exactly how `GRANT_ID_MALFORMED` came to be rejected as "unsupported".
27
+ */
28
+ export declare const V2_REFUSAL_CODES: Set<string>;
29
+ /**
30
+ * Every vocabulary the adapter restates from the pinned contract, paired with the
31
+ * place in the schema it must equal.
32
+ *
33
+ * The closed schema gates first, so a semantic check that has drifted *narrower*
34
+ * than the contract no longer opens a hole — it produces the opposite failure:
35
+ * a contract-valid record admitted by the schema and then thrown out by a stale
36
+ * harness set, which is precisely what `GRANT_ID_MALFORMED` did. One manifest, one
37
+ * test over all of it, so re-pinning cannot quietly leave a set behind.
38
+ */
39
+ export declare const V2_RESTATED_VOCABULARIES: ReadonlyArray<{
40
+ name: string;
41
+ kind: "enum" | "propertyNames" | "discriminators" | "typeNames" | "numericPropertyNames";
42
+ pointer: string;
43
+ values: ReadonlySet<string>;
44
+ }>;
45
+ /**
46
+ * Harness-side subsets of a contract vocabulary, not restatements of one. They encode
47
+ * the harness's own semantics — which lease outcomes a receipt may be appended after,
48
+ * and which may precede that release — so the assertion on them is containment, not
49
+ * equality. Anything the test *equality*-asserts belongs in the manifest above
50
+ * instead; membership here is a claim that the harness deliberately holds a subset.
51
+ */
52
+ export declare const V2_VOCABULARY_SUBSETS: ReadonlyArray<{
53
+ name: string;
54
+ pointer: string;
55
+ values: ReadonlySet<string>;
56
+ }>;