@plune-ai/cli 0.17.1 → 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
@@ -124,7 +124,7 @@ the last resort, used only when `PLUNE_TOKEN` is set nowhere else. Nothing write
124
124
  | `plune run start` | Open a platform run — or join the one already carrying `--key` — and print its id. Flags: `--key <externalKey>` (generated when absent), `--json` for one machine-readable line. |
125
125
  | `plune run finish <id>` | Close a platform run. This is what the reporter tells you to do for a run it had to leave open. Flags: `--terminate` to record it as cut short, `--reason <text>`. |
126
126
  | `plune run exec -- <command>` | Open a run, run the command inside it, close the run — and exit with whatever the command returned. Sets `PLUNE_SHARED_RUN` for you, so anything reporting inside joins that run. Flags: `--key <externalKey>`. |
127
- | `plune run delete <id>` | Delete a run and everything it produced — its results and the review-queue entries it raised. Approved test cases and the audit log stay. **Recoverable for six months** (ask Plune to put it back), then gone for good. No prompt: it is your data, and this command belongs in scripts. |
127
+ | `plune run delete <id>` | Delete a run and everything it produced — its results and the review-queue entries it raised. Approved test cases and the audit log stay. **Recoverable for 30 days** (an owner of the project can put it back from Settings → Bin), then gone for good. No prompt: it is your data, and this command belongs in scripts. |
128
128
  | `plune run report` | Replay `.plune/pending-results.jsonl` — send what the reporter could not. The lines of one reporter run go back as one run, with the commit and branch the reporter recorded. Once every result has gone the file is renamed to `pending-results.<time>.sent.jsonl`; if anything did not go, it stays as it was. Flags: `--file <path>`. |
129
129
  | `plune ingest [dir]` | Record a [Cairn](https://github.com/plune-ai/cairn) run in Plune. Omit `[dir]` for the newest run under `./runs`, or name the directory holding `report.json`. Generated cases arrive as **review proposals** — nothing is created until a person approves it. |
130
130
  | `plune pull [file]` | Write the project's test cases as **one Markdown document** — Testomat's classical format, plus Plune's own columns — to `plune/cases.md` (or `[file]`). Refuses to overwrite a file git sees as modified; `--force` overrides, `--suite <id>` takes one suite or folder and what is under it. |
@@ -291,8 +291,8 @@ Two suites, or a sharded matrix, report into a single run when every job shares
291
291
  it is not the last one:
292
292
 
293
293
  ```bash
294
- PLUNE_SHARED_RUN=1 plune run import ./junit.xml --key "$GITHUB_RUN_ID"
295
- PLUNE_SHARED_RUN=1 plune run import ./app/junit.xml --key "$GITHUB_RUN_ID"
294
+ PLUNE_SHARED_RUN=1 plune run import ./junit.xml --key "$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
295
+ PLUNE_SHARED_RUN=1 plune run import ./app/junit.xml --key "$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
296
296
  plune run finish "$RUN_ID" # once, when they are all done
297
297
  ```
298
298
 
package/dist/cli.cjs CHANGED
@@ -1621,6 +1621,76 @@ var init_concurrency = __esm({
1621
1621
  }
1622
1622
  });
1623
1623
 
1624
+ // src/types/test-case-schema.ts
1625
+ var import_zod2, TEST_CASE_SCHEMA_VERSION, externalKeySchema, manualStepSchema, assertionResultRecordSchema, manualCaseSchema, automatedCaseSchema, evalCaseSchema, testCaseBaseSchema, testCaseSchema;
1626
+ var init_test_case_schema = __esm({
1627
+ "src/types/test-case-schema.ts"() {
1628
+ "use strict";
1629
+ init_cjs_shims();
1630
+ import_zod2 = require("zod");
1631
+ TEST_CASE_SCHEMA_VERSION = 2;
1632
+ externalKeySchema = import_zod2.z.object({
1633
+ kind: import_zod2.z.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
1634
+ value: import_zod2.z.string().min(1)
1635
+ });
1636
+ manualStepSchema = import_zod2.z.object({
1637
+ action: import_zod2.z.string().min(1),
1638
+ expected: import_zod2.z.string().min(1).optional()
1639
+ });
1640
+ assertionResultRecordSchema = import_zod2.z.object({
1641
+ type: import_zod2.z.string().min(1),
1642
+ passed: import_zod2.z.boolean(),
1643
+ score: import_zod2.z.number().optional(),
1644
+ reason: import_zod2.z.string().optional()
1645
+ });
1646
+ manualCaseSchema = import_zod2.z.object({
1647
+ type: import_zod2.z.literal("manual"),
1648
+ steps: import_zod2.z.array(manualStepSchema).min(1),
1649
+ expected: import_zod2.z.string().min(1).optional()
1650
+ });
1651
+ automatedCaseSchema = import_zod2.z.object({
1652
+ type: import_zod2.z.literal("automated"),
1653
+ specRef: import_zod2.z.string().min(1)
1654
+ });
1655
+ evalCaseSchema = import_zod2.z.object({
1656
+ type: import_zod2.z.literal("eval"),
1657
+ prompt: import_zod2.z.string().min(1),
1658
+ goldenSetId: import_zod2.z.string().min(1).optional(),
1659
+ assertions: import_zod2.z.array(assertionResultRecordSchema).min(1)
1660
+ });
1661
+ testCaseBaseSchema = import_zod2.z.object({
1662
+ schemaVersion: import_zod2.z.literal(TEST_CASE_SCHEMA_VERSION),
1663
+ id: import_zod2.z.string().min(1),
1664
+ externalKeys: import_zod2.z.array(externalKeySchema).optional(),
1665
+ /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1666
+ is_flaky: import_zod2.z.boolean().optional(),
1667
+ /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1668
+ is_muted: import_zod2.z.boolean().optional()
1669
+ });
1670
+ testCaseSchema = import_zod2.z.discriminatedUnion("type", [
1671
+ testCaseBaseSchema.merge(manualCaseSchema),
1672
+ testCaseBaseSchema.merge(automatedCaseSchema),
1673
+ testCaseBaseSchema.merge(evalCaseSchema)
1674
+ ]);
1675
+ }
1676
+ });
1677
+
1678
+ // src/types/results.ts
1679
+ function rowStatus(row) {
1680
+ if (row.inconclusive !== void 0) return row.inconclusive;
1681
+ if (row.error !== void 0) return "errored";
1682
+ return row.assertions.every((a) => a.passed) ? "passed" : "failed";
1683
+ }
1684
+ var ROW_INCONCLUSIVE;
1685
+ var init_results = __esm({
1686
+ "src/types/results.ts"() {
1687
+ "use strict";
1688
+ init_cjs_shims();
1689
+ init_test_case_schema();
1690
+ ROW_INCONCLUSIVE = ["blocked", "skipped"];
1691
+ }
1692
+ });
1693
+
1624
1694
  // src/orchestrator/run.ts
1625
1695
  function resolvePrompt(template, vars) {
1626
1696
  return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => {
@@ -1754,10 +1824,25 @@ function resolveTemplate(ev, baseDir) {
1754
1824
  }
1755
1825
  throw new RunConfigError(`Eval "${ev.id}" has neither prompt nor prompt_file`);
1756
1826
  }
1757
- function classify(row) {
1758
- if (row.error !== void 0) return "errored";
1759
- if (row.assertions.some((a) => !a.passed)) return "failed";
1760
- return "passed";
1827
+ function summarize(evals, durationMs) {
1828
+ const summary = {
1829
+ total: 0,
1830
+ passed: 0,
1831
+ failed: 0,
1832
+ errored: 0,
1833
+ blocked: 0,
1834
+ skipped: 0,
1835
+ cost_usd: 0,
1836
+ duration_ms: durationMs
1837
+ };
1838
+ for (const ev of evals) {
1839
+ for (const row of ev.rows) {
1840
+ summary.total += 1;
1841
+ summary[rowStatus(row)] += 1;
1842
+ summary.cost_usd += row.usage?.cost_usd ?? 0;
1843
+ }
1844
+ }
1845
+ return summary;
1761
1846
  }
1762
1847
  async function runOrchestration(config, options, deps) {
1763
1848
  const started = deps.now();
@@ -1792,26 +1877,12 @@ async function runOrchestration(config, options, deps) {
1792
1877
  noCache
1793
1878
  })
1794
1879
  );
1795
- const passed = rowResults.every((r) => classify(r) === "passed");
1880
+ const passed = rowResults.every((r) => rowStatus(r) === "passed");
1796
1881
  evalResults.push({ id: ev.id, tags: ev.tags ?? [], rows: rowResults, passed });
1797
1882
  if (options.bail === true && !passed) break;
1798
1883
  }
1799
1884
  const finished = deps.now();
1800
- const summary = {
1801
- total: 0,
1802
- passed: 0,
1803
- failed: 0,
1804
- errored: 0,
1805
- cost_usd: 0,
1806
- duration_ms: finished - started
1807
- };
1808
- for (const ev of evalResults) {
1809
- for (const row of ev.rows) {
1810
- summary.total += 1;
1811
- summary[classify(row)] += 1;
1812
- summary.cost_usd += row.usage?.cost_usd ?? 0;
1813
- }
1814
- }
1885
+ const summary = summarize(evalResults, finished - started);
1815
1886
  return {
1816
1887
  schemaVersion: 1,
1817
1888
  plune_version: PLUNE_VERSION,
@@ -1834,6 +1905,7 @@ var init_run = __esm({
1834
1905
  init_judge();
1835
1906
  init_concurrency();
1836
1907
  init_errors2();
1908
+ init_results();
1837
1909
  DEFAULT_MAX_TOKENS2 = 1024;
1838
1910
  PLUNE_VERSION = "0.1.0";
1839
1911
  RunConfigError = class extends Error {
@@ -2027,8 +2099,10 @@ function renderConsole(result, opts = {}) {
2027
2099
  const passedTxt = s.green(`${summary.passed} passed`);
2028
2100
  const failedTxt = summary.failed > 0 ? s.red(`${summary.failed} failed`) : `${summary.failed} failed`;
2029
2101
  const erroredTxt = summary.errored > 0 ? s.yellow(`${summary.errored} errored`) : `${summary.errored} errored`;
2102
+ const blockedTxt = (summary.blocked ?? 0) > 0 ? ` \xB7 ${s.yellow(`${summary.blocked} blocked`)}` : "";
2103
+ const skippedTxt = (summary.skipped ?? 0) > 0 ? ` \xB7 ${s.dim(`${summary.skipped} skipped`)}` : "";
2030
2104
  lines.push(
2031
- `${passedTxt} \xB7 ${failedTxt} \xB7 ${erroredTxt} \xB7 ${summary.total} total \xB7 $${summary.cost_usd.toFixed(4)} \xB7 ${summary.duration_ms}ms`
2105
+ `${passedTxt} \xB7 ${failedTxt} \xB7 ${erroredTxt}${blockedTxt}${skippedTxt} \xB7 ${summary.total} total \xB7 $${summary.cost_usd.toFixed(4)} \xB7 ${summary.duration_ms}ms`
2032
2106
  );
2033
2107
  lines.push("");
2034
2108
  for (const ev of result.evals) {
@@ -2037,17 +2111,21 @@ function renderConsole(result, opts = {}) {
2037
2111
  lines.push(`${mark} ${s.bold(ev.id)}${tags}`);
2038
2112
  let passedCount = 0;
2039
2113
  for (const row of ev.rows) {
2114
+ const status = rowStatus(row);
2115
+ if (status === "passed") {
2116
+ passedCount += 1;
2117
+ continue;
2118
+ }
2119
+ lines.push(` ${s.dim("vars:")} ${JSON.stringify(row.vars)}`);
2120
+ if (status === "blocked" || status === "skipped") {
2121
+ lines.push(` ${s.yellow(status)} (not judged)`);
2122
+ continue;
2123
+ }
2040
2124
  if (row.error !== void 0) {
2041
- lines.push(` ${s.dim("vars:")} ${JSON.stringify(row.vars)}`);
2042
2125
  lines.push(` ${s.red("error:")} ${row.error.message}`);
2043
2126
  continue;
2044
2127
  }
2045
2128
  const failed = row.assertions.filter((a) => !a.passed);
2046
- if (failed.length === 0) {
2047
- passedCount += 1;
2048
- continue;
2049
- }
2050
- lines.push(` ${s.dim("vars:")} ${JSON.stringify(row.vars)}`);
2051
2129
  if (row.output !== null) {
2052
2130
  lines.push(` ${s.dim("output:")} ${truncate(row.output, maxOut)}`);
2053
2131
  }
@@ -2068,6 +2146,7 @@ var init_console = __esm({
2068
2146
  "src/reporters/console.ts"() {
2069
2147
  "use strict";
2070
2148
  init_cjs_shims();
2149
+ init_results();
2071
2150
  init_style();
2072
2151
  init_helpers();
2073
2152
  DEFAULT_MAX_OUTPUT = 500;
@@ -2086,11 +2165,6 @@ var init_json = __esm({
2086
2165
  });
2087
2166
 
2088
2167
  // src/reporters/markdown.ts
2089
- function classify2(row) {
2090
- if (row.error !== void 0) return "errored";
2091
- if (row.assertions.some((a) => !a.passed)) return "failed";
2092
- return "passed";
2093
- }
2094
2168
  function fence(content) {
2095
2169
  const runs = content.match(new RegExp(TICK + "+", "g"));
2096
2170
  const longest = runs ? runs.reduce((m, r) => Math.max(m, r.length), 0) : 0;
@@ -2111,18 +2185,22 @@ function renderMarkdown(result, opts = {}) {
2111
2185
  out.push(`| Passed | ${summary.passed} |`);
2112
2186
  out.push(`| Failed | ${summary.failed} |`);
2113
2187
  out.push(`| Errored | ${summary.errored} |`);
2188
+ if ((summary.blocked ?? 0) > 0) out.push(`| Blocked | ${summary.blocked} |`);
2189
+ if ((summary.skipped ?? 0) > 0) out.push(`| Skipped | ${summary.skipped} |`);
2114
2190
  out.push(`| Cost (USD) | ${summary.cost_usd.toFixed(4)} |`);
2115
2191
  out.push(`| Duration (ms) | ${summary.duration_ms} |`);
2116
2192
  out.push("");
2117
2193
  const failures = [];
2194
+ let allPassed = true;
2118
2195
  for (const ev of result.evals) {
2119
2196
  for (const row of ev.rows) {
2120
- const state = classify2(row);
2121
- if (state !== "passed") failures.push({ id: ev.id, row, state });
2197
+ const state = rowStatus(row);
2198
+ if (state !== "passed") allPassed = false;
2199
+ if (state === "failed" || state === "errored") failures.push({ id: ev.id, row, state });
2122
2200
  }
2123
2201
  }
2124
2202
  if (failures.length === 0) {
2125
- out.push("All evals passed.");
2203
+ if (allPassed) out.push("All evals passed.");
2126
2204
  return out.join("\n");
2127
2205
  }
2128
2206
  out.push("## Failures");
@@ -2153,6 +2231,7 @@ var init_markdown = __esm({
2153
2231
  "src/reporters/markdown.ts"() {
2154
2232
  "use strict";
2155
2233
  init_cjs_shims();
2234
+ init_results();
2156
2235
  init_helpers();
2157
2236
  DEFAULT_MAX_OUTPUT2 = 500;
2158
2237
  TICK = String.fromCharCode(96);
@@ -2298,7 +2377,7 @@ function retryAfterMs2(res) {
2298
2377
  const seconds = Number(raw.trim());
2299
2378
  return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : null;
2300
2379
  }
2301
- function classify3(status) {
2380
+ function classify(status) {
2302
2381
  if (status === 401 || status === 403) return "auth";
2303
2382
  if (status === 409) return "conflict";
2304
2383
  if (status === 400) return "invalid";
@@ -2341,13 +2420,13 @@ function createClient(opts) {
2341
2420
  }
2342
2421
  }
2343
2422
  const said = await detailOf(res);
2344
- const kind = classify3(res.status);
2423
+ const kind = classify(res.status);
2345
2424
  last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {} };
2346
2425
  if (kind !== "unavailable") break;
2347
2426
  if (res.status === 429 && retryAfterMs2(res) === null) {
2348
2427
  last.kind = "invalid";
2349
2428
  last.detail = "quota exceeded (no Retry-After)";
2350
- if (said.detail) last.detail += ` \u2014 ${said.detail}`;
2429
+ if (said.detail) last.detail += ` \u2014 ${scrub(said.detail)}`;
2351
2430
  break;
2352
2431
  }
2353
2432
  if (attempt < RETRY_DELAYS_MS.length) {
@@ -3404,7 +3483,7 @@ async function handleRunDelete(id, options = {}) {
3404
3483
  );
3405
3484
  }
3406
3485
  write(`Run ${id} deleted, with its results and the review-queue entries it produced.`);
3407
- write("Recoverable for 30 days \u2014 ask Plune to put it back.");
3486
+ write("Recoverable for 30 days \u2014 an owner of the project can put it back from Settings \u2192 Bin.");
3408
3487
  }
3409
3488
  async function handleRunExec(options) {
3410
3489
  const [command, ...args] = options.argv;
@@ -4544,18 +4623,13 @@ var init_report = __esm({
4544
4623
  });
4545
4624
 
4546
4625
  // src/diff/diff.ts
4547
- function rowStatus(r) {
4548
- if (r.error !== void 0) return "errored";
4549
- if (r.assertions.some((a) => !a.passed)) return "failed";
4550
- return "passed";
4551
- }
4552
4626
  function evalStatus(e) {
4553
4627
  const statuses = e.rows.map(rowStatus);
4554
4628
  if (statuses.includes("failed")) return "failed";
4555
4629
  if (statuses.includes("errored")) return "errored";
4556
4630
  return "passed";
4557
4631
  }
4558
- function classify4(baseline, current) {
4632
+ function classify2(baseline, current) {
4559
4633
  if (current === "errored") return "errored";
4560
4634
  if (baseline === "absent") return current === "passed" ? "new-pass" : "new-fail";
4561
4635
  if (current === "absent") return "removed";
@@ -4569,7 +4643,7 @@ function diffRuns(baseline, current) {
4569
4643
  const evals = ids.map((id) => {
4570
4644
  const baselineP = baseMap.get(id) ?? "absent";
4571
4645
  const currentP = curMap.get(id) ?? "absent";
4572
- return { id, status: classify4(baselineP, currentP), baseline: baselineP, current: currentP };
4646
+ return { id, status: classify2(baselineP, currentP), baseline: baselineP, current: currentP };
4573
4647
  });
4574
4648
  const count2 = (s) => evals.filter((e) => e.status === s).length;
4575
4649
  const summary = {
@@ -4589,6 +4663,7 @@ var init_diff = __esm({
4589
4663
  "src/diff/diff.ts"() {
4590
4664
  "use strict";
4591
4665
  init_cjs_shims();
4666
+ init_results();
4592
4667
  }
4593
4668
  });
4594
4669
 
@@ -5007,71 +5082,6 @@ var init_logout = __esm({
5007
5082
  }
5008
5083
  });
5009
5084
 
5010
- // src/types/test-case-schema.ts
5011
- var import_zod2, TEST_CASE_SCHEMA_VERSION, externalKeySchema, manualStepSchema, assertionResultRecordSchema, manualCaseSchema, automatedCaseSchema, evalCaseSchema, testCaseBaseSchema, testCaseSchema;
5012
- var init_test_case_schema = __esm({
5013
- "src/types/test-case-schema.ts"() {
5014
- "use strict";
5015
- init_cjs_shims();
5016
- import_zod2 = require("zod");
5017
- TEST_CASE_SCHEMA_VERSION = 2;
5018
- externalKeySchema = import_zod2.z.object({
5019
- kind: import_zod2.z.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
5020
- value: import_zod2.z.string().min(1)
5021
- });
5022
- manualStepSchema = import_zod2.z.object({
5023
- action: import_zod2.z.string().min(1),
5024
- expected: import_zod2.z.string().min(1).optional()
5025
- });
5026
- assertionResultRecordSchema = import_zod2.z.object({
5027
- type: import_zod2.z.string().min(1),
5028
- passed: import_zod2.z.boolean(),
5029
- score: import_zod2.z.number().optional(),
5030
- reason: import_zod2.z.string().optional()
5031
- });
5032
- manualCaseSchema = import_zod2.z.object({
5033
- type: import_zod2.z.literal("manual"),
5034
- steps: import_zod2.z.array(manualStepSchema).min(1),
5035
- expected: import_zod2.z.string().min(1).optional()
5036
- });
5037
- automatedCaseSchema = import_zod2.z.object({
5038
- type: import_zod2.z.literal("automated"),
5039
- specRef: import_zod2.z.string().min(1)
5040
- });
5041
- evalCaseSchema = import_zod2.z.object({
5042
- type: import_zod2.z.literal("eval"),
5043
- prompt: import_zod2.z.string().min(1),
5044
- goldenSetId: import_zod2.z.string().min(1).optional(),
5045
- assertions: import_zod2.z.array(assertionResultRecordSchema).min(1)
5046
- });
5047
- testCaseBaseSchema = import_zod2.z.object({
5048
- schemaVersion: import_zod2.z.literal(TEST_CASE_SCHEMA_VERSION),
5049
- id: import_zod2.z.string().min(1),
5050
- externalKeys: import_zod2.z.array(externalKeySchema).optional(),
5051
- /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
5052
- is_flaky: import_zod2.z.boolean().optional(),
5053
- /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
5054
- is_muted: import_zod2.z.boolean().optional()
5055
- });
5056
- testCaseSchema = import_zod2.z.discriminatedUnion("type", [
5057
- testCaseBaseSchema.merge(manualCaseSchema),
5058
- testCaseBaseSchema.merge(automatedCaseSchema),
5059
- testCaseBaseSchema.merge(evalCaseSchema)
5060
- ]);
5061
- }
5062
- });
5063
-
5064
- // src/types/results.ts
5065
- var ROW_INCONCLUSIVE;
5066
- var init_results = __esm({
5067
- "src/types/results.ts"() {
5068
- "use strict";
5069
- init_cjs_shims();
5070
- init_test_case_schema();
5071
- ROW_INCONCLUSIVE = ["blocked", "skipped"];
5072
- }
5073
- });
5074
-
5075
5085
  // src/types/run-result-schema.ts
5076
5086
  function parseRunResult(value2) {
5077
5087
  return runResultSchema.parse(value2);
@@ -5120,6 +5130,9 @@ var init_run_result_schema = __esm({
5120
5130
  passed: import_zod3.z.number(),
5121
5131
  failed: import_zod3.z.number(),
5122
5132
  errored: import_zod3.z.number(),
5133
+ /** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
5134
+ blocked: import_zod3.z.number().optional(),
5135
+ skipped: import_zod3.z.number().optional(),
5123
5136
  cost_usd: import_zod3.z.number(),
5124
5137
  duration_ms: import_zod3.z.number()
5125
5138
  });
@@ -5934,7 +5947,7 @@ function createProgram() {
5934
5947
  runCommand.command("delete").argument(
5935
5948
  "<id>",
5936
5949
  "The run id \u2014 the one `plune run import` printed, or the one in the dashboard URL"
5937
- ).description("Delete a run and everything it produced \u2014 recoverable for six months").action(async (id, _options, command) => {
5950
+ ).description("Delete a run and everything it produced \u2014 recoverable for 30 days").action(async (id, _options, command) => {
5938
5951
  try {
5939
5952
  const { handleRunDelete: handleRunDelete2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
5940
5953
  await handleRunDelete2(id);
package/dist/index.cjs CHANGED
@@ -1272,6 +1272,64 @@ async function mapLimit(items, limit, fn) {
1272
1272
  return results;
1273
1273
  }
1274
1274
 
1275
+ // src/types/test-case-schema.ts
1276
+ var import_zod2 = require("zod");
1277
+ var TEST_CASE_SCHEMA_VERSION = 2;
1278
+ var externalKeySchema = import_zod2.z.object({
1279
+ kind: import_zod2.z.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
1280
+ value: import_zod2.z.string().min(1)
1281
+ });
1282
+ var manualStepSchema = import_zod2.z.object({
1283
+ action: import_zod2.z.string().min(1),
1284
+ expected: import_zod2.z.string().min(1).optional()
1285
+ });
1286
+ var assertionResultRecordSchema = import_zod2.z.object({
1287
+ type: import_zod2.z.string().min(1),
1288
+ passed: import_zod2.z.boolean(),
1289
+ score: import_zod2.z.number().optional(),
1290
+ reason: import_zod2.z.string().optional()
1291
+ });
1292
+ var manualCaseSchema = import_zod2.z.object({
1293
+ type: import_zod2.z.literal("manual"),
1294
+ steps: import_zod2.z.array(manualStepSchema).min(1),
1295
+ expected: import_zod2.z.string().min(1).optional()
1296
+ });
1297
+ var automatedCaseSchema = import_zod2.z.object({
1298
+ type: import_zod2.z.literal("automated"),
1299
+ specRef: import_zod2.z.string().min(1)
1300
+ });
1301
+ var evalCaseSchema = import_zod2.z.object({
1302
+ type: import_zod2.z.literal("eval"),
1303
+ prompt: import_zod2.z.string().min(1),
1304
+ goldenSetId: import_zod2.z.string().min(1).optional(),
1305
+ assertions: import_zod2.z.array(assertionResultRecordSchema).min(1)
1306
+ });
1307
+ var testCaseBaseSchema = import_zod2.z.object({
1308
+ schemaVersion: import_zod2.z.literal(TEST_CASE_SCHEMA_VERSION),
1309
+ id: import_zod2.z.string().min(1),
1310
+ externalKeys: import_zod2.z.array(externalKeySchema).optional(),
1311
+ /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1312
+ is_flaky: import_zod2.z.boolean().optional(),
1313
+ /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1314
+ is_muted: import_zod2.z.boolean().optional()
1315
+ });
1316
+ var testCaseSchema = import_zod2.z.discriminatedUnion("type", [
1317
+ testCaseBaseSchema.merge(manualCaseSchema),
1318
+ testCaseBaseSchema.merge(automatedCaseSchema),
1319
+ testCaseBaseSchema.merge(evalCaseSchema)
1320
+ ]);
1321
+ function parseTestCase(value) {
1322
+ return testCaseSchema.parse(value);
1323
+ }
1324
+
1325
+ // src/types/results.ts
1326
+ var ROW_INCONCLUSIVE = ["blocked", "skipped"];
1327
+ function rowStatus(row) {
1328
+ if (row.inconclusive !== void 0) return row.inconclusive;
1329
+ if (row.error !== void 0) return "errored";
1330
+ return row.assertions.every((a) => a.passed) ? "passed" : "failed";
1331
+ }
1332
+
1275
1333
  // src/orchestrator/run.ts
1276
1334
  var DEFAULT_MAX_TOKENS2 = 1024;
1277
1335
  var PLUNE_VERSION = "0.1.0";
@@ -1414,10 +1472,25 @@ function resolveTemplate(ev, baseDir) {
1414
1472
  }
1415
1473
  throw new RunConfigError(`Eval "${ev.id}" has neither prompt nor prompt_file`);
1416
1474
  }
1417
- function classify(row) {
1418
- if (row.error !== void 0) return "errored";
1419
- if (row.assertions.some((a) => !a.passed)) return "failed";
1420
- return "passed";
1475
+ function summarize(evals, durationMs) {
1476
+ const summary = {
1477
+ total: 0,
1478
+ passed: 0,
1479
+ failed: 0,
1480
+ errored: 0,
1481
+ blocked: 0,
1482
+ skipped: 0,
1483
+ cost_usd: 0,
1484
+ duration_ms: durationMs
1485
+ };
1486
+ for (const ev of evals) {
1487
+ for (const row of ev.rows) {
1488
+ summary.total += 1;
1489
+ summary[rowStatus(row)] += 1;
1490
+ summary.cost_usd += row.usage?.cost_usd ?? 0;
1491
+ }
1492
+ }
1493
+ return summary;
1421
1494
  }
1422
1495
  async function runOrchestration(config, options, deps) {
1423
1496
  const started = deps.now();
@@ -1452,26 +1525,12 @@ async function runOrchestration(config, options, deps) {
1452
1525
  noCache
1453
1526
  })
1454
1527
  );
1455
- const passed = rowResults.every((r) => classify(r) === "passed");
1528
+ const passed = rowResults.every((r) => rowStatus(r) === "passed");
1456
1529
  evalResults.push({ id: ev.id, tags: ev.tags ?? [], rows: rowResults, passed });
1457
1530
  if (options.bail === true && !passed) break;
1458
1531
  }
1459
1532
  const finished = deps.now();
1460
- const summary = {
1461
- total: 0,
1462
- passed: 0,
1463
- failed: 0,
1464
- errored: 0,
1465
- cost_usd: 0,
1466
- duration_ms: finished - started
1467
- };
1468
- for (const ev of evalResults) {
1469
- for (const row of ev.rows) {
1470
- summary.total += 1;
1471
- summary[classify(row)] += 1;
1472
- summary.cost_usd += row.usage?.cost_usd ?? 0;
1473
- }
1474
- }
1533
+ const summary = summarize(evalResults, finished - started);
1475
1534
  return {
1476
1535
  schemaVersion: 1,
1477
1536
  plune_version: PLUNE_VERSION,
@@ -1556,61 +1615,6 @@ var path5 = __toESM(require("path"), 1);
1556
1615
 
1557
1616
  // src/types/run-result-schema.ts
1558
1617
  var import_zod3 = require("zod");
1559
-
1560
- // src/types/test-case-schema.ts
1561
- var import_zod2 = require("zod");
1562
- var TEST_CASE_SCHEMA_VERSION = 2;
1563
- var externalKeySchema = import_zod2.z.object({
1564
- kind: import_zod2.z.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
1565
- value: import_zod2.z.string().min(1)
1566
- });
1567
- var manualStepSchema = import_zod2.z.object({
1568
- action: import_zod2.z.string().min(1),
1569
- expected: import_zod2.z.string().min(1).optional()
1570
- });
1571
- var assertionResultRecordSchema = import_zod2.z.object({
1572
- type: import_zod2.z.string().min(1),
1573
- passed: import_zod2.z.boolean(),
1574
- score: import_zod2.z.number().optional(),
1575
- reason: import_zod2.z.string().optional()
1576
- });
1577
- var manualCaseSchema = import_zod2.z.object({
1578
- type: import_zod2.z.literal("manual"),
1579
- steps: import_zod2.z.array(manualStepSchema).min(1),
1580
- expected: import_zod2.z.string().min(1).optional()
1581
- });
1582
- var automatedCaseSchema = import_zod2.z.object({
1583
- type: import_zod2.z.literal("automated"),
1584
- specRef: import_zod2.z.string().min(1)
1585
- });
1586
- var evalCaseSchema = import_zod2.z.object({
1587
- type: import_zod2.z.literal("eval"),
1588
- prompt: import_zod2.z.string().min(1),
1589
- goldenSetId: import_zod2.z.string().min(1).optional(),
1590
- assertions: import_zod2.z.array(assertionResultRecordSchema).min(1)
1591
- });
1592
- var testCaseBaseSchema = import_zod2.z.object({
1593
- schemaVersion: import_zod2.z.literal(TEST_CASE_SCHEMA_VERSION),
1594
- id: import_zod2.z.string().min(1),
1595
- externalKeys: import_zod2.z.array(externalKeySchema).optional(),
1596
- /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1597
- is_flaky: import_zod2.z.boolean().optional(),
1598
- /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1599
- is_muted: import_zod2.z.boolean().optional()
1600
- });
1601
- var testCaseSchema = import_zod2.z.discriminatedUnion("type", [
1602
- testCaseBaseSchema.merge(manualCaseSchema),
1603
- testCaseBaseSchema.merge(automatedCaseSchema),
1604
- testCaseBaseSchema.merge(evalCaseSchema)
1605
- ]);
1606
- function parseTestCase(value) {
1607
- return testCaseSchema.parse(value);
1608
- }
1609
-
1610
- // src/types/results.ts
1611
- var ROW_INCONCLUSIVE = ["blocked", "skipped"];
1612
-
1613
- // src/types/run-result-schema.ts
1614
1618
  var RUN_RESULT_SCHEMA_VERSION = 1;
1615
1619
  var binaryVerdictSchema = import_zod3.z.object({
1616
1620
  question: import_zod3.z.string().min(1),
@@ -1648,6 +1652,9 @@ var summarySchema = import_zod3.z.object({
1648
1652
  passed: import_zod3.z.number(),
1649
1653
  failed: import_zod3.z.number(),
1650
1654
  errored: import_zod3.z.number(),
1655
+ /** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
1656
+ blocked: import_zod3.z.number().optional(),
1657
+ skipped: import_zod3.z.number().optional(),
1651
1658
  cost_usd: import_zod3.z.number(),
1652
1659
  duration_ms: import_zod3.z.number()
1653
1660
  });
package/dist/index.d.cts CHANGED
@@ -5,6 +5,14 @@ interface Summary {
5
5
  passed: number;
6
6
  failed: number;
7
7
  errored: number;
8
+ /**
9
+ * plune#1245. Rows the runner said were never judged (`RowResult.inconclusive`), counted apart from
10
+ * `failed`. The runner always writes both; they are optional so a RunResult written before them is
11
+ * still valid and `schemaVersion` stays 1. With them, `passed + failed + errored + blocked + skipped`
12
+ * is `total`.
13
+ */
14
+ blocked?: number;
15
+ skipped?: number;
8
16
  cost_usd: number;
9
17
  duration_ms: number;
10
18
  }
@@ -430,6 +438,8 @@ declare const runResultSchema: z.ZodObject<{
430
438
  passed: z.ZodNumber;
431
439
  failed: z.ZodNumber;
432
440
  errored: z.ZodNumber;
441
+ blocked: z.ZodOptional<z.ZodNumber>;
442
+ skipped: z.ZodOptional<z.ZodNumber>;
433
443
  cost_usd: z.ZodNumber;
434
444
  duration_ms: z.ZodNumber;
435
445
  }, z.core.$strip>;
package/dist/index.d.ts CHANGED
@@ -5,6 +5,14 @@ interface Summary {
5
5
  passed: number;
6
6
  failed: number;
7
7
  errored: number;
8
+ /**
9
+ * plune#1245. Rows the runner said were never judged (`RowResult.inconclusive`), counted apart from
10
+ * `failed`. The runner always writes both; they are optional so a RunResult written before them is
11
+ * still valid and `schemaVersion` stays 1. With them, `passed + failed + errored + blocked + skipped`
12
+ * is `total`.
13
+ */
14
+ blocked?: number;
15
+ skipped?: number;
8
16
  cost_usd: number;
9
17
  duration_ms: number;
10
18
  }
@@ -430,6 +438,8 @@ declare const runResultSchema: z.ZodObject<{
430
438
  passed: z.ZodNumber;
431
439
  failed: z.ZodNumber;
432
440
  errored: z.ZodNumber;
441
+ blocked: z.ZodOptional<z.ZodNumber>;
442
+ skipped: z.ZodOptional<z.ZodNumber>;
433
443
  cost_usd: z.ZodNumber;
434
444
  duration_ms: z.ZodNumber;
435
445
  }, z.core.$strip>;
package/dist/index.js CHANGED
@@ -1216,6 +1216,64 @@ async function mapLimit(items, limit, fn) {
1216
1216
  return results;
1217
1217
  }
1218
1218
 
1219
+ // src/types/test-case-schema.ts
1220
+ import { z as z2 } from "zod";
1221
+ var TEST_CASE_SCHEMA_VERSION = 2;
1222
+ var externalKeySchema = z2.object({
1223
+ kind: z2.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
1224
+ value: z2.string().min(1)
1225
+ });
1226
+ var manualStepSchema = z2.object({
1227
+ action: z2.string().min(1),
1228
+ expected: z2.string().min(1).optional()
1229
+ });
1230
+ var assertionResultRecordSchema = z2.object({
1231
+ type: z2.string().min(1),
1232
+ passed: z2.boolean(),
1233
+ score: z2.number().optional(),
1234
+ reason: z2.string().optional()
1235
+ });
1236
+ var manualCaseSchema = z2.object({
1237
+ type: z2.literal("manual"),
1238
+ steps: z2.array(manualStepSchema).min(1),
1239
+ expected: z2.string().min(1).optional()
1240
+ });
1241
+ var automatedCaseSchema = z2.object({
1242
+ type: z2.literal("automated"),
1243
+ specRef: z2.string().min(1)
1244
+ });
1245
+ var evalCaseSchema = z2.object({
1246
+ type: z2.literal("eval"),
1247
+ prompt: z2.string().min(1),
1248
+ goldenSetId: z2.string().min(1).optional(),
1249
+ assertions: z2.array(assertionResultRecordSchema).min(1)
1250
+ });
1251
+ var testCaseBaseSchema = z2.object({
1252
+ schemaVersion: z2.literal(TEST_CASE_SCHEMA_VERSION),
1253
+ id: z2.string().min(1),
1254
+ externalKeys: z2.array(externalKeySchema).optional(),
1255
+ /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1256
+ is_flaky: z2.boolean().optional(),
1257
+ /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1258
+ is_muted: z2.boolean().optional()
1259
+ });
1260
+ var testCaseSchema = z2.discriminatedUnion("type", [
1261
+ testCaseBaseSchema.merge(manualCaseSchema),
1262
+ testCaseBaseSchema.merge(automatedCaseSchema),
1263
+ testCaseBaseSchema.merge(evalCaseSchema)
1264
+ ]);
1265
+ function parseTestCase(value) {
1266
+ return testCaseSchema.parse(value);
1267
+ }
1268
+
1269
+ // src/types/results.ts
1270
+ var ROW_INCONCLUSIVE = ["blocked", "skipped"];
1271
+ function rowStatus(row) {
1272
+ if (row.inconclusive !== void 0) return row.inconclusive;
1273
+ if (row.error !== void 0) return "errored";
1274
+ return row.assertions.every((a) => a.passed) ? "passed" : "failed";
1275
+ }
1276
+
1219
1277
  // src/orchestrator/run.ts
1220
1278
  var DEFAULT_MAX_TOKENS2 = 1024;
1221
1279
  var PLUNE_VERSION = "0.1.0";
@@ -1358,10 +1416,25 @@ function resolveTemplate(ev, baseDir) {
1358
1416
  }
1359
1417
  throw new RunConfigError(`Eval "${ev.id}" has neither prompt nor prompt_file`);
1360
1418
  }
1361
- function classify(row) {
1362
- if (row.error !== void 0) return "errored";
1363
- if (row.assertions.some((a) => !a.passed)) return "failed";
1364
- return "passed";
1419
+ function summarize(evals, durationMs) {
1420
+ const summary = {
1421
+ total: 0,
1422
+ passed: 0,
1423
+ failed: 0,
1424
+ errored: 0,
1425
+ blocked: 0,
1426
+ skipped: 0,
1427
+ cost_usd: 0,
1428
+ duration_ms: durationMs
1429
+ };
1430
+ for (const ev of evals) {
1431
+ for (const row of ev.rows) {
1432
+ summary.total += 1;
1433
+ summary[rowStatus(row)] += 1;
1434
+ summary.cost_usd += row.usage?.cost_usd ?? 0;
1435
+ }
1436
+ }
1437
+ return summary;
1365
1438
  }
1366
1439
  async function runOrchestration(config, options, deps) {
1367
1440
  const started = deps.now();
@@ -1396,26 +1469,12 @@ async function runOrchestration(config, options, deps) {
1396
1469
  noCache
1397
1470
  })
1398
1471
  );
1399
- const passed = rowResults.every((r) => classify(r) === "passed");
1472
+ const passed = rowResults.every((r) => rowStatus(r) === "passed");
1400
1473
  evalResults.push({ id: ev.id, tags: ev.tags ?? [], rows: rowResults, passed });
1401
1474
  if (options.bail === true && !passed) break;
1402
1475
  }
1403
1476
  const finished = deps.now();
1404
- const summary = {
1405
- total: 0,
1406
- passed: 0,
1407
- failed: 0,
1408
- errored: 0,
1409
- cost_usd: 0,
1410
- duration_ms: finished - started
1411
- };
1412
- for (const ev of evalResults) {
1413
- for (const row of ev.rows) {
1414
- summary.total += 1;
1415
- summary[classify(row)] += 1;
1416
- summary.cost_usd += row.usage?.cost_usd ?? 0;
1417
- }
1418
- }
1477
+ const summary = summarize(evalResults, finished - started);
1419
1478
  return {
1420
1479
  schemaVersion: 1,
1421
1480
  plune_version: PLUNE_VERSION,
@@ -1500,61 +1559,6 @@ import * as path5 from "path";
1500
1559
 
1501
1560
  // src/types/run-result-schema.ts
1502
1561
  import { z as z3 } from "zod";
1503
-
1504
- // src/types/test-case-schema.ts
1505
- import { z as z2 } from "zod";
1506
- var TEST_CASE_SCHEMA_VERSION = 2;
1507
- var externalKeySchema = z2.object({
1508
- kind: z2.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
1509
- value: z2.string().min(1)
1510
- });
1511
- var manualStepSchema = z2.object({
1512
- action: z2.string().min(1),
1513
- expected: z2.string().min(1).optional()
1514
- });
1515
- var assertionResultRecordSchema = z2.object({
1516
- type: z2.string().min(1),
1517
- passed: z2.boolean(),
1518
- score: z2.number().optional(),
1519
- reason: z2.string().optional()
1520
- });
1521
- var manualCaseSchema = z2.object({
1522
- type: z2.literal("manual"),
1523
- steps: z2.array(manualStepSchema).min(1),
1524
- expected: z2.string().min(1).optional()
1525
- });
1526
- var automatedCaseSchema = z2.object({
1527
- type: z2.literal("automated"),
1528
- specRef: z2.string().min(1)
1529
- });
1530
- var evalCaseSchema = z2.object({
1531
- type: z2.literal("eval"),
1532
- prompt: z2.string().min(1),
1533
- goldenSetId: z2.string().min(1).optional(),
1534
- assertions: z2.array(assertionResultRecordSchema).min(1)
1535
- });
1536
- var testCaseBaseSchema = z2.object({
1537
- schemaVersion: z2.literal(TEST_CASE_SCHEMA_VERSION),
1538
- id: z2.string().min(1),
1539
- externalKeys: z2.array(externalKeySchema).optional(),
1540
- /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1541
- is_flaky: z2.boolean().optional(),
1542
- /** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
1543
- is_muted: z2.boolean().optional()
1544
- });
1545
- var testCaseSchema = z2.discriminatedUnion("type", [
1546
- testCaseBaseSchema.merge(manualCaseSchema),
1547
- testCaseBaseSchema.merge(automatedCaseSchema),
1548
- testCaseBaseSchema.merge(evalCaseSchema)
1549
- ]);
1550
- function parseTestCase(value) {
1551
- return testCaseSchema.parse(value);
1552
- }
1553
-
1554
- // src/types/results.ts
1555
- var ROW_INCONCLUSIVE = ["blocked", "skipped"];
1556
-
1557
- // src/types/run-result-schema.ts
1558
1562
  var RUN_RESULT_SCHEMA_VERSION = 1;
1559
1563
  var binaryVerdictSchema = z3.object({
1560
1564
  question: z3.string().min(1),
@@ -1592,6 +1596,9 @@ var summarySchema = z3.object({
1592
1596
  passed: z3.number(),
1593
1597
  failed: z3.number(),
1594
1598
  errored: z3.number(),
1599
+ /** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
1600
+ blocked: z3.number().optional(),
1601
+ skipped: z3.number().optional(),
1595
1602
  cost_usd: z3.number(),
1596
1603
  duration_ms: z3.number()
1597
1604
  });
@@ -215,7 +215,7 @@ function createClient(opts) {
215
215
  if (res.status === 429 && retryAfterMs(res) === null) {
216
216
  last.kind = "invalid";
217
217
  last.detail = "quota exceeded (no Retry-After)";
218
- if (said.detail) last.detail += ` \u2014 ${said.detail}`;
218
+ if (said.detail) last.detail += ` \u2014 ${scrub(said.detail)}`;
219
219
  break;
220
220
  }
221
221
  if (attempt < RETRY_DELAYS_MS.length) {
@@ -166,7 +166,7 @@ function createClient(opts) {
166
166
  if (res.status === 429 && retryAfterMs(res) === null) {
167
167
  last.kind = "invalid";
168
168
  last.detail = "quota exceeded (no Retry-After)";
169
- if (said.detail) last.detail += ` \u2014 ${said.detail}`;
169
+ if (said.detail) last.detail += ` \u2014 ${scrub(said.detail)}`;
170
170
  break;
171
171
  }
172
172
  if (attempt < RETRY_DELAYS_MS.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plune-ai/cli",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "AI-powered assertion testing for LLM apps — write assertions, run them against any provider, get a pass/fail report (local, CI, or diff).",
5
5
  "keywords": [
6
6
  "ai",