@skill-harness/cli 0.30.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.
Files changed (2) hide show
  1. package/dist/cli.js +72 -13
  2. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFileSync, existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
- import { discover, resolveSkill, loadSpec, parseSpec, appendScenario, parseModelRef, runSkillModel, formatScorecard, readResults, regradeRun, lintSkill, failsGate, renderTemplateSpec, isTemplateSpec, renderDraftSpec, buildSuggestPrompt, parseSuggestDraft, rescoreRun, regateRun, specPathForRunDir, collectLift, collectReport, collectStability, boundaryCells, stabilityNote, PATH_LEGEND, restampSkill, computeCoverage, formatCoverage, HARNESS_VERSION, defaultJudge, recordedJudgeOrDefault, assertJudgeAllowed, assertNotDowngraded, downgradeWarning, isScoredMode, resolveArm, isFreeOfflineCommand, diagnoseRun, loadCalibration, runCalibration, } from "@skill-harness/core";
5
+ import { discover, resolveSkill, loadSpec, parseSpec, appendScenario, parseModelRef, runSkillModel, formatScorecard, readResults, regradeRun, lintSkill, failsGate, renderTemplateSpec, isTemplateSpec, renderDraftSpec, buildSuggestPrompt, parseSuggestDraft, rescoreRun, regateRun, specPathForRunDir, collectLift, collectReport, collectStability, boundaryCells, stabilityNote, PATH_LEGEND, restampSkill, computeCoverage, formatCoverage, HARNESS_VERSION, defaultJudge, recordedJudgeOrDefault, assertJudgeAllowed, assertNotDowngraded, downgradeWarning, isScoredMode, resolveArm, isFreeOfflineCommand, diagnoseRun, loadCalibration, runCalibration, planCalibration, forkSavedRun, withProcessScope, } from "@skill-harness/core";
6
6
  import { getAdapter } from "@skill-harness/adapters";
7
7
  import { serveReview } from "./serve.js";
8
8
  import { cmdDecision } from "./decision.js";
@@ -11,7 +11,7 @@ const DEFAULT_MODEL = "fireworks:accounts/fireworks/models/deepseek-v4-pro";
11
11
  // SKILL_HARNESS_JUDGE over a baked value — it was duplicated in three places
12
12
  // before, and the pi extension's copy could disagree with this one.
13
13
  const DEFAULT_SUGGEST_MODEL = "openai-codex:gpt-5.6-sol";
14
- const REPEATABLE = new Set(["model", "turn", "check"]);
14
+ const REPEATABLE = new Set(["model", "turn", "check", "reuse"]);
15
15
  function parseArgs(argv) {
16
16
  const _ = [];
17
17
  const flags = {};
@@ -65,6 +65,15 @@ export function flagBool(args, key) {
65
65
  const v = args.flags[key];
66
66
  return v === true || v === "true" || v === "1";
67
67
  }
68
+ function selectedIds(args) {
69
+ const raw = flagStr(args, "only");
70
+ if (raw === undefined)
71
+ return undefined;
72
+ const ids = raw.split(",").map(s => s.trim());
73
+ if (ids.some(id => !id) || new Set(ids).size !== ids.length)
74
+ throw new Error("--only requires a nonempty, unique comma-separated ID list");
75
+ return ids;
76
+ }
68
77
  function resolveModels(args) {
69
78
  const models = [...(args.multi.model ?? [])];
70
79
  const file = flagStr(args, "models");
@@ -174,8 +183,7 @@ export async function cmdRun(args, adapterOverride) {
174
183
  // not on PATH" for a plain flag typo, sending the reader off to fix an install
175
184
  // that was never the problem. Free, offline validation first — it is also why
176
185
  // CI, which has no `pi`, could not test this refusal at all.
177
- const onlyRaw = flagStr(args, "only");
178
- const only = onlyRaw ? onlyRaw.split(",").map((x) => x.trim()).filter(Boolean) : undefined;
186
+ const only = selectedIds(args);
179
187
  const harnessName = flagStr(args, "harness", "pi");
180
188
  const adapter = adapterOverride ?? getAdapter(harnessName);
181
189
  if (!(await adapter.available()))
@@ -254,15 +262,16 @@ export async function cmdRun(args, adapterOverride) {
254
262
  process.exitCode = 1;
255
263
  }
256
264
  export async function cmdGrade(args, adapterOverride) {
257
- const runDir = args._[0];
265
+ const only = selectedIds(args);
266
+ let runDir = args._[0];
258
267
  if (!runDir)
259
268
  throw new Error("usage: skill-harness grade <run-dir> [--judge prov:model] [--suspect-only | --unparsed-only]");
260
269
  if (!existsSync(runDir)) {
261
270
  throw new Error(`run dir not found: ${resolve(runDir)} (relative paths resolve against the cwd)`);
262
271
  }
263
272
  // spec lives at <runDir>/../../../specification.yaml (results/<tag>/<ts> -> tests/)
264
- const testsDir = dirname(dirname(dirname(runDir)));
265
- const specPath = join(testsDir, "specification.yaml");
273
+ const specPath = flagStr(args, "spec") ? resolve(flagStr(args, "spec")) : join(dirname(dirname(dirname(runDir))), "specification.yaml");
274
+ const testsDir = dirname(specPath);
266
275
  const spec = loadSpec(specPath);
267
276
  const prev = existsSync(join(runDir, "results.yaml")) ? readResults(runDir) : null;
268
277
  // Re-judge with the run's RECORDED judge + harness (parity with /rejudge) —
@@ -293,9 +302,17 @@ export async function cmdGrade(args, adapterOverride) {
293
302
  const stale = downgradeWarning(dirname(testsDir));
294
303
  if (stale)
295
304
  console.error(stale);
305
+ const output = flagStr(args, "out");
306
+ if (output !== undefined) {
307
+ if (!output.trim())
308
+ throw new Error("--out requires a new directory");
309
+ runDir = forkSavedRun(runDir, output);
310
+ console.log(`Reusing saved answers in ${runDir}; original run preserved.`);
311
+ }
296
312
  const results = await regradeRun({
297
313
  runDir, spec, adapter, judge, specDir: testsDir, now: nowIso,
298
314
  onlySuspect: flagBool(args, "suspect-only"), onlyUnparsed,
315
+ only,
299
316
  });
300
317
  for (const s of results.scenarios) {
301
318
  console.log(` ${s.id} → ${s.judge_verdict}: ${s.judge_reason}`);
@@ -734,11 +751,12 @@ export function help() {
734
751
 
735
752
  run <skill|all> --skills <root> [--model prov:model ...] [--models file] [--only A1,D2]
736
753
  [--mode red|green|force] [--judge prov:model] [--harness pi] [--label name] [--parallel N] [--reps N] [--pass-threshold T]
737
- [--canary] green only: spend ONE probe proving the skill reached the model, and abort the run if it did not
754
+ [--canary] green only: one instructed-loading probe; autonomous selection stays untested
738
755
  [--structured] request structured capture (Pi uses it by default for subject usage)
739
756
  [--arm <name>] measure under a named arm from <skills-root>/tests/arms.yaml
740
757
  (loads its extensions, seeds pi-daddy definitions, tags the run dir)
741
758
  grade <run-dir> [--judge prov:model] [--suspect-only] re-grade saved transcripts (neutral judge)
759
+ [--only A1,A2] [--out NEW_DIR] [--spec path] preserve originals and judge selected saved answers
742
760
  regrade <run-dir> --unparsed-only re-judge ERROR criterion reps with recorded judge
743
761
  rescore <run-dir>... re-score saved reps vs current spec thresholds (${free("rescore")})
744
762
  regate <run-dir>... [--judge prov:model] re-evaluate saved gates (no subject call; judges fail→pass reps)
@@ -749,7 +767,10 @@ export function help() {
749
767
  init <skill> --skills <root> [--force] scaffold a commented template spec (${free("init")})
750
768
  preflight <skill|all> --skills <root> [--specs <root>] eval checks (${free("preflight")})
751
769
  calibrate <manifest.json> [--run --judge p:m --out NEW_DIR] validate or judge reviewed controls
770
+ [--plan --judge p:m] [--only c1,c2] [--reuse PRIOR_DIR ...] exact historical reuse, no new subject calls
752
771
  diagnose <run-dir> --skills <root> [--specs <root>] inspect saved evidence (${free("diagnose")})
772
+ Paid commands accept --max-memory-mb N / --max-write-mb N for sampled Linux process limits.
773
+ run/grade/calibrate save bounded private resources-*.json; unsupported host metrics stay unknown.
753
774
  In Pi: /skill-harness create <idea> guided skill + optional agent + eval creation
754
775
  suggest <skill> --skills <root> [--model prov:model] [--force] LLM-draft a spec from SKILL.md (spends tokens)
755
776
  list --skills <root> discovered skills + spec status (${free("list")})
@@ -766,7 +787,7 @@ export function help() {
766
787
  defaults: model=${DEFAULT_MODEL} judge=${defaultJudge()} mode=green harness=pi
767
788
  green and force are both scored; red is the unscored baseline. green delivery depends on the
768
789
  harness version (pi >= 0.83.0 discloses only the skill's description and loads the body on demand),
769
- so --mode force is the delivery that cannot silently degrade — and --canary proves green per run.
790
+ --mode force supplies the skill body; --canary checks instructed loading separately from autonomous selection.
770
791
  the judge default runs through Pi on your ChatGPT subscription (\`openai-codex\`), not a metered
771
792
  API key. Set SKILL_HARNESS_JUDGE to change it for a repo or a shell; --judge wins over both.`;
772
793
  }
@@ -797,25 +818,63 @@ export async function cmdCalibrate(args, adapterOverride) {
797
818
  if (!args._[0])
798
819
  throw new Error("usage: skill-harness calibrate <manifest.json> [--run --judge provider:model --out NEW_DIR]");
799
820
  const calibration = loadCalibration(resolve(args._[0]));
821
+ const judgeToken = flagStr(args, "judge");
822
+ const only = selectedIds(args);
823
+ const reuse = (args.multi.reuse ?? []).map(p => { if (!p.trim())
824
+ throw new Error("--reuse requires a prior calibration directory"); return resolve(p); });
825
+ if (flagBool(args, "plan")) {
826
+ if (!judgeToken)
827
+ throw new Error("Calibration --plan needs an explicit --judge; it makes zero provider calls.");
828
+ const plan = planCalibration(calibration, { judge: parseModelRef(judgeToken), only, reuse });
829
+ console.log(JSON.stringify({ ...plan, providerCalls: 0 }, null, 2));
830
+ if (!plan.ready)
831
+ process.exitCode = 1;
832
+ return;
833
+ }
800
834
  if (!flagBool(args, "run")) {
801
- console.log(JSON.stringify({ status: calibration.ready ? "validated" : "needs-review", purpose: "judge-calibration-only", providerCalls: 0, cases: calibration.controls.length, issues: calibration.issues }));
835
+ console.log(JSON.stringify({ status: calibration.ready ? "validated" : "needs-review", purpose: "judge-calibration-only", executionGates: "not-evaluated", providerCalls: 0, cases: calibration.controls.length, issues: calibration.issues }));
802
836
  if (!calibration.ready)
803
837
  process.exitCode = 1;
804
838
  return;
805
839
  }
806
- const judgeToken = flagStr(args, "judge");
807
840
  const output = flagStr(args, "out");
808
841
  if (!judgeToken || !output)
809
842
  throw new Error("Calibration --run needs an explicitly selected --judge and a new --out directory.");
810
843
  const adapter = adapterOverride ?? getAdapter("pi");
811
844
  if (!await adapter.available())
812
845
  throw new Error("Pi is required for judge calibration. Configure the selected model in Pi first.");
813
- const result = await runCalibration(calibration, { adapter, judge: parseModelRef(judgeToken), output: resolve(output), allowMeteredJudge: flagBool(args, "allow-metered-judge"), onProgress: p => console.log(`Judging control ${p.index + 1}/${p.total}: ${p.id}`) });
846
+ const result = await runCalibration(calibration, { adapter, judge: parseModelRef(judgeToken), only, reuse, output: resolve(output), allowMeteredJudge: flagBool(args, "allow-metered-judge"), onProgress: p => console.log(`Judging control ${p.index + 1}/${p.total}: ${p.id}`) });
814
847
  console.log(JSON.stringify(result, null, 2));
815
848
  if (result.status !== "matched")
816
849
  process.exitCode = 1;
817
850
  }
818
851
  export async function main(argv) {
852
+ if (!["run", "grade", "regrade", "calibrate", "regate", "suggest"].includes(argv[0]))
853
+ return dispatch(argv);
854
+ const args = parseArgs(argv.slice(1));
855
+ const limit = (name) => {
856
+ const raw = flagStr(args, name);
857
+ if (raw === undefined)
858
+ return undefined;
859
+ const number = Number(raw);
860
+ if (!Number.isFinite(number) || number <= 0 || !Number.isSafeInteger(number * 1048576))
861
+ throw new Error(`--${name} requires a positive MiB value`);
862
+ return number * 1048576;
863
+ };
864
+ const controller = new AbortController();
865
+ const interrupt = () => { process.exitCode = 130; controller.abort(new Error("SIGINT")); };
866
+ const terminate = () => { process.exitCode = 143; controller.abort(new Error("SIGTERM")); };
867
+ process.once("SIGINT", interrupt);
868
+ process.once("SIGTERM", terminate);
869
+ try {
870
+ await withProcessScope({ signal: controller.signal, maxRssBytes: limit("max-memory-mb"), maxWriteBytes: limit("max-write-mb") }, () => dispatch(argv));
871
+ }
872
+ finally {
873
+ process.removeListener("SIGINT", interrupt);
874
+ process.removeListener("SIGTERM", terminate);
875
+ }
876
+ }
877
+ async function dispatch(argv) {
819
878
  const cmd = argv[0];
820
879
  if (cmd === "decision")
821
880
  return cmdDecision(argv.slice(1));
@@ -867,7 +926,7 @@ export async function main(argv) {
867
926
  if (!process.env.VITEST) {
868
927
  main(process.argv.slice(2)).catch((e) => {
869
928
  console.error(`error: ${e instanceof Error ? e.message : e}`);
870
- process.exitCode = 1;
929
+ process.exitCode ||= 1;
871
930
  });
872
931
  }
873
932
  //# sourceMappingURL=cli.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-harness/cli",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "skill-harness CLI — run, grade, review, and lint agent-skill scenarios on the pi harness",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "prepack": "node ../../scripts/release-pack.mjs --guard"
45
45
  },
46
46
  "dependencies": {
47
- "@skill-harness/core": "0.30.0",
48
- "@skill-harness/adapters": "0.30.0"
47
+ "@skill-harness/core": "0.31.0",
48
+ "@skill-harness/adapters": "0.31.0"
49
49
  }
50
50
  }