@skill-harness/cli 0.32.3 → 0.33.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.
@@ -177,6 +177,7 @@ function render() {
177
177
  const errorNote = g.errors > 0 ? ` — ${g.errors} infrastructure error${g.errors === 1 ? "" : "s"}` : "";
178
178
  gradeHtml = `${g.letter} (${g.pct}%) · ${g.passed}/${g.total}${suspectNote}${errorNote} ${badge}`;
179
179
  }
180
+ const summaryHtml = col.summaryLines ? `<details><summary>Evidence, profiles and usage</summary><div class="meta">${col.summaryLines.map(line => escapeHtml(line)).join("<br>")}</div></details>` : "";
180
181
  const m = col.metrics;
181
182
  const metricHtml = m && m.total_reps > 0
182
183
  ? `<div class='grade' title='subject metrics reported by ${m.subject_metrics_reps}/${m.total_reps} reps'>` +
@@ -200,7 +201,7 @@ function render() {
200
201
  : `lift 0 · no effect`;
201
202
  const inc = ls.inconclusive > 0 ? ` · ${ls.inconclusive} inconclusive` : "";
202
203
  const part = col.lift.partial ? " · partial" : "";
203
- html += `<th><div>${escapeHtml(col.label)}</div><div class='grade'>${gradeHtml}</div>${metricHtml}` +
204
+ html += `<th><div>${escapeHtml(col.label)}</div><div class='grade'>${gradeHtml}</div>${metricHtml}${summaryHtml}` +
204
205
  `<div class='lift ${cls}' title='vs red baseline ${escapeHtml(col.lift.redTimestamp)} — ${ls.kept} passed without the skill too'>${body}${inc}${part}</div></th>`;
205
206
  return;
206
207
  }
@@ -210,7 +211,7 @@ function render() {
210
211
  if (none) {
211
212
  liftHtml = `<div class='lift none' title='${escapeHtml(none.title)}'>${escapeHtml(none.text)}</div>`;
212
213
  }
213
- html += `<th><div>${escapeHtml(col.label)}</div><div class='grade'>${gradeHtml}</div>${metricHtml}${liftHtml}</th>`;
214
+ html += `<th><div>${escapeHtml(col.label)}</div><div class='grade'>${gradeHtml}</div>${metricHtml}${summaryHtml}${liftHtml}</th>`;
214
215
  });
215
216
  html += "</tr></thead><tbody>";
216
217
  for (const scn of DATA.scenarios) {
package/dist/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ import { withThinking, modelProfileLabel } from "@skill-harness/core";
2
3
  import { readFileSync, existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
3
4
  import { basename, dirname, join, resolve } from "node:path";
4
5
  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, planCalibration, forkSavedRun, withProcessScope, } from "@skill-harness/core";
6
+ 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, requestedSubject, loadCalibration, runCalibration, planCalibration, forkSavedRun, withProcessScope, } from "@skill-harness/core";
6
7
  import { getAdapter } from "@skill-harness/adapters";
7
8
  import { serveReview } from "./serve.js";
8
9
  import { cmdDecision } from "./decision.js";
10
+ import { formatDiagnosis } from "./diagnose-format.js";
9
11
  const DEFAULT_MODEL = "fireworks:accounts/fireworks/models/deepseek-v4-pro";
10
12
  // The judge default lives in core (`defaultJudge()`), which resolves
11
13
  // SKILL_HARNESS_JUDGE over a baked value — it was duplicated in three places
@@ -172,7 +174,7 @@ export async function cmdRun(args, adapterOverride) {
172
174
  // any subject tokens are spent: a refusal that arrives after the model has been
173
175
  // paid for is a worse version of the problem it exists to prevent.
174
176
  const judgeFlagRun = flagStr(args, "judge");
175
- const judge = parseModelRef(judgeFlagRun ?? defaultJudge());
177
+ const judge = withThinking(parseModelRef(judgeFlagRun ?? defaultJudge()), flagStr(args, "judge-thinking"));
176
178
  assertJudgeAllowed(judge, {
177
179
  source: judgeFlagRun ? "--judge" : "the default judge (SKILL_HARNESS_JUDGE or the baked value)",
178
180
  allowMetered: flagBool(args, "allow-metered-judge"),
@@ -184,6 +186,7 @@ export async function cmdRun(args, adapterOverride) {
184
186
  // that was never the problem. Free, offline validation first — it is also why
185
187
  // CI, which has no `pi`, could not test this refusal at all.
186
188
  const only = selectedIds(args);
189
+ const models = resolveModels(args).map(token => ({ token, model: withThinking(parseModelRef(token), flagStr(args, "thinking")) }));
187
190
  const harnessName = flagStr(args, "harness", "pi");
188
191
  const adapter = adapterOverride ?? getAdapter(harnessName);
189
192
  if (!(await adapter.available()))
@@ -194,7 +197,6 @@ export async function cmdRun(args, adapterOverride) {
194
197
  const label = flagStr(args, "label") || null;
195
198
  const parallel = Math.max(1, Number(flagStr(args, "parallel", "1")) || 1);
196
199
  const { reps, passThreshold } = parseRunTuning(args);
197
- const modelTokens = resolveModels(args);
198
200
  // Resolved ONCE, before the skill loop: a typo'd arm name fails before any
199
201
  // token is spent on any skill.
200
202
  const armName = flagStr(args, "arm") || null;
@@ -212,12 +214,11 @@ export async function cmdRun(args, adapterOverride) {
212
214
  // that look comparable and are not. Checked per skill, before its first token.
213
215
  assertNotDowngraded(dirname(skill.testsDir), "run");
214
216
  const spec = loadSpec(skill.specPath);
215
- for (const token of modelTokens) {
216
- const model = parseModelRef(token);
217
+ for (const { token, model } of models) {
217
218
  // The version is on the banner because a stale global install is otherwise
218
219
  // invisible: a 0.1.0 binary grades a 0.3.x corpus, produces plausible
219
220
  // numbers, and nothing on screen says which tool made them.
220
- console.log(`\n▶ ${spec.skill} · ${harnessName}:${token} · mode=${mode} · judge=${judge.provider}:${judge.model} · skill-harness ${HARNESS_VERSION}`);
221
+ console.log(`\n▶ ${spec.skill} · ${harnessName}:${token} · mode=${mode} · subject=${modelProfileLabel(model)} · judge=${modelProfileLabel(judge)} · skill-harness ${HARNESS_VERSION}`);
221
222
  const summary = await runSkillModel({
222
223
  spec,
223
224
  skillDir: skill.dir,
@@ -279,10 +280,10 @@ export async function cmdGrade(args, adapterOverride) {
279
280
  // the current Pi default.
280
281
  const judgeFlag = flagStr(args, "judge");
281
282
  const onlyUnparsed = flagBool(args, "unparsed-only");
282
- if (onlyUnparsed && judgeFlag !== undefined)
283
+ if (onlyUnparsed && (judgeFlag !== undefined || flagStr(args, "judge-thinking") !== undefined))
283
284
  throw new Error("--unparsed-only uses the recorded judge; --judge cannot be combined with it");
284
285
  const resolvedRecordedJudge = judgeFlag ? undefined : recordedJudgeOrDefault(prev?.judge);
285
- const judge = judgeFlag ? parseModelRef(judgeFlag) : resolvedRecordedJudge.judge;
286
+ const judge = withThinking(judgeFlag ? parseModelRef(judgeFlag) : resolvedRecordedJudge.judge, flagStr(args, "judge-thinking"));
286
287
  if (onlyUnparsed && resolvedRecordedJudge?.migratedFrom)
287
288
  throw new Error("--unparsed-only cannot use this retired recorded judge; use grade to migrate it explicitly");
288
289
  if (resolvedRecordedJudge?.migratedFrom) {
@@ -368,7 +369,7 @@ export async function cmdRegate(args, adapterOverride) {
368
369
  const spec = loadSpec(specPath);
369
370
  const prev = readResults(runDir);
370
371
  const resolvedRecordedJudge = judgeFlag ? undefined : recordedJudgeOrDefault(prev.judge);
371
- const judge = judgeFlag ? parseModelRef(judgeFlag) : resolvedRecordedJudge.judge;
372
+ const judge = withThinking(judgeFlag ? parseModelRef(judgeFlag) : resolvedRecordedJudge.judge, flagStr(args, "judge-thinking"));
372
373
  if (resolvedRecordedJudge?.migratedFrom) {
373
374
  console.error(`skill-harness: recorded judge ${resolvedRecordedJudge.migratedFrom.provider}:${resolvedRecordedJudge.migratedFrom.model} was removed; re-judging through Pi with ${judge.provider}:${judge.model}`);
374
375
  }
@@ -750,6 +751,7 @@ export function help() {
750
751
  return `skill-harness ${HARNESS_VERSION} — test/optimize loop for agent skills (pi harness)
751
752
 
752
753
  run <skill|all> --skills <root> [--model prov:model ...] [--models file] [--only A1,D2]
754
+ [--thinking LEVEL] [--judge-thinking LEVEL]
753
755
  [--mode red|green|force] [--judge prov:model] [--harness pi] [--label name] [--parallel N] [--reps N] [--pass-threshold T]
754
756
  [--canary] green only: one instructed-loading probe; autonomous selection stays untested
755
757
  [--structured] request structured capture (Pi uses it by default for subject usage)
@@ -768,7 +770,7 @@ export function help() {
768
770
  preflight <skill|all> --skills <root> [--specs <root>] eval checks (${free("preflight")})
769
771
  calibrate <manifest.json> [--run --judge p:m --out NEW_DIR] validate or judge reviewed controls
770
772
  [--plan --judge p:m] [--only c1,c2] [--reuse PRIOR_DIR ...] exact historical reuse, no new subject calls
771
- diagnose <run-dir> --skills <root> [--specs <root>] inspect saved evidence (${free("diagnose")})
773
+ diagnose <run-dir> --skills <root> [--specs <root>] [--format json|text] inspect saved evidence (${free("diagnose")})
772
774
  Paid commands accept --max-memory-mb N / --max-write-mb N for sampled Linux process limits.
773
775
  run/grade/calibrate save bounded private resources-*.json; unsupported host metrics stay unknown.
774
776
  In Pi: /skill-harness create <idea> guided skill + optional agent + eval creation
@@ -807,12 +809,25 @@ export async function cmdPreflight(args) {
807
809
  }
808
810
  export async function cmdDiagnose(args) {
809
811
  if (!args._[0])
810
- throw new Error("usage: skill-harness diagnose <run-dir> --skills <root> [--specs <root>]");
812
+ throw new Error("usage: skill-harness diagnose <run-dir> --skills <root> [--specs <root>] [--format json|text]");
813
+ const format = flagStr(args, "format") ?? "json";
814
+ if (format !== "json" && format !== "text")
815
+ throw new Error("diagnose --format must be json or text");
811
816
  const runDir = resolve(args._[0]);
812
817
  const recorded = readResults(runDir);
813
818
  const { skills, specs } = roots(args);
814
819
  const selected = resolveSkill(skills, recorded.skill, specs);
815
- console.log(JSON.stringify(diagnoseRun(runDir, selected.dir, selected.specPath), null, 2));
820
+ if ((args.multi.model?.length ?? 0) > 1)
821
+ throw new Error("diagnose previews one subject profile at a time");
822
+ const modelToken = args.multi.model?.[0] ?? flagStr(args, "model"), judgeToken = flagStr(args, "judge");
823
+ if (modelToken?.includes(","))
824
+ throw new Error("diagnose previews one subject profile at a time");
825
+ const profiles = {
826
+ subject: modelToken !== undefined || Object.hasOwn(args.flags, "thinking") ? withThinking(modelToken !== undefined ? parseModelRef(modelToken) : requestedSubject(recorded), flagStr(args, "thinking")) : undefined,
827
+ judge: judgeToken !== undefined || Object.hasOwn(args.flags, "judge-thinking") ? withThinking(judgeToken !== undefined ? parseModelRef(judgeToken) : recorded.judge, flagStr(args, "judge-thinking")) : undefined,
828
+ };
829
+ const report = diagnoseRun(runDir, selected.dir, selected.specPath, profiles);
830
+ console.log(format === "text" ? formatDiagnosis(report) : JSON.stringify(report, null, 2));
816
831
  }
817
832
  export async function cmdCalibrate(args, adapterOverride) {
818
833
  if (!args._[0])
@@ -825,7 +840,7 @@ export async function cmdCalibrate(args, adapterOverride) {
825
840
  if (flagBool(args, "plan")) {
826
841
  if (!judgeToken)
827
842
  throw new Error("Calibration --plan needs an explicit --judge; it makes zero provider calls.");
828
- const plan = planCalibration(calibration, { judge: parseModelRef(judgeToken), only, reuse });
843
+ const plan = planCalibration(calibration, { judge: withThinking(parseModelRef(judgeToken), flagStr(args, "judge-thinking")), only, reuse });
829
844
  console.log(JSON.stringify({ ...plan, providerCalls: 0 }, null, 2));
830
845
  if (!plan.ready)
831
846
  process.exitCode = 1;
@@ -843,7 +858,7 @@ export async function cmdCalibrate(args, adapterOverride) {
843
858
  const adapter = adapterOverride ?? getAdapter("pi");
844
859
  if (!await adapter.available())
845
860
  throw new Error("Pi is required for judge calibration. Configure the selected model in Pi first.");
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}`) });
861
+ const result = await runCalibration(calibration, { adapter, judge: withThinking(parseModelRef(judgeToken), flagStr(args, "judge-thinking")), only, reuse, output: resolve(output), allowMeteredJudge: flagBool(args, "allow-metered-judge"), onProgress: p => console.log(`Judging control ${p.index + 1}/${p.total}: ${p.id}`) });
847
862
  console.log(JSON.stringify(result, null, 2));
848
863
  if (result.status !== "matched")
849
864
  process.exitCode = 1;
@@ -0,0 +1,3 @@
1
+ import type { Diagnosis } from "@skill-harness/core";
2
+ /** Human view of the same offline report; never a second scoring policy. */
3
+ export declare function formatDiagnosis(report: Diagnosis): string;
@@ -0,0 +1,37 @@
1
+ import { modelProfileLabel } from "@skill-harness/core";
2
+ /** Human view of the same offline report; never a second scoring policy. */
3
+ export function formatDiagnosis(report) {
4
+ const grouped = report.criterionDimensions;
5
+ const lines = [
6
+ `${grouped.skill} · ${grouped.model} · ${grouped.mode}`,
7
+ "Saved criterion votes — current author-declared grouping, not a new assessment.",
8
+ "Counts are criterion observations, not passing answers. Mixed criteria remain indivisible.",
9
+ "No model calls; saved results and acceptance rules unchanged.",
10
+ `Coverage: ${grouped.coverage.availableVotes}/${grouped.coverage.expectedVotes} known-size votes available; ${grouped.coverage.unknownScenarioSizes} scenario sizes unknown.`,
11
+ `Mapping: ${grouped.metadataSha256}`,
12
+ ];
13
+ if (report.summary) {
14
+ const summary = report.summary;
15
+ lines.push(`Subject requested: ${modelProfileLabel(summary.subject.requested)}; effective thinking: unknown.`, `Judge requested: ${modelProfileLabel(summary.judge.requested)}; effective thinking: unknown.`, `Retained token observations: ${summary.usage.subject.recordedInvocations} subject invocations (total unknown); ${summary.usage.judge.recordedInvocations}/${summary.usage.judge.expectedInvocations ?? "unknown"} judge attempts. Full per-counter coverage: --format json. Costs are separate.`, summary.limits);
16
+ }
17
+ for (const scenario of grouped.scenarios) {
18
+ lines.push("", `${scenario.id}: recorded ${scenario.original.judge_verdict}; override ${scenario.original.override ?? "none"}; objective ${scenario.original.objective?.status ?? "not recorded"}`);
19
+ for (const [dimension, counts] of Object.entries(scenario.counts)) {
20
+ if (counts.pass + counts.fail + counts.unavailable > 0)
21
+ lines.push(` ${dimension}: ${counts.pass} PASS, ${counts.fail} FAIL, ${counts.unavailable} unavailable`);
22
+ }
23
+ const reasons = new Set([scenario.unavailableReason, ...scenario.repetitions.map(rep => rep.unavailableReason)].filter(Boolean));
24
+ for (const reason of reasons)
25
+ lines.push(` Unavailable: ${reason}`);
26
+ }
27
+ if (report.issues.length) {
28
+ lines.push("", "Evidence and next checks:");
29
+ for (const issue of report.issues)
30
+ lines.push(`- ${issue.scenario ? issue.scenario + ": " : ""}${issue.category}: ${issue.evidence}\n Next: ${issue.next}`);
31
+ }
32
+ lines.push("", "Reuse plan:");
33
+ for (const item of report.reusePlan.scenarios)
34
+ lines.push(`- ${item.id}: ${item.action} — ${item.note}`);
35
+ return lines.join("\n");
36
+ }
37
+ //# sourceMappingURL=diagnose-format.js.map
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
1
  {
2
- "name": "@skill-harness/cli",
3
- "version": "0.32.3",
4
- "description": "skill-harness CLI — run, grade, review, and lint agent-skill scenarios on the pi harness",
5
- "type": "module",
6
- "license": "MIT",
7
- "main": "./dist/cli.js",
8
- "bin": {
9
- "skill-harness": "dist/cli.js"
10
- },
11
- "exports": {
12
- ".": "./dist/cli.js",
13
- "./serve": "./dist/serve.js"
14
- },
15
- "keywords": [
16
- "agent-skills",
17
- "skill",
18
- "llm",
19
- "testing",
20
- "eval",
21
- "grading",
22
- "pi",
23
- "ci",
24
- "harness"
25
- ],
26
- "files": [
27
- "dist/**/*.js",
28
- "dist/**/*.d.ts",
29
- "assets",
30
- "LICENSE",
31
- "README.md"
32
- ],
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/mojomanyana/skill-harness.git"
36
- },
37
- "publishConfig": {
38
- "access": "public"
39
- },
40
- "engines": {
41
- "node": ">=20"
42
- },
43
- "scripts": {
44
- "prepack": "node ../../scripts/release-pack.mjs --guard"
45
- },
46
- "dependencies": {
47
- "@skill-harness/core": "0.32.3",
48
- "@skill-harness/adapters": "0.32.3"
49
- }
2
+ "name": "@skill-harness/cli",
3
+ "version": "0.33.0",
4
+ "description": "skill-harness CLI \u2014 run, grade, review, and lint agent-skill scenarios on the pi harness",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/cli.js",
8
+ "bin": {
9
+ "skill-harness": "dist/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": "./dist/cli.js",
13
+ "./serve": "./dist/serve.js"
14
+ },
15
+ "keywords": [
16
+ "agent-skills",
17
+ "skill",
18
+ "llm",
19
+ "testing",
20
+ "eval",
21
+ "grading",
22
+ "pi",
23
+ "ci",
24
+ "harness"
25
+ ],
26
+ "files": [
27
+ "dist/**/*.js",
28
+ "dist/**/*.d.ts",
29
+ "assets",
30
+ "LICENSE",
31
+ "README.md"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/mojomanyana/skill-harness.git"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "scripts": {
44
+ "prepack": "node ../../scripts/release-pack.mjs --guard"
45
+ },
46
+ "dependencies": {
47
+ "@skill-harness/core": "0.33.0",
48
+ "@skill-harness/adapters": "0.33.0"
49
+ }
50
50
  }