@skill-harness/cli 0.16.0 → 0.18.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
@@ -24,19 +24,6 @@ npm i -g @skill-harness/cli
24
24
  | `skill-harness grade <run-dir> [--judge p:m]` | re-grade saved transcripts |
25
25
  | `skill-harness review <skill> --skills <root> [--port N]` | serve the interactive review UI |
26
26
  | `skill-harness add-test <skill> --skills <root> --id ID --title T ...` | scaffold a new scenario |
27
- | `skill-harness qualification prepare\|start\|status\|poll\|validate\|abort ...` | durable external-config qualification lifecycle |
28
-
29
- ## Retained learning
30
-
31
- `skill-harness learning` opens the guided offline workflow; `learning help` documents
32
- named retained inputs, complete quality review, scoped trust, independent labels,
33
- separate adoption decisions and later observations. No hand-authored manifests or model calls.
34
- The Pi package exposes the same flow as `/skill-harness learning`; connected producer registry
35
- activation/rollback remain separately authorized operations, never a side effect of reading.
36
- `@skill-harness/cli/learning` exports `runLearningCommand`, `runLearningWizard` and
37
- `reviewLearningComparison` for the existing producer dashboard bridge.
38
-
39
- [Product guide](https://github.com/mojomanyana/skill-harness/blob/main/docs/factory/PRODUCT-GUIDE.md).
40
27
 
41
28
  ## More
42
29
 
@@ -255,13 +255,7 @@ function render() {
255
255
  const objMark = obj
256
256
  ? `<span class='lm obj-${obj.status.toLowerCase()}' title='${escapeHtml(`objective ${obj.status}: ${obj.detail}`)}'>◉ ${escapeHtml(obj.status.toLowerCase())}</span>`
257
257
  : "";
258
- // Adjudication. `unresolved` is amber and says so in words — a disagreement
259
- // that renders as a clean verdict is the whole failure this feature prevents.
260
- const adj = cell.adjudication;
261
- const adjMark = adj
262
- ? `<span class='lm adj-${adj.state === "unresolved" ? "unresolved" : "settled"}' title='${escapeHtml(`adjudication ${adj.state} (${adj.trigger}) · ${adj.detail}`)}'>⚖ ${adj.state === "unresolved" ? "unresolved" : `${adj.count} agree`}</span>`
263
- : "";
264
- html += `<td class='cell ${v}${sel}' data-col='${col.index}' data-id='${scn.id}'>${v}${ov}${suspectBadge}${liftMark}${stabMark}${objMark}${adjMark}${reps}</td>`;
258
+ html += `<td class='cell ${v}${sel}' data-col='${col.index}' data-id='${scn.id}'>${v}${ov}${suspectBadge}${liftMark}${stabMark}${objMark}${reps}</td>`;
265
259
  });
266
260
  html += "</tr>";
267
261
  }
@@ -290,7 +284,6 @@ async function openPanel(colIndex, scenarioId) {
290
284
  ${cell.suspect ? `<div class="reason" style="color:#b45309"><b>⚠ suspect:</b> judge listed no failed item — re-judge before trusting this FAIL</div>` : ""}
291
285
  ${cell.stability ? `<div class="reason" style="color:#b45309"><b>⇄ boundary cell:</b> ${escapeHtml(cell.stability.note)}</div>` : ""}
292
286
  ${cell.objective ? `<div class="reason"><b>◉ objective ${escapeHtml(cell.objective.status)}:</b> ${escapeHtml(cell.objective.detail)}</div>` : ""}
293
- ${cell.adjudication ? `<div class="reason"${cell.adjudication.state === "unresolved" ? ' style="color:#b45309"' : ""}><b>⚖ adjudication ${escapeHtml(cell.adjudication.state)}</b> (${escapeHtml(cell.adjudication.trigger)}): ${escapeHtml(cell.adjudication.detail)}</div>` : ""}
294
287
  ${cell.metrics ? `<div class="reason"><b>cost/latency:</b> ${cell.metrics.input_tokens == null ? "subject tokens unavailable" : `${cell.metrics.input_tokens} input / ${cell.metrics.output_tokens || 0} output / ${cell.metrics.cache_read_tokens || 0} cache-read`} (${cell.metrics.subject_metrics_reps}/${cell.metrics.total_reps} reps) · ${cell.metrics.judge_calls} judge + ${cell.metrics.judge_rejudge_calls} re-judge · ${cell.metrics.wall_time_ms}ms · ${cell.metrics.tool_calls == null ? "tool calls unavailable" : `${cell.metrics.tool_calls} tools, ${cell.metrics.delegated_children || 0} delegated, max concurrency ${cell.metrics.max_concurrency || 0}`}</div>` : ""}
295
288
  <div class="toggle">
296
289
  <button data-v="PASS" class="PASS ${cell.override === 'PASS' ? 'active PASS' : ''}">PASS</button>
@@ -298,7 +291,6 @@ async function openPanel(colIndex, scenarioId) {
298
291
  <button data-v="" class="JUDGE ${!cell.override ? 'active JUDGE' : ''}">use judge (${cell.judge_verdict})</button>
299
292
  </div>
300
293
  <div class="rejudge"><button id="rejudgeBtn">Re-judge (${escapeHtml(col.judge.provider + ":" + col.judge.model)})</button> <span class="saved" id="rejudged"></span></div>
301
- <div class="rejudge"><button id="adjBtn">Adjudicate column…</button> <span class="saved" id="adjudged"></span></div>
302
294
  <label class="fld">note <span class="saved" id="saved">saved ✓</span></label>
303
295
  <textarea id="note" placeholder="why you overrode / what to fix in SKILL.md">${escapeHtml(cell.note || "")}</textarea>
304
296
  <label class="fld">transcript</label>
@@ -323,40 +315,6 @@ async function openPanel(colIndex, scenarioId) {
323
315
  location.reload(); // re-judge rewrote results.yaml; reload the fresh matrix + grades
324
316
  } catch (e) { rj.disabled = false; rj.textContent = "Re-judge"; }
325
317
  };
326
-
327
- // Two requests on purpose. The first prices the work and spends nothing; the
328
- // confirm shows that exact ceiling; only then does anything call a judge. A
329
- // single endpoint could not disclose a count before charging for it.
330
- const adjBtn = document.getElementById("adjBtn");
331
- if (adjBtn) adjBtn.onclick = async () => {
332
- const status = document.getElementById("adjudged");
333
- const show = (msg, err) => { if (status) { status.textContent = msg; status.classList.toggle("err", !!err); status.classList.add("show"); } };
334
- adjBtn.disabled = true;
335
- try {
336
- const pr = await fetch("/adjudicate", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ col: colIndex, step: "plan" }) });
337
- const plan = await pr.json().catch(() => ({}));
338
- if (!pr.ok || !plan.ok) { show(plan.error || "could not plan adjudication", true); adjBtn.disabled = false; return; }
339
- if (!plan.triggered.length) { show("no cell triggered — nothing to adjudicate", false); adjBtn.disabled = false; return; }
340
-
341
- // Counts, never dollars: the default judge is a Claude subscription and
342
- // reports no per-call usage, so a cost figure here would be invented.
343
- const ok = window.confirm(
344
- `Adjudicate ${plan.triggered.length} cell(s) with ${plan.judge}?\n\n` +
345
- `Up to ${plan.maxAdditionalCalls} additional judge call(s).\n` +
346
- `No tie-break judge from the browser — a disagreement stays unresolved and blocks SHIP.\n\n` +
347
- plan.detail.join("\n")
348
- );
349
- if (!ok) { show("cancelled — nothing spent", false); adjBtn.disabled = false; return; }
350
-
351
- adjBtn.textContent = "Adjudicating…";
352
- const rr = await fetch("/adjudicate", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ col: colIndex, step: "run" }) });
353
- const done = await rr.json().catch(() => ({}));
354
- if (!rr.ok || !done.ok) { show(done.error || "adjudication failed", true); adjBtn.disabled = false; adjBtn.textContent = "Adjudicate column…"; return; }
355
- location.reload();
356
- } catch (e) {
357
- show("adjudication failed", true); adjBtn.disabled = false; adjBtn.textContent = "Adjudicate column…";
358
- }
359
- };
360
318
  let t;
361
319
  document.getElementById("note").addEventListener("input", (e) => {
362
320
  cell.note = e.target.value;
package/dist/cli.d.ts CHANGED
@@ -23,21 +23,13 @@ export declare function releaseExitCode(summaries: Array<{
23
23
  };
24
24
  }>): 0 | 1;
25
25
  export declare function cmdRun(args: Args): Promise<void>;
26
- export declare function cmdCompare(args: Args, adapterOverride?: HarnessAdapter): Promise<void>;
27
26
  export declare function cmdGrade(args: Args, adapterOverride?: HarnessAdapter): Promise<void>;
28
- /**
29
- * Re-score saved runs against the current spec's thresholds — no model or judge calls.
30
- * Reps are the measurement; thresholds are policy. When policy changes, recompute rather
31
- * than reconcile two numbers in prose.
32
- */
33
- export declare function cmdJudgeAgreement(args: Args): void;
34
27
  /**
35
28
  * Re-evaluate needle gates against the saved staged diffs — no subject call. A rep
36
29
  * whose gate verdict flips from fail to pass was never judged and costs one judge call.
37
30
  * The completion summary reports how many judge calls were made.
38
31
  */
39
32
  export declare function cmdRegate(args: Args, adapterOverride?: HarnessAdapter): Promise<void>;
40
- export declare function cmdScreen(args: Args): Promise<void>;
41
33
  export declare function cmdInit(args: Args): Promise<void>;
42
34
  export declare function cmdSuggest(args: Args, adapterOverride?: HarnessAdapter): Promise<void>;
43
35
  /**
package/dist/cli.js CHANGED
@@ -1,15 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync, readdirSync } from "node:fs";
3
- import { load as yamlLoad } from "js-yaml";
2
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
4
3
  import { basename, dirname, join, resolve, relative } from "node:path";
5
4
  import { tmpdir } from "node:os";
6
- 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, resolveAdjudicationJudges, adjudicateRun, judgeResemblesSubject, computeCoverage, formatCoverage, selectAffected, formatAffected, gitDiff, exec, HARNESS_VERSION, defaultJudge, assertJudgeAllowed, judgeAgreement, assertNotDowngraded, downgradeWarning, isScoredMode, resolveArm, isFreeOfflineCommand, screenResults, formatScreen, } 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, assertJudgeAllowed, assertNotDowngraded, downgradeWarning, isScoredMode, resolveArm, isFreeOfflineCommand, } from "@skill-harness/core";
7
6
  import { getAdapter } from "@skill-harness/adapters";
8
7
  import { serveReview } from "./serve.js";
9
- import { runCompareCommand } from "./compare.js";
10
- import { cmdQualification } from "./qualification.js";
11
- import { cmdArchive } from "./archive.js";
12
- import { runLearningCommand } from "./learning.js";
13
8
  const DEFAULT_MODEL = "fireworks:accounts/fireworks/models/deepseek-v4-pro";
14
9
  // The judge default lives in core (`defaultJudge()`), which resolves
15
10
  // SKILL_HARNESS_JUDGE over a baked value — it was duplicated in three places
@@ -46,6 +41,16 @@ function parseArgs(argv) {
46
41
  }
47
42
  return { _, flags, multi };
48
43
  }
44
+ function assertNoRetiredFlags(command, args) {
45
+ const retired = command === "run"
46
+ ? ["affected"]
47
+ : command === "grade"
48
+ ? ["auto-rejudge", "secondary-judge", "tie-break-judge"]
49
+ : [];
50
+ const found = retired.find((flag) => Object.hasOwn(args.flags, flag));
51
+ if (found)
52
+ throw new Error(`--${found} was removed; this command refuses to silently run with different behavior`);
53
+ }
49
54
  export function flagStr(args, key, fallback) {
50
55
  const v = args.flags[key];
51
56
  if (typeof v === "string")
@@ -143,11 +148,7 @@ export async function cmdRun(args) {
143
148
  // that was never the problem. Free, offline validation first — it is also why
144
149
  // CI, which has no `pi`, could not test this refusal at all.
145
150
  const onlyRaw = flagStr(args, "only");
146
- let only = onlyRaw ? onlyRaw.split(",").map((x) => x.trim()).filter(Boolean) : undefined;
147
- const affected = flagBool(args, "affected");
148
- if (affected && only) {
149
- throw new Error("--affected and --only both choose the scenario set — pass one, not both");
150
- }
151
+ const only = onlyRaw ? onlyRaw.split(",").map((x) => x.trim()).filter(Boolean) : undefined;
151
152
  const harnessName = flagStr(args, "harness", "pi");
152
153
  const adapter = getAdapter(harnessName);
153
154
  if (!(await adapter.available()))
@@ -176,17 +177,6 @@ export async function cmdRun(args) {
176
177
  // that look comparable and are not. Checked per skill, before its first token.
177
178
  assertNotDowngraded(skill.dir, "run");
178
179
  const spec = loadSpec(skill.specPath);
179
- if (affected) {
180
- // Reuses the exact `--only` machinery, so an affected run is partial and
181
- // cannot report SHIP — the same guarantee, through the same code path.
182
- const result = await computeAffected(args, spec.scenarios, skill.specPath);
183
- console.log(formatAffected(result, spec.scenarios.length));
184
- only = result.selected.map((sel) => sel.id);
185
- if (only.length === 0) {
186
- console.log(`skip ${skill.name}: no scenario is affected by this change`);
187
- continue;
188
- }
189
- }
190
180
  for (const token of modelTokens) {
191
181
  const model = parseModelRef(token);
192
182
  // The version is on the banner because a stale global install is otherwise
@@ -229,98 +219,10 @@ export async function cmdRun(args) {
229
219
  console.log(`\nReview interactively: skill-harness review ${skills[0]?.name ?? "<skill>"} --skills ${root}`);
230
220
  // A full delivered run is a release gate. NOT READY — including one critical
231
221
  // failure hidden by a high aggregate — must be machine-visible to CI. Red
232
- // baselines and partial/affected branch feedback are deliberately excluded.
222
+ // baselines and partial branch feedback are deliberately excluded.
233
223
  if (releaseExitCode(summaries) !== 0)
234
224
  process.exitCode = 1;
235
225
  }
236
- export async function cmdCompare(args, adapterOverride) {
237
- const target = args._[0];
238
- const reference = flagStr(args, "reference");
239
- const candidateRoot = flagStr(args, "candidate");
240
- if (!target || !reference || !candidateRoot) {
241
- throw new Error("usage: skill-harness compare <skill|all> --reference <git-ref-or-skills-root> --candidate <skills-root> --model <provider:model> --reps N");
242
- }
243
- const judgeFlag = flagStr(args, "judge");
244
- const judgeToken = judgeFlag ?? defaultJudge();
245
- const judge = parseModelRef(judgeToken);
246
- assertJudgeAllowed(judge, {
247
- source: judgeFlag ? "--judge" : "the default judge (SKILL_HARNESS_JUDGE or the baked value)",
248
- allowMetered: flagBool(args, "allow-metered-judge"),
249
- });
250
- const harnessName = flagStr(args, "harness", "pi");
251
- const adapter = adapterOverride ?? getAdapter(harnessName);
252
- if (!(await adapter.available()))
253
- throw new Error(`harness \`${harnessName}\` is not on PATH`);
254
- const mode = (flagStr(args, "mode", "force") || "force");
255
- if (mode === "red")
256
- throw new Error("compare measures a skill candidate, so --mode must be green or force (red is a no-skill baseline)");
257
- const tuning = parseRunTuning(args);
258
- const onlyRaw = flagStr(args, "only");
259
- let only = onlyRaw ? onlyRaw.split(",").map((id) => id.trim()).filter(Boolean) : undefined;
260
- const affected = flagBool(args, "affected");
261
- if (affected && only)
262
- throw new Error("--affected and --only both choose the comparison scenario set — pass one, not both");
263
- if (affected) {
264
- if (target === "all")
265
- throw new Error("compare --affected currently requires one skill so each selected ID has an unambiguous spec");
266
- const skill = resolveSkill(candidateRoot, target);
267
- const spec = loadSpec(skill.specPath);
268
- const selectionArgs = {
269
- ...args,
270
- flags: {
271
- ...args.flags,
272
- // A git-ref reference is the natural diff base. A reference directory
273
- // has no common history, so the caller must supply --base explicitly.
274
- ...(!flagStr(args, "base") && !existsSync(reference) ? { base: reference } : {}),
275
- },
276
- };
277
- if (existsSync(reference) && !flagStr(args, "base")) {
278
- throw new Error("compare --affected with a reference directory needs --base <git-ref> for candidate change selection");
279
- }
280
- const selected = await computeAffected(selectionArgs, spec.scenarios, skill.specPath);
281
- console.log(formatAffected(selected, spec.scenarios.length));
282
- only = selected.selected.map((entry) => entry.id);
283
- if (only.length === 0) {
284
- console.log("compare: no affected scenarios — 0 subject calls, 0 judge calls; no release claim produced");
285
- return;
286
- }
287
- }
288
- const threshold = (name) => {
289
- const raw = flagStr(args, name);
290
- if (raw === undefined)
291
- return undefined;
292
- const value = Number(raw);
293
- if (!Number.isFinite(value) || value < 0)
294
- throw new Error(`--${name} must be a non-negative ratio (got \`${raw}\`)`);
295
- return value;
296
- };
297
- const thresholds = {
298
- max_subject_token_increase: threshold("max-subject-token-increase"),
299
- max_wall_time_increase: threshold("max-wall-time-increase"),
300
- max_tool_call_increase: threshold("max-tool-call-increase"),
301
- };
302
- const hasThreshold = Object.values(thresholds).some((value) => value !== undefined);
303
- const result = await runCompareCommand({
304
- target,
305
- reference,
306
- candidateRoot,
307
- models: resolveModels(args),
308
- judgeToken,
309
- mode,
310
- reps: tuning.reps,
311
- passThreshold: tuning.passThreshold,
312
- parallel: Math.max(1, Number(flagStr(args, "parallel", "1")) || 1),
313
- only,
314
- canary: flagBool(args, "canary"),
315
- output: flagStr(args, "output") || undefined,
316
- thresholds: hasThreshold ? thresholds : undefined,
317
- adapter,
318
- now: nowIso,
319
- });
320
- console.log(`\ncomparison artifacts: ${result.outputDir}`);
321
- if (result.exitCode !== 0)
322
- process.exitCode = result.exitCode;
323
- }
324
226
  export async function cmdGrade(args, adapterOverride) {
325
227
  const runDir = args._[0];
326
228
  if (!runDir)
@@ -360,30 +262,7 @@ export async function cmdGrade(args, adapterOverride) {
360
262
  for (const s of results.scenarios) {
361
263
  console.log(` ${s.id} → ${s.judge_verdict}: ${s.judge_reason}`);
362
264
  }
363
- let final = results;
364
- // Adjudication is opt-in. Without --auto-rejudge nothing below runs and not one
365
- // extra call is made — a spec may declare triggers, but spec configuration alone
366
- // never authorizes spending.
367
- const judges = resolveAdjudicationJudges({
368
- enabled: flagBool(args, "auto-rejudge"),
369
- primary: judge,
370
- secondaryToken: flagStr(args, "secondary-judge"),
371
- tieBreakToken: flagStr(args, "tie-break-judge"),
372
- subjectToken: results.model,
373
- parseRef: parseModelRef,
374
- assertAllowed: (j, source) => assertJudgeAllowed(j, { source, allowMetered: flagBool(args, "allow-metered-judge") }),
375
- resemblesSubject: judgeResemblesSubject,
376
- warn: (m) => console.error(m),
377
- });
378
- if (judges) {
379
- final = await adjudicateRun({
380
- runDir, spec, adapter, results, primaryJudge: judge,
381
- secondaryJudge: judges.secondary, tieBreakJudge: judges.tieBreak,
382
- specDir: testsDir, now: nowIso,
383
- log: (m) => console.log(m),
384
- });
385
- }
386
- const g = final.effective_grade;
265
+ const g = results.effective_grade;
387
266
  console.log(`\n re-graded with ${judge.provider}:${judge.model} → ${g.letter} (${g.pct}%) ${g.ship ? "SHIP" : "NOT READY"}`);
388
267
  }
389
268
  /**
@@ -391,20 +270,6 @@ export async function cmdGrade(args, adapterOverride) {
391
270
  * Reps are the measurement; thresholds are policy. When policy changes, recompute rather
392
271
  * than reconcile two numbers in prose.
393
272
  */
394
- export function cmdJudgeAgreement(args) {
395
- const raw = args._[0];
396
- if (!raw)
397
- throw new Error("usage: skill-harness judge-agreement <run-dir>");
398
- const runDir = resolve(raw);
399
- if (!existsSync(runDir))
400
- throw new Error(`run dir not found: ${runDir}`);
401
- const report = judgeAgreement(readResults(runDir));
402
- for (const cell of report.cells) {
403
- console.log(` ${cell.status === "agree" ? "=" : cell.status === "disagree" ? "≠" : "?"} ${cell.scenario}: ${cell.status} — ${cell.detail}${cell.judges.length ? ` (${cell.judges.join(" vs ")})` : ""}`);
404
- }
405
- console.log(`\njudge agreement: ${report.agree} agree / ${report.disagree} disagree / ${report.error} error; ${report.rate === null ? "n/a" : `${(report.rate * 100).toFixed(1)}%`} across ${report.comparable} comparable scenario(s).`);
406
- console.log("offline report only; no model or judge calls.");
407
- }
408
273
  async function cmdRescore(args) {
409
274
  const runDirs = args._;
410
275
  if (runDirs.length === 0)
@@ -523,13 +388,6 @@ async function cmdRestamp(args) {
523
388
  console.log(`\n${runs} run(s) examined: ${upgraded} upgraded, ${unprovable} left alone, ${unchanged} already current.` +
524
389
  `${partial > 0 ? ` (${partial} upgraded only in part.)` : ""} No models were called.`);
525
390
  }
526
- export async function cmdScreen(args) {
527
- if (args._.length === 0)
528
- throw new Error("usage: skill-harness screen <run-dir> [<run-dir> ...]");
529
- const results = args._.map(runDir => readResults(resolve(runDir)));
530
- console.log(formatScreen(screenResults(results)));
531
- console.log(`\n${results.length} retained result(s) screened; 0 subject calls, 0 judge calls.`);
532
- }
533
391
  async function cmdStability(args) {
534
392
  const root = flagStr(args, "skills", process.cwd());
535
393
  const target = args._[0] ?? "all";
@@ -652,7 +510,6 @@ async function cmdCoverage(args) {
652
510
  // always points at, so report on it even when nothing references it —
653
511
  // otherwise a skill with zero `covers` reports 0 sections and looks fine.
654
512
  baseFiles: [relative(specDir, join(skill.dir, "SKILL.md")).split("\\").join("/")],
655
- pendingCaptures: readPendingCaptures(specDir),
656
513
  });
657
514
  console.log(formatCoverage(report, spec.skill));
658
515
  if (report.uncovered.length)
@@ -672,54 +529,6 @@ async function cmdCoverage(args) {
672
529
  process.exitCode = 1;
673
530
  }
674
531
  }
675
- /** Pending captures and the sections they are parked against. Free, offline, tolerant. */
676
- function readPendingCaptures(specDir) {
677
- const dir = join(specDir, "captures");
678
- if (!existsSync(dir))
679
- return [];
680
- const out = [];
681
- for (const file of readdirSync(dir).filter((f) => f.endsWith(".yaml"))) {
682
- try {
683
- const raw = yamlLoad(readFileSync(join(dir, file), "utf8"));
684
- if (!raw || raw.status === "promoted")
685
- continue;
686
- const covers = Array.isArray(raw.covers) ? raw.covers.filter((c) => typeof c === "string") : [];
687
- if (covers.length)
688
- out.push({ id: String(raw.id ?? file.replace(/\.yaml$/, "")), covers });
689
- }
690
- catch {
691
- // A malformed capture is the capture command's problem to report; coverage
692
- // must not fail because a draft file is mid-edit.
693
- }
694
- }
695
- return out;
696
- }
697
- /** `affected` — which scenarios a change could plausibly touch. Free and offline. */
698
- async function cmdAffected(args) {
699
- const root = flagStr(args, "skills", process.cwd());
700
- const target = args._[0];
701
- if (!target)
702
- throw new Error("usage: skill-harness affected <skill> --skills <root> [--base <git-ref>]");
703
- const skill = resolveSkill(root, target);
704
- if (!skill.hasSpec)
705
- throw new Error(`${target} has no spec`);
706
- const spec = loadSpec(skill.specPath);
707
- const result = await computeAffected(args, spec.scenarios, skill.specPath);
708
- console.log(formatAffected(result, spec.scenarios.length));
709
- }
710
- /** Shared by `affected` and `run --affected`, so the two can never disagree. */
711
- async function computeAffected(args, scenarios, specPath) {
712
- const base = flagStr(args, "base", "HEAD");
713
- const repoRoot = await gitRepoRoot(dirname(specPath));
714
- const diff = await gitDiff(repoRoot, base);
715
- return selectAffected({ scenarios, specDir: dirname(specPath), diff, repoRoot });
716
- }
717
- async function gitRepoRoot(from) {
718
- const r = await exec("git", ["rev-parse", "--show-toplevel"], { cwd: from, timeoutMs: 30_000 });
719
- if (r.code !== 0)
720
- throw new Error(`not a git repository (from ${from}) — --affected needs one to diff against`);
721
- return r.stdout.trim();
722
- }
723
532
  /** Write a spec to disk, creating its tests/ dir. The single choke point for spec
724
533
  * writes (init/suggest) so a future atomic-write/backup/audit change lands in one place. */
725
534
  function writeSpecFile(specPath, text) {
@@ -881,31 +690,16 @@ export function help() {
881
690
  return `skill-harness ${HARNESS_VERSION} — test/optimize loop for agent skills (pi harness)
882
691
 
883
692
  run <skill|all> --skills <root> [--model prov:model ...] [--models file] [--only A1,D2]
884
- [--affected --base <git-ref>] run only the scenarios a change could touch (partial; never SHIPs)
885
693
  [--mode red|green|force] [--judge prov:model] [--harness pi] [--label name] [--parallel N] [--reps N] [--pass-threshold T]
886
694
  [--canary] green only: spend ONE probe proving the skill reached the model, and abort the run if it did not
887
695
  [--structured] record subject tokens, cost and wall time (needs pi --mode json)
888
696
  [--arm <name>] measure under a named arm from <skills-root>/tests/arms.yaml
889
697
  (loads its extensions, seeds pi-daddy definitions, tags the run dir)
890
- compare <skill|all> --reference <git-ref-or-skills-root> --candidate <skills-root>
891
- [--model prov:model ...] [--mode green|force] [--judge prov:model] [--reps N] [--only IDs | --affected --base ref]
892
- [--output dir] [--max-subject-token-increase R] [--max-wall-time-increase R] [--max-tool-call-increase R]
893
- paired setup (not seeded sampling); critical regression exit 2, ordinary regression exit 1
894
698
  grade <run-dir> [--judge prov:model] [--suspect-only] re-grade saved transcripts (neutral judge)
895
- [--auto-rejudge] [--secondary-judge p:m] [--tie-break-judge p:m]
896
- ask again about untrustworthy cells (ambiguous / contradictory / non-unanimous /
897
- ship-deciding). OFF by default; prints the exact MAX extra call count first.
898
- archive ingest|inspect|watch --policy file --source id explicit external ingestion/metadata (${free("archive")})
899
- learning [status|import|review|trust|decide|adoption|outcome|guide] guided retained learning (${free("learning")}; no models)
900
- use learning help; quality, trust, adoption and later outcomes stay separate
901
- archive weekly|trust|access --state /private/dir --request file durable learning/consent lifecycle (${free("archive")})
902
- watch requires --max-polls N; optional --interval-ms N and --previous checkpoint
903
- judge-agreement <run-dir> compare two distinct persisted judge votes per scenario (${free("judge-agreement")})
904
699
  rescore <run-dir>... re-score saved reps vs current spec thresholds (${free("rescore")})
905
700
  regate <run-dir>... [--judge prov:model] re-evaluate saved gates (no subject call; judges fail→pass reps)
906
701
  restamp <skill|all> --skills <root> [--from <git-ref>] record the model-visible skill digest on runs that still match (${free("restamp")}; one-time migration)
907
702
  stability <skill|all> --skills <root> [--window N] [--all] run-over-run verdict flips per scenario (${free("stability")})
908
- screen <run-dir>... retained control/treatment + criterion rates (${free("screen")})
909
703
  review <skill> --skills <root> [--port N] serve the interactive review UI
910
704
  add-test <skill> --skills <root> --id ID --title T --turn ... --check ... [--critical] [--mode seeded --fixture path]
911
705
  init <skill> --skills <root> [--force] scaffold a commented template spec (${free("init")})
@@ -913,12 +707,6 @@ export function help() {
913
707
  list --skills <root> discovered skills + spec status (${free("list")})
914
708
  lint <skill|all> --skills <root> validate specs/fixtures + results-consistency (${free("lint")}; CI gate; exits non-zero on findings)
915
709
  coverage <skill|all> --skills <root> [--strict] which instruction sections have a declared test (${free("coverage")})
916
- affected <skill> --skills <root> [--base ref] which scenarios a change could touch (${free("affected")})
917
- qualification <prepare|start|status|poll|validate|panel|cell|abort> durable qualification-runner-v1 lifecycle + offline panel collapse
918
- prepare --spool DIR --config FILE --request FILE [--expected-config-sha256 HEX] (required in production)
919
- start|status|poll|abort --spool DIR --id ID (abort also requires --reason ID)
920
- start may resume only with --continuation-authority-file <private-0600-prebound-one-use-capability>
921
- validate --spool DIR (all calls use external, schema-validated arms; no automatic retry)
922
710
 
923
711
  version print ${HARNESS_VERSION} and exit (also --version / -v)
924
712
 
@@ -931,22 +719,15 @@ defaults: model=${DEFAULT_MODEL} judge=${defaultJudge()} mode=green harness=p
931
719
  }
932
720
  export async function main(argv) {
933
721
  const cmd = argv[0];
934
- if (cmd === "learning") {
935
- await runLearningCommand(argv.slice(1));
936
- return;
937
- }
938
722
  const args = parseArgs(argv.slice(1));
723
+ assertNoRetiredFlags(cmd, args);
939
724
  switch (cmd) {
940
725
  case "run": return cmdRun(args);
941
- case "compare": return cmdCompare(args);
942
726
  case "grade": return cmdGrade(args);
943
- case "archive": return cmdArchive(args);
944
- case "judge-agreement": return cmdJudgeAgreement(args);
945
727
  case "rescore": return cmdRescore(args);
946
728
  case "regate": return cmdRegate(args);
947
729
  case "restamp": return cmdRestamp(args);
948
730
  case "stability": return cmdStability(args);
949
- case "screen": return cmdScreen(args);
950
731
  case "review": return cmdReview(args);
951
732
  case "add-test": return cmdAddTest(args);
952
733
  case "init": return cmdInit(args);
@@ -954,8 +735,6 @@ export async function main(argv) {
954
735
  case "list": return cmdList(args);
955
736
  case "lint": return cmdLint(args);
956
737
  case "coverage": return cmdCoverage(args);
957
- case "affected": return cmdAffected(args);
958
- case "qualification": return cmdQualification(args);
959
738
  case "version":
960
739
  case "--version":
961
740
  case "-v":
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, appendJudgeHistory, refreshRubricHashes, findJudgeRawFiles, effectiveThreshold, scoreContextFor, isScoredMode, rebuildScenarioResult, mergeScenarioMetrics, carryRepObjectives, envFlag, planAdjudication, adjudicateRun, assertJudgeAllowed, cellsFromResults, } 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, } 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/. */
@@ -142,15 +142,8 @@ export async function serveReview(opts) {
142
142
  const merged = results.scenarios.map((s) => {
143
143
  if (s.id !== body.scenarioId)
144
144
  return s;
145
- const judge_history = appendJudgeHistory(s, results.judge, {
146
- judge: results.judge,
147
- verdict: rr.judge_verdict,
148
- reason: rr.judge_reason,
149
- suspect: rr.suspect,
150
- criteria: rr.rep_judgments?.find(panel => panel.repetition === 0)?.judgments[0]?.criteria,
151
- });
152
145
  // Same contract as `grade`, through the same choke point.
153
- return rebuildScenarioResult({ ...rr, metrics: mergeScenarioMetrics(s.metrics, rr.metrics), rep_judgments: carryRepObjectives(rr.rep_judgments, s.rep_judgments), judge_history }, s, { objective: "carry", adjudication: "drop" });
146
+ return rebuildScenarioResult({ ...rr, metrics: mergeScenarioMetrics(s.metrics, rr.metrics), rep_judgments: carryRepObjectives(rr.rep_judgments, s.rep_judgments) }, s, { objective: "carry", adjudication: "drop" });
154
147
  });
155
148
  const written = writeResults(column.runDir, {
156
149
  schema: results.schema, subject_invocations: results.subject_invocations,
@@ -187,79 +180,6 @@ export async function serveReview(opts) {
187
180
  }
188
181
  return;
189
182
  }
190
- /**
191
- * Adjudication, in two steps: `plan` returns the exact call ceiling and
192
- * spends nothing; `run` executes it.
193
- *
194
- * Two requests rather than one because the ceiling has to be disclosed
195
- * BEFORE anything is spent, and a single endpoint that both prices and
196
- * charges cannot do that — the UI would be showing a count for work that
197
- * had already happened.
198
- */
199
- if (req.method === "POST" && url.pathname === "/adjudicate") {
200
- const body = JSON.parse((await readBody(req)) || "{}");
201
- const data = collectReport(opts.skillDir);
202
- const column = data.columns.find((c) => c.index === body.col);
203
- if (!column) {
204
- res.writeHead(404).end("unknown column");
205
- return;
206
- }
207
- const results = readResults(column.runDir);
208
- if (!isScoredMode(results.mode)) {
209
- res.writeHead(400, { "content-type": "application/json" });
210
- res.end(JSON.stringify({ ok: false, error: `only scored runs (green/force) can be adjudicated — for a ${results.mode} run use \`skill-harness grade\`` }));
211
- return;
212
- }
213
- const specPath = join(opts.skillDir, "tests", "specification.yaml");
214
- const spec = loadSpec(specPath);
215
- const adapter = opts.adapter ?? getAdapter(results.harness);
216
- // Priced exactly as `adjudicateRun` will perform it — including per-rep
217
- // verdicts, without which the quoted ceiling can be lower than the spend.
218
- const cells = cellsFromResults(column.runDir, results);
219
- // No tie-break judge from the browser: adding a third judge is a judge
220
- // CHOICE, and the UI has nowhere honest to make one. A disagreement here
221
- // stays unresolved and blocks SHIP, which is the safe direction.
222
- const plan = planAdjudication({
223
- cells, scenarios: spec.scenarios, shipBar: spec.ship_bar, critical: spec.critical,
224
- tieBreakAvailable: false,
225
- });
226
- if (body.step !== "run") {
227
- res.writeHead(200, { "content-type": "application/json" });
228
- res.end(JSON.stringify({
229
- ok: true, step: "plan",
230
- triggered: plan.triggered,
231
- maxAdditionalCalls: plan.maxAdditionalCalls,
232
- judge: `${results.judge.provider}:${results.judge.model}`,
233
- detail: plan.decisions.filter((d) => d.triggers.length).map((d) => `${d.id}: ${d.triggers.join(", ")}`),
234
- }));
235
- return;
236
- }
237
- if (!(await adapter.available())) {
238
- res.writeHead(400, { "content-type": "application/json" });
239
- res.end(JSON.stringify({ ok: false, error: `harness \`${results.harness}\` is not on PATH` }));
240
- return;
241
- }
242
- try {
243
- assertJudgeAllowed(results.judge, { source: "the run's recorded judge", allowMetered: envFlag("SKILL_HARNESS_ALLOW_METERED_JUDGE") });
244
- const written = await adjudicateRun({
245
- runDir: column.runDir, spec, adapter, results,
246
- primaryJudge: results.judge,
247
- // Asked again as an independent draw. The judge-variance study measured
248
- // ~2% self-disagreement on identical transcripts, so this is a real
249
- // second opinion rather than a no-op.
250
- secondaryJudge: results.judge,
251
- specDir: dirname(specPath), now: () => new Date().toISOString(),
252
- });
253
- ensureResultsGitignore(join(opts.skillDir, "tests", "results"));
254
- res.writeHead(200, { "content-type": "application/json" });
255
- res.end(JSON.stringify({ ok: true, step: "run", grade: written.effective_grade }));
256
- }
257
- catch (e) {
258
- res.writeHead(400, { "content-type": "application/json" });
259
- res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : String(e) }));
260
- }
261
- return;
262
- }
263
183
  if (req.method === "POST" && url.pathname === "/save") {
264
184
  const body = JSON.parse((await readBody(req)) || "{}");
265
185
  const data = collectReport(opts.skillDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-harness/cli",
3
- "version": "0.16.0",
3
+ "version": "0.18.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",
@@ -10,8 +10,7 @@
10
10
  },
11
11
  "exports": {
12
12
  ".": "./dist/cli.js",
13
- "./serve": "./dist/serve.js",
14
- "./learning": "./dist/learning.js"
13
+ "./serve": "./dist/serve.js"
15
14
  },
16
15
  "keywords": [
17
16
  "agent-skills",
@@ -28,7 +27,6 @@
28
27
  "dist/**/*.js",
29
28
  "dist/**/*.d.ts",
30
29
  "assets",
31
- "docs/*.md",
32
30
  "LICENSE",
33
31
  "README.md"
34
32
  ],
@@ -46,7 +44,7 @@
46
44
  "prepack": "node ../../scripts/release-pack.mjs --guard"
47
45
  },
48
46
  "dependencies": {
49
- "@skill-harness/core": "0.16.0",
50
- "@skill-harness/adapters": "0.16.0"
47
+ "@skill-harness/core": "0.18.0",
48
+ "@skill-harness/adapters": "0.18.0"
51
49
  }
52
50
  }
@@ -1,3 +0,0 @@
1
- import type { Args } from './cli.js';
2
- /** Explicit operator requests, not a scheduler or model route. The inert host cannot invoke this CLI. */
3
- export declare function cmdArchiveLearning(args: Args): void;