@skill-harness/adapters 0.29.0 → 0.31.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/README.md CHANGED
@@ -4,7 +4,7 @@ Harness adapters for [skill-harness](https://github.com/mojomanyana/skill-harnes
4
4
  the Pi subject/judge runner, the frozen pi-daddy 0.17/v2
5
5
  normalizer, and the separately pinned production ledger-v3 normalizer.
6
6
 
7
- Fresh subject execution requires **exactly Pi 1.0.4**. Different or unavailable
7
+ Fresh subject execution supports **Pi 1.0.4 and 1.1.0**. Other or unavailable
8
8
  versions produce execution ERROR before a subject call. Retained results remain
9
9
  readable; the historical Pi 0.83.0 raw parser is not current qualification.
10
10
 
package/dist/pi-json.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ExecutionTraceV1, ModelRef, RunMode } from "@skill-harness/core";
1
+ import type { ExecutionTraceV1, ModelRef, RunMode, ProcessObservation } from "@skill-harness/core";
2
2
  /**
3
3
  * Run `pi --mode json` and build an execution trace, **streaming**.
4
4
  *
@@ -9,10 +9,10 @@ import type { ExecutionTraceV1, ModelRef, RunMode } from "@skill-harness/core";
9
9
  * into a string (which is what the shared `exec()` helper does) would exhaust
10
10
  * memory partway through a long wave, taking the whole run with it.
11
11
  *
12
- * So this deliberately does NOT reuse `exec()`. The two quadratic event types are
12
+ * The lifecycle is shared with `exec()`, but capture is streaming. Quadratic events are
13
13
  * dropped as each line arrives, so the giant ones are never retained; the
14
- * remainder — a few KB of terminal events — is held until `close` and parsed
15
- * once. What this bounds is the 52 MB, not the residue.
14
+ * remainder is bounded by both bytes and record count. Oversized lines or retained
15
+ * evidence terminate the owned process and report an infrastructure error.
16
16
  */
17
17
  /**
18
18
  * The two quadratic event types, matched at the head of the object where pi
@@ -37,6 +37,11 @@ export interface PiJsonRunOptions {
37
37
  * as "inherit", so the control arm (which never sets this) is unaffected.
38
38
  */
39
39
  env?: NodeJS.ProcessEnv;
40
+ signal?: AbortSignal;
41
+ maxLineBytes?: number;
42
+ maxRetainedBytes?: number;
43
+ maxRecords?: number;
44
+ maxOutputBytes?: number;
40
45
  }
41
46
  export interface PiJsonRunResult {
42
47
  trace: ExecutionTraceV1;
@@ -51,5 +56,6 @@ export interface PiJsonRunResult {
51
56
  * verdict.
52
57
  */
53
58
  providerFailure: string | null;
59
+ observation?: ProcessObservation;
54
60
  }
55
61
  export declare function runPiJson(opts: PiJsonRunOptions): Promise<PiJsonRunResult>;
package/dist/pi-json.js CHANGED
@@ -1,6 +1,4 @@
1
- import { spawn } from "node:child_process";
2
- import { StringDecoder } from "node:string_decoder";
3
- import { parseTrace, providerFailureFromJsonLine } from "@skill-harness/core";
1
+ import { BoundedCapture, ProcessOutputLimitError, runProcess, parseTrace, providerFailureFromJsonLine } from "@skill-harness/core";
4
2
  /**
5
3
  * Run `pi --mode json` and build an execution trace, **streaming**.
6
4
  *
@@ -11,10 +9,10 @@ import { parseTrace, providerFailureFromJsonLine } from "@skill-harness/core";
11
9
  * into a string (which is what the shared `exec()` helper does) would exhaust
12
10
  * memory partway through a long wave, taking the whole run with it.
13
11
  *
14
- * So this deliberately does NOT reuse `exec()`. The two quadratic event types are
12
+ * The lifecycle is shared with `exec()`, but capture is streaming. Quadratic events are
15
13
  * dropped as each line arrives, so the giant ones are never retained; the
16
- * remainder — a few KB of terminal events — is held until `close` and parsed
17
- * once. What this bounds is the 52 MB, not the residue.
14
+ * remainder is bounded by both bytes and record count. Oversized lines or retained
15
+ * evidence terminate the owned process and report an infrastructure error.
18
16
  */
19
17
  /**
20
18
  * The two quadratic event types, matched at the head of the object where pi
@@ -24,105 +22,99 @@ import { parseTrace, providerFailureFromJsonLine } from "@skill-harness/core";
24
22
  export const SKIPPED_TYPE_RE = /^\s*\{\s*"type"\s*:\s*"(?:message_update|tool_execution_update)"/;
25
23
  /** How much stderr to retain — enough to diagnose, bounded so a loop cannot blow up. */
26
24
  const MAX_STDERR_CHARS = 8000;
27
- export function runPiJson(opts) {
28
- return new Promise((resolve, reject) => {
29
- const child = spawn("pi", opts.args, {
30
- cwd: opts.cwd,
31
- env: opts.env,
32
- // stdin from /dev/null: pi hangs waiting on it otherwise, and a hang in a
33
- // wave is indistinguishable from a slow model until the timeout fires.
34
- stdio: ["ignore", "pipe", "pipe"],
35
- });
36
- const kept = [];
37
- let stderr = "";
38
- let providerFailure = null;
39
- let settled = false;
40
- const timer = setTimeout(() => {
41
- if (settled)
42
- return;
43
- settled = true;
44
- child.kill("SIGKILL");
45
- reject(new Error(`pi --mode json timed out after ${opts.timeoutMs}ms`));
46
- }, opts.timeoutMs);
47
- const acceptLine = (line) => {
48
- // Prefilter before the full parse: the events skipped here are both the
49
- // overwhelming majority of lines and by far the largest.
50
- //
51
- // Anchored on the `type` field, NOT a substring of the whole line. A raw
52
- // `line.includes('"message_update"')` also matched any event whose
53
- // ARGUMENTS contained that text — so a `tool_execution_start` for, say,
54
- // `grep '"message_update"' logs/` was dropped before parsing, and a
55
- // dropped start means the call never enters the trace at all. A
56
- // `forbid_calls` gate on that tool then passed for want of the evidence.
57
- if (!line.trim())
58
- return;
59
- if (SKIPPED_TYPE_RE.test(line)) {
60
- // Validate but never retain the quadratic payload. Otherwise a malformed
61
- // update could disappear and make an incomplete capture look successful.
62
- try {
63
- const event = JSON.parse(line);
64
- // A late update invalidates an earlier final/settlement. Consecutive
65
- // updates need only one small state marker, not their accumulated text.
66
- if (event.type === "message_update" && kept.at(-1) !== '{"type":"message_update"}') {
67
- kept.push('{"type":"message_update"}');
68
- }
69
- }
70
- catch {
71
- kept.push("null"); // one parser-owned malformed-record error per bad line
72
- }
25
+ export async function runPiJson(opts) {
26
+ const kept = [];
27
+ const stderr = new BoundedCapture(MAX_STDERR_CHARS);
28
+ const maxLine = opts.maxLineBytes ?? 8 * 1024 * 1024;
29
+ const maxRetained = opts.maxRetainedBytes ?? 16 * 1024 * 1024;
30
+ const maxRecords = opts.maxRecords ?? 50_000;
31
+ for (const limit of [maxLine, maxRetained, maxRecords])
32
+ if (!Number.isSafeInteger(limit) || limit < 1)
33
+ throw new Error("Invalid Pi capture limit");
34
+ let retainedBytes = 0;
35
+ let providerFailure = null;
36
+ const keep = (line) => {
37
+ const size = Buffer.byteLength(line, "utf8");
38
+ if (retainedBytes + size > maxRetained || kept.length >= maxRecords)
39
+ throw new ProcessOutputLimitError("Pi retained JSON evidence limit exceeded");
40
+ kept.push(line);
41
+ retainedBytes += size;
42
+ };
43
+ const acceptLine = (line) => {
44
+ // Prefilter before the full parse: the events skipped here are both the
45
+ // overwhelming majority of lines and by far the largest.
46
+ //
47
+ // Anchored on the `type` field, NOT a substring of the whole line. A raw
48
+ // `line.includes('"message_update"')` also matched any event whose
49
+ // ARGUMENTS contained that text — so a `tool_execution_start` for, say,
50
+ // `grep '"message_update"' logs/` was dropped before parsing, and a
51
+ // dropped start means the call never enters the trace at all. A
52
+ // `forbid_calls` gate on that tool then passed for want of the evidence.
53
+ if (!line.trim())
54
+ return;
55
+ if (SKIPPED_TYPE_RE.test(line)) {
56
+ // Validate but never retain the quadratic payload. Otherwise a malformed
57
+ // update could disappear and make an incomplete capture look successful.
58
+ let event;
59
+ try {
60
+ event = JSON.parse(line);
61
+ }
62
+ catch {
63
+ keep("null"); // one parser-owned malformed-record error per bad line
73
64
  return;
74
65
  }
75
- kept.push(line);
76
- if (providerFailure === null)
77
- providerFailure = providerFailureFromJsonLine(line);
78
- };
79
- // JSONL is LF-framed. readline also splits legal Unicode string separators
80
- // on newer Node runtimes, turning valid model text into malformed records.
81
- const decoder = new StringDecoder("utf8");
82
- let pending = "";
83
- child.stdout.on("data", (chunk) => {
84
- pending += decoder.write(chunk);
66
+ // A late update invalidates an earlier final/settlement. Consecutive
67
+ // updates need only one small state marker, not their accumulated text.
68
+ if (event.type === "message_update" && kept.at(-1) !== '{"type":"message_update"}')
69
+ keep('{"type":"message_update"}');
70
+ return;
71
+ }
72
+ keep(line);
73
+ if (providerFailure === null)
74
+ providerFailure = providerFailureFromJsonLine(line);
75
+ };
76
+ // Frame bytes at LF, so split UTF-8 and legal Unicode separators stay intact.
77
+ // Enforce the bound before decoding or parsing an unterminated giant line.
78
+ let pending = new BoundedCapture(maxLine);
79
+ const append = (chunk) => {
80
+ pending.push(chunk);
81
+ if (pending.truncated)
82
+ throw new ProcessOutputLimitError("Pi JSON line byte limit exceeded");
83
+ };
84
+ const flush = () => {
85
+ if (pending.bytes)
86
+ acceptLine(pending.text());
87
+ pending = new BoundedCapture(maxLine);
88
+ };
89
+ const result = await runProcess("pi", opts.args, { cwd: opts.cwd, env: opts.env,
90
+ timeoutMs: opts.timeoutMs, signal: opts.signal, maxOutputBytes: opts.maxOutputBytes,
91
+ onStdout(chunk) {
85
92
  let start = 0, end;
86
- while ((end = pending.indexOf("\n", start)) >= 0) {
87
- acceptLine(pending.slice(start, end));
93
+ while ((end = chunk.indexOf(10, start)) >= 0) {
94
+ append(chunk.subarray(start, end));
95
+ flush();
88
96
  start = end + 1;
89
97
  }
90
- pending = pending.slice(start);
91
- });
92
- child.stdout.on("end", () => {
93
- pending += decoder.end();
94
- if (pending)
95
- acceptLine(pending);
96
- pending = "";
97
- });
98
- child.stderr.on("data", (chunk) => {
99
- if (stderr.length < MAX_STDERR_CHARS)
100
- stderr += chunk.toString("utf8");
101
- });
102
- child.on("error", (err) => {
103
- if (settled)
104
- return;
105
- settled = true;
106
- clearTimeout(timer);
107
- reject(err);
108
- });
109
- child.on("close", (code) => {
110
- if (settled)
111
- return;
112
- settled = true;
113
- clearTimeout(timer);
114
- const parsed = parseTrace(kept, {
115
- piVersion: opts.piVersion,
116
- subject: opts.subject,
117
- scenarioId: opts.scenarioId,
118
- mode: opts.mode,
119
- rep: opts.rep,
120
- turn: opts.turn,
121
- changedPaths: opts.changedPaths,
122
- homeDir: opts.homeDir,
123
- });
124
- resolve({ ...parsed, code, stderr: stderr.slice(0, MAX_STDERR_CHARS), providerFailure });
125
- });
98
+ if (start < chunk.length)
99
+ append(chunk.subarray(start));
100
+ },
101
+ onStderr(chunk) { stderr.push(chunk); },
102
+ onStdoutEnd: flush, });
103
+ if (result.error) {
104
+ if (result.observation.reason === "timeout")
105
+ throw new Error(`pi --mode json timed out after ${opts.timeoutMs}ms`);
106
+ throw result.error;
107
+ }
108
+ const parsed = parseTrace(kept, {
109
+ piVersion: opts.piVersion,
110
+ subject: opts.subject,
111
+ scenarioId: opts.scenarioId,
112
+ mode: opts.mode,
113
+ rep: opts.rep,
114
+ turn: opts.turn,
115
+ changedPaths: opts.changedPaths,
116
+ homeDir: opts.homeDir,
126
117
  });
118
+ return { ...parsed, code: result.code, stderr: stderr.text(), providerFailure, observation: result.observation };
127
119
  }
128
120
  //# sourceMappingURL=pi-json.js.map
package/dist/pi.js CHANGED
@@ -134,6 +134,7 @@ export const piAdapter = {
134
134
  "--no-context-files",
135
135
  "--no-extensions",
136
136
  ...extensionFlags(req.extensions),
137
+ ...(req.tools === undefined ? [] : req.tools.length ? ["--tools", req.tools.join(",")] : ["--no-tools"]),
137
138
  "--provider",
138
139
  req.model.provider,
139
140
  "--model",
@@ -204,6 +205,7 @@ export const piAdapter = {
204
205
  "--no-context-files",
205
206
  "--no-extensions",
206
207
  ...extensionFlags(req.extensions),
208
+ ...(req.tools === undefined ? [] : req.tools.length ? ["--tools", req.tools.join(",")] : ["--no-tools"]),
207
209
  "--provider",
208
210
  req.model.provider,
209
211
  "--model",
@@ -293,7 +295,7 @@ export const piAdapter = {
293
295
  ...(executionFailure ? { executionFailure } : {}),
294
296
  };
295
297
  },
296
- /** Run the judge through Pi: no skills, context files, extensions or session. */
298
+ /** Judge only the supplied evidence: no tools, MCP, skills, context files, extensions or session. */
297
299
  async judge(req) {
298
300
  if (req.model.provider === "claude-code") {
299
301
  throw new Error("judge provider `claude-code` was removed; configure a Pi provider such as `openai-codex`");
@@ -303,6 +305,8 @@ export const piAdapter = {
303
305
  "--no-context-files",
304
306
  "--no-extensions",
305
307
  "--no-session",
308
+ "--no-tools",
309
+ "--no-mcp",
306
310
  "--provider",
307
311
  req.model.provider,
308
312
  "--model",
@@ -313,7 +317,7 @@ export const piAdapter = {
313
317
  const r = await exec("pi", args, { cwd: req.cwd, timeoutMs: PI_TIMEOUT_MS });
314
318
  // Surface failures: pi writes provider errors (auth, out-of-credits) to stderr
315
319
  // and exits non-zero with empty stdout. Pass them through so grading can report.
316
- if (r.stdout.trim().length === 0 && (r.code !== 0 || r.stderr.trim())) {
320
+ if (r.code !== 0 || (r.stdout.trim().length === 0 && r.stderr.trim())) {
317
321
  return `[judge error: pi exited ${r.code}] ${r.stderr.trim()}`;
318
322
  }
319
323
  return r.stdout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-harness/adapters",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "skill-harness Pi runner and judge adapter (internal API)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,6 +42,6 @@
42
42
  "prepack": "node ../../scripts/release-pack.mjs --guard"
43
43
  },
44
44
  "dependencies": {
45
- "@skill-harness/core": "0.29.0"
45
+ "@skill-harness/core": "0.31.0"
46
46
  }
47
47
  }