@skill-harness/adapters 0.4.0 → 0.6.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.
Files changed (2) hide show
  1. package/dist/pi.js +62 -5
  2. package/package.json +2 -2
package/dist/pi.js CHANGED
@@ -1,17 +1,52 @@
1
- import { mkdtempSync, readFileSync } from "node:fs";
1
+ import { existsSync, mkdtempSync, readFileSync, statSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
- import { join } from "node:path";
3
+ import { join, resolve } from "node:path";
4
4
  import { exec, onPath, envNum } from "@skill-harness/core";
5
5
  const PI_TIMEOUT_MS = envNum("PI_TIMEOUT_MS", 300_000);
6
- /** Skill-activation flags for a given run mode. */
6
+ /**
7
+ * Refuse to hand pi a skill dir it will silently ignore.
8
+ *
9
+ * Measured on pi 0.83.0: `pi --skill /nonexistent/skilldir -p "hi"` answers
10
+ * normally and exits 0. So a wrong path does not fail a run — it turns every
11
+ * scenario into a no-skill baseline that still looks like a result. Two full waves
12
+ * in the reference corpus scored ≈ their naked-model baseline before a
13
+ * contradictory failure mix gave it away.
14
+ *
15
+ * Returns the ABSOLUTE dir: `discover()` builds `join(root, name)`, so
16
+ * `--skills .` yields a relative path, and pi runs in a neutral cwd of the
17
+ * harness's choosing — a relative `--skill` resolved there points at nothing,
18
+ * which is precisely the case pi swallows.
19
+ */
20
+ function requireSkillDir(skillDir, mode) {
21
+ const abs = resolve(skillDir);
22
+ const md = join(abs, "SKILL.md");
23
+ const isDir = existsSync(abs) && statSync(abs).isDirectory();
24
+ if (!isDir || !existsSync(md)) {
25
+ throw new Error(`mode=${mode} needs a skill directory with a SKILL.md, but ${abs} ${isDir ? "has none" : "is not a directory"}` +
26
+ (abs === skillDir ? "" : ` (given \`${skillDir}\`, resolved against ${process.cwd()})`) +
27
+ ` — pi accepts \`--skill <nonexistent>\` silently (exit 0, a normal answer, no skill in context),` +
28
+ ` so this run would measure a model with no skill and report it as a result.`);
29
+ }
30
+ return abs;
31
+ }
32
+ /**
33
+ * Skill-activation flags for a given run mode.
34
+ *
35
+ * `green` asks pi to activate the skill and is therefore only as good as pi's
36
+ * delivery: 0.80.x wrapped the prompt with the skill body, 0.83.0 discloses the
37
+ * description and loads the body on demand ("models don't always do this" — pi's
38
+ * own docs). `force` puts SKILL.md in the system prompt, which no pi version has
39
+ * made conditional. Both are checked the same way, because the failure being
40
+ * prevented — a skill dir that isn't there — costs a whole wave either way.
41
+ */
7
42
  function skillFlags(mode, skillDir) {
8
43
  switch (mode) {
9
44
  case "red":
10
45
  return ["--no-skills"];
11
46
  case "green":
12
- return ["--skill", skillDir];
47
+ return ["--skill", requireSkillDir(skillDir, mode)];
13
48
  case "force": {
14
- const body = readFileSync(join(skillDir, "SKILL.md"), "utf8");
49
+ const body = readFileSync(join(requireSkillDir(skillDir, mode), "SKILL.md"), "utf8");
15
50
  return ["--no-skills", "--append-system-prompt", body];
16
51
  }
17
52
  }
@@ -25,6 +60,28 @@ export const piAdapter = {
25
60
  available() {
26
61
  return Promise.resolve(onPath("pi"));
27
62
  },
63
+ /**
64
+ * `pi --version` (it prints a bare version, e.g. `0.83.0`), recorded in
65
+ * results.yaml as `harness_cli_version`.
66
+ *
67
+ * Null on any failure — a non-zero exit, empty output, or pi missing entirely.
68
+ * A run must not abort because provenance was unavailable, and a fabricated
69
+ * version would be worse than an absent one.
70
+ */
71
+ async version() {
72
+ try {
73
+ const r = await exec("pi", ["--version"], { timeoutMs: 30_000 });
74
+ const line = r.stdout.split("\n")[0]?.trim() ?? "";
75
+ if (r.code !== 0 || line === "")
76
+ return null;
77
+ // Tolerate a future `pi 1.2.3` / `pi version 1.2.3` shape without losing the
78
+ // bare `0.83.0` one: keep the first version-looking token, else the line.
79
+ return /\d+\.\d+\.\d+\S*/.exec(line)?.[0] ?? line;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ },
28
85
  /**
29
86
  * Run a scenario through pi. Single turn → --no-session -p. Multi turn → a
30
87
  * shared --session-dir, -c on every turn after the first. Returns a transcript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-harness/adapters",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "skill-harness harness adapters — pi runner + claude-code judge routing (internal API)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,6 +40,6 @@
40
40
  "prepack": "cp ../../LICENSE ./LICENSE"
41
41
  },
42
42
  "dependencies": {
43
- "@skill-harness/core": "0.4.0"
43
+ "@skill-harness/core": "0.6.0"
44
44
  }
45
45
  }