@skill-harness/cli 0.18.0 → 0.20.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
@@ -3,7 +3,8 @@
3
3
  The command surface for [skill-harness](https://github.com/mojomanyana/skill-harness) —
4
4
  a test/optimize loop for agent skills: run spec'd scenarios on the `pi` harness,
5
5
  LLM-judge each transcript, score against a ship bar, review, and re-run to
6
- measure a `SKILL.md` edit.
6
+ measure a `SKILL.md` edit. Subject, judge, and spec-drafting model calls all run
7
+ through Pi.
7
8
 
8
9
  Most users should install the `skill-harness` meta-package (`npm i -g
9
10
  skill-harness`) instead of this package directly.
@@ -22,8 +23,15 @@ npm i -g @skill-harness/cli
22
23
  | `skill-harness lint <skill\|all> --skills <root>` | validate specs/fixtures; CI gate |
23
24
  | `skill-harness run <skill\|all> --skills <root> [--model p:m ...] [--judge p:m]` | run scenarios, grade, score |
24
25
  | `skill-harness grade <run-dir> [--judge p:m]` | re-grade saved transcripts |
26
+ | `skill-harness rescore <run-dir>...` | re-apply current score thresholds offline |
27
+ | `skill-harness regate <run-dir>...` | re-evaluate saved objective gates |
28
+ | `skill-harness restamp <skill\|all> --skills <root> --from <ref>` | migrate matching model-visible digests |
29
+ | `skill-harness stability <skill\|all> --skills <root>` | report run-over-run verdict flips |
25
30
  | `skill-harness review <skill> --skills <root> [--port N]` | serve the interactive review UI |
26
31
  | `skill-harness add-test <skill> --skills <root> --id ID --title T ...` | scaffold a new scenario |
32
+ | `skill-harness init <skill> --skills <root>` | write an offline spec template |
33
+ | `skill-harness suggest <skill> --skills <root>` | draft a spec through Pi |
34
+ | `skill-harness coverage <skill\|all> --skills <root>` | report declared instruction coverage |
27
35
 
28
36
  ## More
29
37
 
package/dist/cli.js CHANGED
@@ -2,14 +2,14 @@
2
2
  import { readFileSync, existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { basename, dirname, join, resolve, relative } 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, collectStability, boundaryCells, stabilityNote, PATH_LEGEND, restampSkill, computeCoverage, formatCoverage, HARNESS_VERSION, defaultJudge, assertJudgeAllowed, assertNotDowngraded, downgradeWarning, isScoredMode, resolveArm, isFreeOfflineCommand, } 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, collectStability, boundaryCells, stabilityNote, PATH_LEGEND, restampSkill, computeCoverage, formatCoverage, HARNESS_VERSION, defaultJudge, recordedJudgeOrDefault, assertJudgeAllowed, assertNotDowngraded, downgradeWarning, isScoredMode, resolveArm, isFreeOfflineCommand, } from "@skill-harness/core";
6
6
  import { getAdapter } from "@skill-harness/adapters";
7
7
  import { serveReview } from "./serve.js";
8
8
  const DEFAULT_MODEL = "fireworks:accounts/fireworks/models/deepseek-v4-pro";
9
9
  // The judge default lives in core (`defaultJudge()`), which resolves
10
10
  // SKILL_HARNESS_JUDGE over a baked value — it was duplicated in three places
11
11
  // before, and the pi extension's copy could disagree with this one.
12
- const DEFAULT_SUGGEST_MODEL = "claude-code:claude-opus-4-8";
12
+ const DEFAULT_SUGGEST_MODEL = "openai-codex:gpt-5.6-sol";
13
13
  const REPEATABLE = new Set(["model", "turn", "check"]);
14
14
  function parseArgs(argv) {
15
15
  const _ = [];
@@ -236,14 +236,17 @@ export async function cmdGrade(args, adapterOverride) {
236
236
  const spec = loadSpec(specPath);
237
237
  const prev = existsSync(join(runDir, "results.yaml")) ? readResults(runDir) : null;
238
238
  // Re-judge with the run's RECORDED judge + harness (parity with /rejudge) —
239
- // an explicit --judge flag still wins; with no prior results, fall back to
240
- // the CLI default.
239
+ // an explicit --judge flag still wins; absent or retired recorded judges use
240
+ // the current Pi default.
241
241
  const judgeFlag = flagStr(args, "judge");
242
- const judge = judgeFlag ? parseModelRef(judgeFlag) : (prev?.judge ?? parseModelRef(defaultJudge()));
243
- // A regrade reuses the judge the run RECORDED, so a run that names a metered judge
244
- // bills on every later regrade with no flag typed anywhere. Latent rather than live
245
- // in the reference corpus (all ~140 committed runs there record `claude-code`), but
246
- // it is the one path where the cost decision was made by a file, not a person.
242
+ const resolvedRecordedJudge = judgeFlag ? undefined : recordedJudgeOrDefault(prev?.judge);
243
+ const judge = judgeFlag ? parseModelRef(judgeFlag) : resolvedRecordedJudge.judge;
244
+ if (resolvedRecordedJudge?.migratedFrom) {
245
+ console.error(`skill-harness: recorded judge ${resolvedRecordedJudge.migratedFrom.provider}:${resolvedRecordedJudge.migratedFrom.model} was removed; re-judging through Pi with ${judge.provider}:${judge.model}`);
246
+ }
247
+ // A regrade normally reuses the judge the run RECORDED, so a run that names a
248
+ // metered judge bills on every later regrade with no flag typed anywhere. It is the one path
249
+ // where the cost decision was made by a file, not a person.
247
250
  assertJudgeAllowed(judge, {
248
251
  source: judgeFlag ? "--judge" : prev?.judge ? "the run's recorded judge" : "the default judge",
249
252
  allowMetered: flagBool(args, "allow-metered-judge"),
@@ -312,7 +315,11 @@ export async function cmdRegate(args, adapterOverride) {
312
315
  const specPath = specPathForRunDir(runDir);
313
316
  const spec = loadSpec(specPath);
314
317
  const prev = readResults(runDir);
315
- const judge = judgeFlag ? parseModelRef(judgeFlag) : (prev.judge ?? parseModelRef(defaultJudge()));
318
+ const resolvedRecordedJudge = judgeFlag ? undefined : recordedJudgeOrDefault(prev.judge);
319
+ const judge = judgeFlag ? parseModelRef(judgeFlag) : resolvedRecordedJudge.judge;
320
+ if (resolvedRecordedJudge?.migratedFrom) {
321
+ console.error(`skill-harness: recorded judge ${resolvedRecordedJudge.migratedFrom.provider}:${resolvedRecordedJudge.migratedFrom.model} was removed; re-judging through Pi with ${judge.provider}:${judge.model}`);
322
+ }
316
323
  assertJudgeAllowed(judge, {
317
324
  source: judgeFlag ? "--judge" : "the run's recorded judge",
318
325
  allowMetered: flagBool(args, "allow-metered-judge"),
@@ -692,7 +699,7 @@ export function help() {
692
699
  run <skill|all> --skills <root> [--model prov:model ...] [--models file] [--only A1,D2]
693
700
  [--mode red|green|force] [--judge prov:model] [--harness pi] [--label name] [--parallel N] [--reps N] [--pass-threshold T]
694
701
  [--canary] green only: spend ONE probe proving the skill reached the model, and abort the run if it did not
695
- [--structured] record subject tokens, cost and wall time (needs pi --mode json)
702
+ [--structured] request structured capture (Pi uses it by default for subject usage)
696
703
  [--arm <name>] measure under a named arm from <skills-root>/tests/arms.yaml
697
704
  (loads its extensions, seeds pi-daddy definitions, tags the run dir)
698
705
  grade <run-dir> [--judge prov:model] [--suspect-only] re-grade saved transcripts (neutral judge)
@@ -714,8 +721,8 @@ defaults: model=${DEFAULT_MODEL} judge=${defaultJudge()} mode=green harness=p
714
721
  green and force are both scored; red is the unscored baseline. green delivery depends on the
715
722
  harness version (pi >= 0.83.0 discloses only the skill's description and loads the body on demand),
716
723
  so --mode force is the delivery that cannot silently degrade — and --canary proves green per run.
717
- the judge default is Opus on your Claude subscription (\`claude-code\` → \`claude -p\`), not a
718
- metered API key. Set SKILL_HARNESS_JUDGE to change it for a repo or a shell; --judge wins over both.`;
724
+ the judge default runs through Pi on your ChatGPT subscription (\`openai-codex\`), not a metered
725
+ API key. Set SKILL_HARNESS_JUDGE to change it for a repo or a shell; --judge wins over both.`;
719
726
  }
720
727
  export async function main(argv) {
721
728
  const cmd = argv[0];
package/dist/serve.js CHANGED
@@ -3,7 +3,7 @@ import { readFileSync, existsSync } from "node:fs";
3
3
  import { join, dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { spawn } from "node:child_process";
6
- import { collectReport, renderReport, collectTrends, readResults, writeResults, applyOverride, preserveTranscript, findTranscriptFiles, ensureResultsGitignore, appendJournal, loadSpec, regradeScenario, refreshRubricHashes, findJudgeRawFiles, effectiveThreshold, scoreContextFor, isScoredMode, rebuildScenarioResult, mergeScenarioMetrics, carryRepObjectives, envFlag, } from "@skill-harness/core";
6
+ import { collectReport, renderReport, collectTrends, readResults, writeResults, applyOverride, preserveTranscript, findTranscriptFiles, ensureResultsGitignore, appendJournal, loadSpec, regradeScenario, refreshRubricHashes, findJudgeRawFiles, effectiveThreshold, scoreContextFor, isScoredMode, rebuildScenarioResult, mergeScenarioMetrics, carryRepObjectives, envFlag, recordedJudgeOrDefault, assertJudgeAllowed, } from "@skill-harness/core";
7
7
  import { getAdapter } from "@skill-harness/adapters";
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  /** Locate assets/report.template.html relative to dist/ or src/. */
@@ -126,6 +126,17 @@ export async function serveReview(opts) {
126
126
  res.end(JSON.stringify({ ok: false, error: `scenario ${body.scenarioId} is ${delivery?.status ?? "ERROR"}: delivery-gated evidence cannot be re-judged` }));
127
127
  return;
128
128
  }
129
+ const resolvedRecordedJudge = recordedJudgeOrDefault(results.judge);
130
+ try {
131
+ assertJudgeAllowed(resolvedRecordedJudge.judge, {
132
+ source: resolvedRecordedJudge.migratedFrom ? "the current default judge" : "the run's recorded judge",
133
+ });
134
+ }
135
+ catch (e) {
136
+ res.writeHead(400, { "content-type": "application/json" });
137
+ res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : String(e) }));
138
+ return;
139
+ }
129
140
  const adapter = opts.adapter ?? getAdapter(results.harness);
130
141
  if (!(await adapter.available())) {
131
142
  res.writeHead(400, { "content-type": "application/json" });
@@ -135,7 +146,7 @@ export async function serveReview(opts) {
135
146
  const threshold = effectiveThreshold(prev, scenario);
136
147
  try {
137
148
  const rr = await regradeScenario({
138
- runDir: column.runDir, spec, scenario, adapter, judge: results.judge,
149
+ runDir: column.runDir, spec, scenario, adapter, judge: resolvedRecordedJudge.judge,
139
150
  specDir: dirname(specPath), threshold, mode: results.mode,
140
151
  expectedReps: prev.reps ?? 1,
141
152
  });
@@ -147,7 +158,7 @@ export async function serveReview(opts) {
147
158
  });
148
159
  const written = writeResults(column.runDir, {
149
160
  schema: results.schema, subject_invocations: results.subject_invocations,
150
- skill: results.skill, harness: results.harness, model: results.model, judge: results.judge,
161
+ skill: results.skill, harness: results.harness, model: results.model, judge: resolvedRecordedJudge.judge,
151
162
  timestamp: results.timestamp, label: results.label, mode: results.mode, scenarios: merged,
152
163
  partial: results.partial,
153
164
  // Provenance survives a UI re-judge, same as it does through `grade`.
@@ -169,7 +180,13 @@ export async function serveReview(opts) {
169
180
  const g = written.effective_grade;
170
181
  appendJournal(column.runDir, { event: "score", ts: new Date().toISOString(), passed: g.passed, total: g.total, pct: g.pct, letter: g.letter, ship: g.ship, note: g.note });
171
182
  res.writeHead(200, { "content-type": "application/json" });
172
- res.end(JSON.stringify({ ok: true, grade: g }));
183
+ res.end(JSON.stringify({
184
+ ok: true,
185
+ grade: g,
186
+ judgeMigration: resolvedRecordedJudge.migratedFrom
187
+ ? `${resolvedRecordedJudge.migratedFrom.provider}:${resolvedRecordedJudge.migratedFrom.model} → ${resolvedRecordedJudge.judge.provider}:${resolvedRecordedJudge.judge.model}`
188
+ : undefined,
189
+ }));
173
190
  }
174
191
  catch (e) {
175
192
  // regradeScenario (or the write/journal that follows) failed — surface the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-harness/cli",
3
- "version": "0.18.0",
3
+ "version": "0.20.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.18.0",
48
- "@skill-harness/adapters": "0.18.0"
47
+ "@skill-harness/core": "0.20.0",
48
+ "@skill-harness/adapters": "0.20.0"
49
49
  }
50
50
  }