@skill-harness/cli 0.1.2 → 0.3.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.
@@ -75,3 +75,55 @@ export function gradeColumn(col, shipBar, critical) {
75
75
 
76
76
  return { passed, total, pct, letter, ship, criticalFails, bFails, suspect };
77
77
  }
78
+
79
+ /**
80
+ * Red-vs-green class for ONE scenario — mirrors `classify` in
81
+ * packages/core/src/lift.ts exactly. Kept in the client because the author
82
+ * flips green verdicts live in this UI: a server-computed class would freeze at
83
+ * page load and could show "gained" beside a cell the author just marked FAIL.
84
+ *
85
+ * `liftCell` supplies the red side (verdict + redSuspect, from the baseline run,
86
+ * which this view never edits); `greenCell` is the live report cell, so its
87
+ * override and suspect state are read through the same effective()/suspect rule
88
+ * the grader uses.
89
+ */
90
+ export function liftClass(liftCell, greenCell) {
91
+ const conclusive = (verdict, suspect) =>
92
+ !suspect && verdict !== "ERROR" && verdict !== "JUDGE-AMBIGUOUS";
93
+
94
+ const redOk = conclusive(liftCell.red, liftCell.redSuspect);
95
+ const greenOk = conclusive(effective(greenCell), !!greenCell.suspect && !greenCell.override);
96
+ if (!redOk || !greenOk) return "inconclusive";
97
+
98
+ const redPass = liftCell.red === "PASS";
99
+ const greenPass = effective(greenCell) === "PASS";
100
+ if (redPass && greenPass) return "kept";
101
+ if (!redPass && greenPass) return "gained";
102
+ if (redPass && !greenPass) return "regressed";
103
+ return "both-fail";
104
+ }
105
+
106
+ /**
107
+ * Aggregate a column's lift over the live cells — mirrors computeLift's counters
108
+ * in packages/core/src/lift.ts. Only scenarios the baseline also covered are
109
+ * counted, matching the server's intersection rule.
110
+ */
111
+ export function liftSummary(col) {
112
+ const out = { gained: 0, regressed: 0, kept: 0, bothFail: 0, inconclusive: 0, compared: 0, redPassed: 0, greenPassed: 0, delta: 0 };
113
+ if (!col.lift) return null;
114
+ for (const id of Object.keys(col.lift.cells)) {
115
+ const greenCell = col.cells[id];
116
+ if (!greenCell) continue;
117
+ const liftCell = col.lift.cells[id];
118
+ const cls = liftClass(liftCell, greenCell);
119
+ out.compared++;
120
+ if (cls === "both-fail") out.bothFail++;
121
+ else out[cls]++;
122
+ if (cls !== "inconclusive") {
123
+ if (liftCell.red === "PASS") out.redPassed++;
124
+ if (effective(greenCell) === "PASS") out.greenPassed++;
125
+ }
126
+ }
127
+ out.delta = out.greenPassed - out.redPassed;
128
+ return out;
129
+ }
@@ -35,6 +35,14 @@
35
35
  .badge { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 11px; font-weight: 700; }
36
36
  .badge.ship { background: var(--pass); color: #fff; }
37
37
  .badge.no { background: var(--fail); color: #fff; }
38
+ /* red-vs-green lift: what the skill itself changed */
39
+ .lift { font-size: 11px; margin-top: 3px; font-weight: 600; }
40
+ .lift.up { color: var(--pass); }
41
+ .lift.down { color: var(--fail); }
42
+ .lift.flat, .lift.none { color: var(--dim); font-weight: 500; }
43
+ .cell .lm { display: block; font-size: 10px; font-weight: 700; }
44
+ .cell .lm.up { color: var(--pass); }
45
+ .cell .lm.down { color: var(--fail); }
38
46
  aside { width: 0; transition: width .15s ease; overflow: hidden; border-left: 1px solid var(--line); background: var(--panel); }
39
47
  aside.open { width: 460px; }
40
48
  .panel { width: 460px; padding: 16px 18px; }
@@ -153,14 +161,43 @@ function render() {
153
161
  const suspectNote = g.suspect > 0 ? ` — ${g.suspect} suspect` : "";
154
162
  gradeHtml = `${g.letter} (${g.pct}%) · ${g.passed}/${g.total}${suspectNote} ${badge}`;
155
163
  }
156
- html += `<th><div>${escapeHtml(col.label)}</div><div class='grade'>${gradeHtml}</div></th>`;
164
+ // Lift answers "does this skill do anything?", which the grade alone cannot:
165
+ // a column can score A because the model never needed the skill. Recomputed
166
+ // from the live cells (see liftSummary) so overrides move it immediately.
167
+ const ls = liftSummary(col);
168
+ let liftHtml = "";
169
+ if (ls && ls.compared > 0) {
170
+ const moved = ls.gained > 0 || ls.regressed > 0;
171
+ const cls = !moved ? "flat" : ls.delta > 0 ? "up" : ls.delta < 0 ? "down" : "flat";
172
+ // All-inconclusive is no measurement, not a zero effect — don't claim "no
173
+ // effect" when nothing was measurable (mirrors liftHeadline in lift.ts).
174
+ const body = ls.compared === ls.inconclusive
175
+ ? `lift — nothing conclusive`
176
+ : moved
177
+ ? `lift ${ls.delta > 0 ? "+" : ""}${ls.delta} · ${ls.gained}↑ ${ls.regressed}↓`
178
+ : `lift 0 · no effect`;
179
+ const inc = ls.inconclusive > 0 ? ` · ${ls.inconclusive} inconclusive` : "";
180
+ const part = col.lift.partial ? " · partial" : "";
181
+ html += `<th><div>${escapeHtml(col.label)}</div><div class='grade'>${gradeHtml}</div>` +
182
+ `<div class='lift ${cls}' title='vs red baseline ${escapeHtml(col.lift.redTimestamp)} — ${ls.kept} passed without the skill too'>${body}${inc}${part}</div></th>`;
183
+ return;
184
+ }
185
+ if (col.mode === "green") {
186
+ // "not measured" is a different claim from "measured no effect" — say which.
187
+ liftHtml = `<div class='lift none' title='run the same scenarios with --mode red to get a baseline'>no red baseline</div>`;
188
+ }
189
+ html += `<th><div>${escapeHtml(col.label)}</div><div class='grade'>${gradeHtml}</div>${liftHtml}</th>`;
157
190
  });
158
191
  html += "</tr></thead><tbody>";
159
192
  for (const scn of DATA.scenarios) {
160
193
  html += `<tr><td class='scn'>${scn.id}${scn.critical ? "<span class='crit' title='critical'>⚠</span>" : ""}<div class='grade'>${escapeHtml(scn.title)}</div></td>`;
161
194
  DATA.columns.forEach((col) => {
162
195
  const cell = col.cells[scn.id];
163
- if (!cell) { html += `<td class='cell empty'>–</td>`; continue; }
196
+ // `return`, not `continue`: this is a forEach callback, so `continue` is a
197
+ // SyntaxError that takes the entire inline script — the whole review UI —
198
+ // down with it. Guarded by report.test.ts's "rendered report is valid
199
+ // JavaScript".
200
+ if (!cell) { html += `<td class='cell empty'>–</td>`; return; }
164
201
  const v = effective(cell);
165
202
  const sel = selected && selected.colIndex === col.index && selected.scenarioId === scn.id ? " sel" : "";
166
203
  // _unsaved wins regardless of override value, so a failed *clear*
@@ -171,7 +208,17 @@ function render() {
171
208
  const misfired = cell.clean != null && cell.clean < cell.reps ? ` · ${cell.reps - cell.clean} misfired` : "";
172
209
  const reps = cell.reps ? `<span class='reps'>${cell.passes}/${cell.clean}${misfired}${cell.flakiness ? ` · flaky ${cell.flakiness.toFixed(2)}` : ""}</span>` : "";
173
210
  const suspectBadge = cell.suspect && !cell.override ? `<span class='ov suspect'>suspect</span>` : "";
174
- html += `<td class='cell ${v}${sel}' data-col='${col.index}' data-id='${scn.id}'>${v}${ov}${suspectBadge}${reps}</td>`;
211
+ // Per-cell lift marker: only the two classes that mean the skill changed
212
+ // the outcome. kept/both-fail/inconclusive are left unmarked — marking
213
+ // every cell would bury the signal this column exists to show.
214
+ const lc = col.lift && col.lift.cells[scn.id] ? liftClass(col.lift.cells[scn.id], cell) : null;
215
+ const liftMark =
216
+ lc === "gained"
217
+ ? `<span class='lm up' title='red baseline ${col.lift.cells[scn.id].red} → PASS with the skill'>↑ skill</span>`
218
+ : lc === "regressed"
219
+ ? `<span class='lm down' title='red baseline PASS → ${effective(cell)} with the skill'>↓ skill</span>`
220
+ : "";
221
+ html += `<td class='cell ${v}${sel}' data-col='${col.index}' data-id='${scn.id}'>${v}${ov}${suspectBadge}${liftMark}${reps}</td>`;
175
222
  });
176
223
  html += "</tr>";
177
224
  }
package/dist/cli.d.ts CHANGED
@@ -12,6 +12,8 @@ export declare function parseRunTuning(args: Args): {
12
12
  passThreshold: number;
13
13
  };
14
14
  export declare function cmdGrade(args: Args, adapterOverride?: HarnessAdapter): Promise<void>;
15
+ export declare function cmdInit(args: Args): Promise<void>;
16
+ export declare function cmdSuggest(args: Args, adapterOverride?: HarnessAdapter): Promise<void>;
15
17
  /** Exit-code contract: 0 = clean (no findings), 1 = >=1 finding, or a resolution error (unknown skill/root, no skills with a spec). */
16
18
  export declare function cmdLint(args: Args): Promise<void>;
17
19
  export declare function main(argv: string[]): Promise<void>;
package/dist/cli.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, existsSync, appendFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
2
+ import { readFileSync, existsSync, appendFileSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { tmpdir } from "node:os";
4
5
  import yaml from "js-yaml";
5
- import { discover, resolveSkill, loadSpec, parseSpec, parseModelRef, runSkillModel, formatScorecard, readResults, regradeRun, lintSkill, } from "@skill-harness/core";
6
+ import { discover, resolveSkill, loadSpec, parseSpec, parseModelRef, runSkillModel, formatScorecard, readResults, regradeRun, lintSkill, renderTemplateSpec, isTemplateSpec, renderDraftSpec, buildSuggestPrompt, parseSuggestDraft, rescoreRun, specPathForRunDir, collectLift, } from "@skill-harness/core";
6
7
  import { getAdapter } from "@skill-harness/adapters";
7
8
  import { serveReview } from "./serve.js";
8
9
  const DEFAULT_MODEL = "fireworks:accounts/fireworks/models/deepseek-v4-pro";
9
10
  const DEFAULT_JUDGE = "anthropic:claude-opus-4-8";
11
+ const DEFAULT_SUGGEST_MODEL = "claude-code:claude-opus-4-8";
10
12
  const REPEATABLE = new Set(["model", "turn", "check"]);
11
13
  function parseArgs(argv) {
12
14
  const _ = [];
@@ -120,6 +122,8 @@ async function cmdRun(args) {
120
122
  const label = flagStr(args, "label") || null;
121
123
  const parallel = Math.max(1, Number(flagStr(args, "parallel", "1")) || 1);
122
124
  const { reps, passThreshold } = parseRunTuning(args);
125
+ const onlyRaw = flagStr(args, "only");
126
+ const only = onlyRaw ? onlyRaw.split(",").map((x) => x.trim()).filter(Boolean) : undefined;
123
127
  const modelTokens = resolveModels(args);
124
128
  const skills = target === "all"
125
129
  ? discover(root).filter((s) => s.hasSpec)
@@ -148,18 +152,26 @@ async function cmdRun(args) {
148
152
  concurrency: parallel,
149
153
  reps,
150
154
  passThreshold,
155
+ only,
151
156
  onProgress: (m) => console.log(m),
152
157
  });
153
158
  summaries.push(summary);
154
- console.log("\n" + formatScorecard(summary) + "\n");
159
+ // Lift is derived from what's on disk, so it picks up a red baseline from
160
+ // any earlier run — the tag dir (<harness>-<modelslug>) is the join key.
161
+ const tag = basename(dirname(summary.runDir));
162
+ const lift = collectLift(skill.dir).find((l) => l.tag === tag);
163
+ console.log("\n" + formatScorecard(summary, lift) + "\n");
155
164
  }
156
165
  }
157
166
  console.log(`\nReview interactively: skill-harness review ${skills[0]?.name ?? "<skill>"} --skills ${root}`);
158
167
  }
159
168
  export async function cmdGrade(args, adapterOverride) {
160
169
  const runDir = args._[0];
161
- if (!runDir || !existsSync(runDir))
162
- throw new Error("usage: skill-harness grade <run-dir> [--judge prov:model]");
170
+ if (!runDir)
171
+ throw new Error("usage: skill-harness grade <run-dir> [--judge prov:model] [--suspect-only]");
172
+ if (!existsSync(runDir)) {
173
+ throw new Error(`run dir not found: ${resolve(runDir)} (relative paths resolve against the cwd)`);
174
+ }
163
175
  // spec lives at <runDir>/../../../specification.yaml (results/<tag>/<ts> -> tests/)
164
176
  const testsDir = dirname(dirname(dirname(runDir)));
165
177
  const specPath = join(testsDir, "specification.yaml");
@@ -171,13 +183,44 @@ export async function cmdGrade(args, adapterOverride) {
171
183
  const judgeFlag = flagStr(args, "judge");
172
184
  const judge = judgeFlag ? parseModelRef(judgeFlag) : (prev?.judge ?? parseModelRef(DEFAULT_JUDGE));
173
185
  const adapter = adapterOverride ?? getAdapter(prev?.harness ?? "pi");
174
- const results = await regradeRun({ runDir, spec, adapter, judge, specDir: testsDir, now: nowIso });
186
+ const results = await regradeRun({
187
+ runDir, spec, adapter, judge, specDir: testsDir, now: nowIso,
188
+ onlySuspect: args.flags["suspect-only"] === true || args.flags["suspect-only"] === "true",
189
+ });
175
190
  for (const s of results.scenarios) {
176
191
  console.log(` ${s.id} → ${s.judge_verdict}: ${s.judge_reason}`);
177
192
  }
178
193
  const g = results.effective_grade;
179
194
  console.log(`\n re-graded with ${judge.provider}:${judge.model} → ${g.letter} (${g.pct}%) ${g.ship ? "SHIP" : "NOT READY"}`);
180
195
  }
196
+ /**
197
+ * Re-score saved runs against the current spec's thresholds — no model or judge calls.
198
+ * Reps are the measurement; thresholds are policy. When policy changes, recompute rather
199
+ * than reconcile two numbers in prose.
200
+ */
201
+ async function cmdRescore(args) {
202
+ const runDirs = args._;
203
+ if (runDirs.length === 0)
204
+ throw new Error("usage: skill-harness rescore <run-dir> [<run-dir> ...]");
205
+ let moved = 0;
206
+ for (const raw of runDirs) {
207
+ const runDir = resolve(raw);
208
+ if (!existsSync(runDir))
209
+ throw new Error(`run dir not found: ${runDir} (relative paths resolve against the cwd)`);
210
+ const spec = loadSpec(specPathForRunDir(runDir));
211
+ const { results, changes } = rescoreRun({ runDir, spec, now: nowIso });
212
+ const g = results.effective_grade;
213
+ console.log(`\n${results.skill} · ${results.model}`);
214
+ for (const c of changes) {
215
+ console.log(` ${c.id}: ${c.from} → ${c.to} (${c.passes}/${c.clean} @ threshold ${c.toThreshold})`);
216
+ }
217
+ if (changes.length === 0)
218
+ console.log(" (no verdict changed)");
219
+ moved += changes.length;
220
+ console.log(` → ${g.letter} (${g.pct}%) ${g.passed}/${g.total} ${g.ship ? "SHIP" : "NOT READY"}`);
221
+ }
222
+ console.log(`\n${runDirs.length} run(s) re-scored, ${moved} verdict(s) moved.`);
223
+ }
181
224
  async function cmdReview(args) {
182
225
  const root = flagStr(args, "skills", process.cwd());
183
226
  const target = args._[0];
@@ -222,6 +265,95 @@ async function cmdAddTest(args) {
222
265
  appendFileSync(skill.specPath, block, "utf8");
223
266
  console.log(`added scenario ${id} to ${skill.specPath}`);
224
267
  }
268
+ /** Write a spec to disk, creating its tests/ dir. The single choke point for spec
269
+ * writes (init/suggest) so a future atomic-write/backup/audit change lands in one place. */
270
+ function writeSpecFile(specPath, text) {
271
+ mkdirSync(dirname(specPath), { recursive: true });
272
+ writeFileSync(specPath, text, "utf8");
273
+ }
274
+ export async function cmdInit(args) {
275
+ const root = flagStr(args, "skills", process.cwd());
276
+ const target = args._[0];
277
+ if (!target)
278
+ throw new Error("usage: skill-harness init <skill> --skills <root> [--force]");
279
+ const skill = resolveSkill(root, target);
280
+ const force = flagStr(args, "force") !== undefined;
281
+ if (skill.hasSpec && !force) {
282
+ throw new Error(`${skill.specPath} exists — edit it, or pass --force to overwrite`);
283
+ }
284
+ const text = renderTemplateSpec(skill.name);
285
+ parseSpec(text, skill.specPath); // guard: the template must always be valid
286
+ writeSpecFile(skill.specPath, text);
287
+ console.log(`wrote template ${skill.specPath} — fill it in, or run \`skill-harness suggest ${skill.name}\` to LLM-draft it.`);
288
+ }
289
+ export async function cmdSuggest(args, adapterOverride) {
290
+ const root = flagStr(args, "skills", process.cwd());
291
+ const target = args._[0];
292
+ if (!target)
293
+ throw new Error("usage: skill-harness suggest <skill> --skills <root> [--model prov:model] [--force]");
294
+ // resolveSkill throws a SKILL.md-specific error when the directory exists but
295
+ // lacks one, so we don't reimplement that check here; a resolved skill always
296
+ // has a SKILL.md at skill.dir.
297
+ const skill = resolveSkill(root, target);
298
+ const skillMd = readFileSync(join(skill.dir, "SKILL.md"), "utf8");
299
+ // Overwrite without --force only when the target is absent or an *unedited*
300
+ // template. A file that still carries the sentinel but no longer matches the
301
+ // pristine template has been hand-edited — refuse it so we never clobber work.
302
+ const force = flagStr(args, "force") !== undefined;
303
+ if (skill.hasSpec && !force) {
304
+ const existing = readFileSync(skill.specPath, "utf8");
305
+ if (existing !== renderTemplateSpec(skill.name)) {
306
+ const hint = isTemplateSpec(existing)
307
+ ? "looks like an edited template — pass --force to overwrite (or delete your edits)"
308
+ : "already has real content — pass --force to overwrite";
309
+ throw new Error(`${skill.specPath} ${hint}`);
310
+ }
311
+ }
312
+ const model = parseModelRef(flagStr(args, "model", DEFAULT_SUGGEST_MODEL));
313
+ const adapter = adapterOverride ?? getAdapter("pi");
314
+ const cwd = mkdtempSync(join(tmpdir(), "sh-suggest-cwd-"));
315
+ try {
316
+ const basePrompt = buildSuggestPrompt(skill.name, skillMd);
317
+ let text = null;
318
+ let count = 0;
319
+ let lastErr = "";
320
+ for (let attempt = 0; attempt < 2 && text === null; attempt++) {
321
+ const prompt = attempt === 0
322
+ ? basePrompt
323
+ : `${basePrompt}\n\nYour previous reply was rejected: ${lastErr}. Return corrected JSON only.`;
324
+ const raw = await adapter.judge({ model, prompt, cwd });
325
+ // A `[judge error` prefix is a hard adapter failure (auth, exec, credits) —
326
+ // retrying won't help, so fail fast with a model hint. An empty reply is
327
+ // treated as a transient miss and gets the same retry as a bad-JSON reply.
328
+ if (raw.startsWith("[judge error")) {
329
+ throw new Error(`model ${model.provider}:${model.model} failed — ${raw.trim()} (try --model fireworks:...)`);
330
+ }
331
+ if (!raw.trim()) {
332
+ lastErr = "model produced no output";
333
+ continue;
334
+ }
335
+ try {
336
+ const draft = parseSuggestDraft(raw);
337
+ const candidate = renderDraftSpec(skill.name, draft);
338
+ parseSpec(candidate, skill.specPath); // validate before writing
339
+ text = candidate;
340
+ count = draft.scenarios.length;
341
+ }
342
+ catch (e) {
343
+ lastErr = e instanceof Error ? e.message : String(e);
344
+ }
345
+ }
346
+ if (text === null) {
347
+ throw new Error(`could not get a valid spec from ${model.provider}:${model.model} after 2 attempts (${lastErr}) — try \`skill-harness init ${skill.name}\` for a manual template`);
348
+ }
349
+ writeSpecFile(skill.specPath, text);
350
+ console.log(`drafted ${count} scenario(s) → ${skill.specPath}`);
351
+ console.log(`review it (especially the proposed critical set), then \`skill-harness run ${skill.name} --skills ${root}\``);
352
+ }
353
+ finally {
354
+ rmSync(cwd, { recursive: true, force: true });
355
+ }
356
+ }
225
357
  /** Exit-code contract: 0 = clean (no findings), 1 = >=1 finding, or a resolution error (unknown skill/root, no skills with a spec). */
226
358
  export async function cmdLint(args) {
227
359
  const root = flagStr(args, "skills", process.cwd());
@@ -269,11 +401,14 @@ export async function cmdLint(args) {
269
401
  // ---------------------------------------------------------------- dispatch
270
402
  const HELP = `skill-harness — test/optimize loop for agent skills (pi harness)
271
403
 
272
- run <skill|all> --skills <root> [--model prov:model ...] [--models file]
404
+ run <skill|all> --skills <root> [--model prov:model ...] [--models file] [--only A1,D2]
273
405
  [--mode red|green|force] [--judge prov:model] [--harness pi] [--label name] [--parallel N] [--reps N] [--pass-threshold T]
274
- grade <run-dir> [--judge prov:model] re-grade saved transcripts (neutral judge)
406
+ grade <run-dir> [--judge prov:model] [--suspect-only] re-grade saved transcripts (neutral judge)
407
+ rescore <run-dir>... re-score saved reps vs current spec thresholds (free)
275
408
  review <skill> --skills <root> [--port N] serve the interactive review UI
276
409
  add-test <skill> --skills <root> --id ID --title T --turn ... --check ... [--critical] [--mode seeded --fixture path]
410
+ init <skill> --skills <root> [--force] scaffold a commented template spec (free, offline)
411
+ suggest <skill> --skills <root> [--model prov:model] [--force] LLM-draft a spec from SKILL.md (spends tokens)
277
412
  list --skills <root> discovered skills + spec status
278
413
  lint <skill|all> --skills <root> validate specs/fixtures + results-consistency (CI gate; exits non-zero on findings)
279
414
 
@@ -284,8 +419,11 @@ export async function main(argv) {
284
419
  switch (cmd) {
285
420
  case "run": return cmdRun(args);
286
421
  case "grade": return cmdGrade(args);
422
+ case "rescore": return cmdRescore(args);
287
423
  case "review": return cmdReview(args);
288
424
  case "add-test": return cmdAddTest(args);
425
+ case "init": return cmdInit(args);
426
+ case "suggest": return cmdSuggest(args);
289
427
  case "list": return cmdList(args);
290
428
  case "lint": return cmdLint(args);
291
429
  case undefined:
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, findJudgeRawFiles, effectiveThreshold, } from "@skill-harness/core";
6
+ import { collectReport, renderReport, collectTrends, readResults, writeResults, applyOverride, preserveTranscript, findTranscriptFiles, ensureResultsGitignore, appendJournal, loadSpec, regradeScenario, findJudgeRawFiles, effectiveThreshold, 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/. */
@@ -202,7 +202,7 @@ export async function serveReview(opts) {
202
202
  console.log(` → ${link}`);
203
203
  console.log(` flip verdicts + add notes in the browser; saves persist to results.yaml.`);
204
204
  console.log(` Ctrl-C to stop.\n`);
205
- if (opts.open !== false && !process.env.SKILL_CHECK_NO_OPEN)
205
+ if (opts.open !== false && !envFlag("NO_OPEN"))
206
206
  tryOpen(link);
207
207
  return { port: port, close: () => server.close() };
208
208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-harness/cli",
3
- "version": "0.1.2",
3
+ "version": "0.3.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",
@@ -34,7 +34,7 @@
34
34
  "prepack": "rm -rf ./assets && cp -r ../../assets ./assets && cp ../../LICENSE ./LICENSE"
35
35
  },
36
36
  "dependencies": {
37
- "@skill-harness/core": "0.1.2",
38
- "@skill-harness/adapters": "0.1.2"
37
+ "@skill-harness/core": "0.3.0",
38
+ "@skill-harness/adapters": "0.3.0"
39
39
  }
40
40
  }