@plune-ai/cli 0.15.0 → 0.16.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/dist/index.cjs CHANGED
@@ -414,11 +414,25 @@ async function withRetry(fn, options) {
414
414
 
415
415
  // src/providers/prices.ts
416
416
  var PRICE_TABLE = {
417
- // Anthropic
417
+ // Anthropic, current models. Source: platform.claude.com/docs/en/about-claude/pricing — base
418
+ // input and output tokens per million, read on 2026-09-29, kept here per thousand.
419
+ "claude-fable-5-1": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
420
+ "claude-opus-5-5": { input_per_1k_usd: 4e-3, output_per_1k_usd: 0.02 },
421
+ "claude-sonnet-5-5": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
422
+ "claude-haiku-4-5": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
423
+ // The dated id of the same model; a config may name either.
424
+ "claude-haiku-4-5-20251001": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
425
+ // Anthropic, older ids: unchanged, so a config that still names one prices as it did. Anthropic has
426
+ // retired these models — a new project should not start on them.
418
427
  "claude-3-5-haiku-latest": { input_per_1k_usd: 8e-4, output_per_1k_usd: 4e-3 },
419
428
  "claude-3-5-sonnet-latest": { input_per_1k_usd: 3e-3, output_per_1k_usd: 0.015 },
420
429
  "claude-3-opus-latest": { input_per_1k_usd: 0.015, output_per_1k_usd: 0.075 },
421
- // OpenAI
430
+ // OpenAI, current models. Source: developers.openai.com/api/docs/pricing — standard tier, the
431
+ // short-context rates (a long-context request costs more), read on 2026-09-29.
432
+ "gpt-6-astra": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
433
+ "gpt-6.1-sol": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
434
+ "gpt-6-luna": { input_per_1k_usd: 1e-4, output_per_1k_usd: 5e-4 },
435
+ // OpenAI, older ids: unchanged (the same rates the page above still lists for both).
422
436
  "gpt-4o": { input_per_1k_usd: 25e-4, output_per_1k_usd: 0.01 },
423
437
  "gpt-4o-mini": { input_per_1k_usd: 15e-5, output_per_1k_usd: 6e-4 },
424
438
  // OpenAI via OpenRouter (namespaced ids). OpenRouter passes OpenAI list price through for
@@ -607,6 +621,18 @@ function getProvider(config, env, pricing) {
607
621
  return createDefaultRegistry().resolve(config.type, config, env, pricing);
608
622
  }
609
623
 
624
+ // src/providers/estimate.ts
625
+ function makeEstimateProvider(config, pricing) {
626
+ return {
627
+ complete: () => Promise.reject(
628
+ new Error("The --dry-run provider only estimates cost; it never calls a model.")
629
+ ),
630
+ estimateCost: (usage, reportedCostUsd) => ({
631
+ cost_usd: resolveCost(usage, config.model, reportedCostUsd, pricing)
632
+ })
633
+ };
634
+ }
635
+
610
636
  // src/providers/mock.ts
611
637
  function makeMockProvider() {
612
638
  return {
@@ -1473,11 +1499,16 @@ var NOOP_CACHE = { get: () => void 0, set: () => {
1473
1499
  function isMockMode(env) {
1474
1500
  return env["PLUNE_MOCK_PROVIDER"] === "1";
1475
1501
  }
1502
+ function resolveProviderFor(config, dryRun) {
1503
+ if (isMockMode(process.env)) return () => makeMockProvider();
1504
+ if (dryRun) return (cfg) => makeEstimateProvider(cfg, config.pricing);
1505
+ return (cfg) => getProvider(cfg, process.env, config.pricing);
1506
+ }
1476
1507
  function buildRealDeps(config, baseDir, dryRun) {
1477
1508
  const dir = path4.join(baseDir, ".plune");
1478
1509
  fs5.mkdirSync(dir, { recursive: true });
1479
1510
  return {
1480
- resolveProvider: isMockMode(process.env) ? () => makeMockProvider() : (cfg) => getProvider(cfg, process.env, config.pricing),
1511
+ resolveProvider: resolveProviderFor(config, dryRun),
1481
1512
  embedder: getDefaultEmbedder(),
1482
1513
  cache: dryRun ? NOOP_CACHE : openCache(path4.join(dir, "cache.db")),
1483
1514
  now: Date.now,
package/dist/index.js CHANGED
@@ -361,11 +361,25 @@ async function withRetry(fn, options) {
361
361
 
362
362
  // src/providers/prices.ts
363
363
  var PRICE_TABLE = {
364
- // Anthropic
364
+ // Anthropic, current models. Source: platform.claude.com/docs/en/about-claude/pricing — base
365
+ // input and output tokens per million, read on 2026-09-29, kept here per thousand.
366
+ "claude-fable-5-1": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
367
+ "claude-opus-5-5": { input_per_1k_usd: 4e-3, output_per_1k_usd: 0.02 },
368
+ "claude-sonnet-5-5": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
369
+ "claude-haiku-4-5": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
370
+ // The dated id of the same model; a config may name either.
371
+ "claude-haiku-4-5-20251001": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
372
+ // Anthropic, older ids: unchanged, so a config that still names one prices as it did. Anthropic has
373
+ // retired these models — a new project should not start on them.
365
374
  "claude-3-5-haiku-latest": { input_per_1k_usd: 8e-4, output_per_1k_usd: 4e-3 },
366
375
  "claude-3-5-sonnet-latest": { input_per_1k_usd: 3e-3, output_per_1k_usd: 0.015 },
367
376
  "claude-3-opus-latest": { input_per_1k_usd: 0.015, output_per_1k_usd: 0.075 },
368
- // OpenAI
377
+ // OpenAI, current models. Source: developers.openai.com/api/docs/pricing — standard tier, the
378
+ // short-context rates (a long-context request costs more), read on 2026-09-29.
379
+ "gpt-6-astra": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
380
+ "gpt-6.1-sol": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
381
+ "gpt-6-luna": { input_per_1k_usd: 1e-4, output_per_1k_usd: 5e-4 },
382
+ // OpenAI, older ids: unchanged (the same rates the page above still lists for both).
369
383
  "gpt-4o": { input_per_1k_usd: 25e-4, output_per_1k_usd: 0.01 },
370
384
  "gpt-4o-mini": { input_per_1k_usd: 15e-5, output_per_1k_usd: 6e-4 },
371
385
  // OpenAI via OpenRouter (namespaced ids). OpenRouter passes OpenAI list price through for
@@ -554,6 +568,18 @@ function getProvider(config, env, pricing) {
554
568
  return createDefaultRegistry().resolve(config.type, config, env, pricing);
555
569
  }
556
570
 
571
+ // src/providers/estimate.ts
572
+ function makeEstimateProvider(config, pricing) {
573
+ return {
574
+ complete: () => Promise.reject(
575
+ new Error("The --dry-run provider only estimates cost; it never calls a model.")
576
+ ),
577
+ estimateCost: (usage, reportedCostUsd) => ({
578
+ cost_usd: resolveCost(usage, config.model, reportedCostUsd, pricing)
579
+ })
580
+ };
581
+ }
582
+
557
583
  // src/providers/mock.ts
558
584
  function makeMockProvider() {
559
585
  return {
@@ -1420,11 +1446,16 @@ var NOOP_CACHE = { get: () => void 0, set: () => {
1420
1446
  function isMockMode(env) {
1421
1447
  return env["PLUNE_MOCK_PROVIDER"] === "1";
1422
1448
  }
1449
+ function resolveProviderFor(config, dryRun) {
1450
+ if (isMockMode(process.env)) return () => makeMockProvider();
1451
+ if (dryRun) return (cfg) => makeEstimateProvider(cfg, config.pricing);
1452
+ return (cfg) => getProvider(cfg, process.env, config.pricing);
1453
+ }
1423
1454
  function buildRealDeps(config, baseDir, dryRun) {
1424
1455
  const dir = path4.join(baseDir, ".plune");
1425
1456
  fs5.mkdirSync(dir, { recursive: true });
1426
1457
  return {
1427
- resolveProvider: isMockMode(process.env) ? () => makeMockProvider() : (cfg) => getProvider(cfg, process.env, config.pricing),
1458
+ resolveProvider: resolveProviderFor(config, dryRun),
1428
1459
  embedder: getDefaultEmbedder(),
1429
1460
  cache: dryRun ? NOOP_CACHE : openCache(path4.join(dir, "cache.db")),
1430
1461
  now: Date.now,
@@ -34,6 +34,7 @@ __export(reporter_core_exports, {
34
34
  RESULT_KEY_MAX: () => RESULT_KEY_MAX,
35
35
  UNSUPPORTED_VARS: () => UNSUPPORTED_VARS,
36
36
  appendBatch: () => appendBatch,
37
+ ciCommit: () => ciCommit,
37
38
  createClient: () => createClient,
38
39
  errorContextOf: () => errorContextOf,
39
40
  failureOf: () => failureOf,
@@ -247,6 +248,28 @@ function defaultRunTitle(env = process.env, now = /* @__PURE__ */ new Date(), cw
247
248
  const where = flag(env, "CI") || flag(env, "GITHUB_ACTIONS") ? "ci" : "local";
248
249
  return `${path.basename(cwd)} \xB7 ${stamp} \xB7 ${where}`;
249
250
  }
251
+ var PROVIDERS = [
252
+ { is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
253
+ { is: "GITLAB_CI", sha: "CI_COMMIT_SHA", branch: ["CI_COMMIT_REF_NAME"] },
254
+ { is: "JENKINS_URL", sha: "GIT_COMMIT", branch: ["GIT_BRANCH"] }
255
+ ];
256
+ function ciCommit(reported, env = process.env) {
257
+ const said = typeof reported === "object" && reported !== null ? reported : {};
258
+ const text = (raw) => typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
259
+ const provider = PROVIDERS.find((p) => value(env, p.is) !== void 0);
260
+ const job = {
261
+ sha: provider === void 0 ? void 0 : value(env, provider.sha),
262
+ branch: provider?.branch.map((name) => value(env, name)).find((v) => v !== void 0)
263
+ };
264
+ const sha = text(said["commitHash"]);
265
+ const sameCommit = sha === void 0 || sha === job.sha;
266
+ const commit = sha ?? job.sha;
267
+ const branch = text(said["branch"]) ?? (sameCommit ? job.branch : void 0);
268
+ return {
269
+ ...commit !== void 0 ? { sha: commit } : {},
270
+ ...branch !== void 0 ? { branch } : {}
271
+ };
272
+ }
250
273
 
251
274
  // src/reporter-core/fallback.ts
252
275
  var fs = __toESM(require("fs"), 1);
@@ -258,6 +281,8 @@ function appendBatch(file, batch) {
258
281
  ts: (/* @__PURE__ */ new Date()).toISOString(),
259
282
  runId: batch.runId,
260
283
  externalKey: batch.externalKey,
284
+ ...batch.session !== void 0 ? { session: batch.session } : {},
285
+ ...batch.meta !== void 0 ? { meta: batch.meta } : {},
261
286
  results: batch.results
262
287
  };
263
288
  fs.appendFileSync(file, `${JSON.stringify(line)}
@@ -311,6 +336,12 @@ function boundKey(key) {
311
336
  if (/[\uD800-\uDBFF]$/.test(head)) head = head.slice(0, -1);
312
337
  return { ...key, value: `${head}#${digest}` };
313
338
  }
339
+ function whyUnoffered(out) {
340
+ if (out.ok || out.status !== 429) return "other";
341
+ if (/review queue quota reached/i.test(out.detail)) return "queue";
342
+ if (/test case quota reached/i.test(out.detail)) return "cases";
343
+ return "other";
344
+ }
314
345
  function boundTitle(title) {
315
346
  return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
316
347
  }
@@ -354,6 +385,7 @@ async function startRun(passed, declared) {
354
385
  `));
355
386
  const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
356
387
  const externalKey = cfg.externalKey ?? null;
388
+ const sessionMarker = (0, import_node_crypto.randomUUID)();
357
389
  const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
358
390
  const offerDiscovered = cfg.offerDiscovered ?? false;
359
391
  const token = cfg.token ?? loadToken() ?? "";
@@ -405,7 +437,13 @@ async function startRun(passed, declared) {
405
437
  log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
406
438
  }
407
439
  function defer(results) {
408
- appendBatch(fallbackPath, { runId, externalKey, results });
440
+ appendBatch(fallbackPath, {
441
+ runId,
442
+ externalKey,
443
+ session: sessionMarker,
444
+ ...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
445
+ results
446
+ });
409
447
  stats.deferred += results.length;
410
448
  }
411
449
  async function resolveKeys(values) {
@@ -502,6 +540,7 @@ async function startRun(passed, declared) {
502
540
  });
503
541
  if (!out.ok || !Array.isArray(out.body?.results)) {
504
542
  stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
543
+ stats.unofferedWhy = whyUnoffered(out);
505
544
  log(
506
545
  `plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
507
546
  );
@@ -844,6 +883,7 @@ function webLink(href) {
844
883
  RESULT_KEY_MAX,
845
884
  UNSUPPORTED_VARS,
846
885
  appendBatch,
886
+ ciCommit,
847
887
  createClient,
848
888
  errorContextOf,
849
889
  failureOf,
@@ -200,6 +200,12 @@ interface ExpectedEntry {
200
200
  testCaseId?: string;
201
201
  externalKey?: KeyRef;
202
202
  }
203
+ /**
204
+ * Why the platform would not take the tests offered to it (plune-ai/cli#71): its review queue is at its
205
+ * ceiling, the project is at its case limit, or anything else — a throttle, an outage, a run that is not
206
+ * there. What a person does next depends on which, and only the first two have advice of their own.
207
+ */
208
+ type UnofferedWhy = 'queue' | 'cases' | 'other';
203
209
  /** The run as the platform answers with it. Only `id` is load-bearing for the client. */
204
210
  interface RunRecord {
205
211
  id: string;
@@ -240,6 +246,12 @@ interface RunStats extends SubmitCounts {
240
246
  * queue at capacity means "empty it and import again"; nothing else here means that.
241
247
  */
242
248
  unoffered: number;
249
+ /**
250
+ * Why they were not, said by the platform's own words and present only when `unoffered` is not zero:
251
+ * that is what carries the reason the paragraph above says a subtraction cannot. The platform's line
252
+ * about it is printed with the failure; this is what a command turns into the advice that fits.
253
+ */
254
+ unofferedWhy?: UnofferedWhy;
243
255
  /**
244
256
  * Results not delivered, written to the fallback file instead of the platform (#790): a batch
245
257
  * refused for any reason — its size, the network, the token, a closed run — or a result whose keys
@@ -375,6 +387,20 @@ interface EnvSettings {
375
387
  ignored: string[];
376
388
  }
377
389
  declare function readEnv(env?: NodeJS.ProcessEnv): EnvSettings;
390
+ /**
391
+ * The commit and the branch a run belongs to (plune-ai/plune#927), for `meta.sha` and `meta.branch`.
392
+ *
393
+ * `reported` is what the runner said about its CI — Playwright's `metadata.ci`, or the copy of it in
394
+ * its JSON report — and outranks the job's variables: it is a project's own word where the CI is one
395
+ * Playwright does not know, and it is where the tests ran when another job imports the report. So a
396
+ * report naming a commit other than this job's gets that commit and no branch of this job's: half of
397
+ * each would be a commit on a branch it is not on. Playwright gives no branch on GitHub at all, which
398
+ * is why the job's variables are read there.
399
+ *
400
+ * Each field is present only when something said it: outside a CI there is nothing to say, and nothing
401
+ * is guessed.
402
+ */
403
+ declare function ciCommit(reported?: unknown, env?: NodeJS.ProcessEnv): Pick<RunMeta, 'sha' | 'branch'>;
378
404
 
379
405
  /**
380
406
  * What ends up in a deferred batch.
@@ -397,10 +423,21 @@ declare const DEFAULT_FALLBACK_PATH: string;
397
423
  *
398
424
  * `runId: null` is a real state, not a missing value — the platform was unreachable when the run
399
425
  * was supposed to be created, so these results have no run yet and replay has to make one.
426
+ *
427
+ * `meta` is where that run came from — the commit and branch, the CI run, the runner (plune-ai/plune#927).
428
+ * Recorded so the run replay makes is the run the reporter would have made; replay reads it and nothing
429
+ * else about the machine it runs on. A line written before the field existed has none.
430
+ *
431
+ * `session` marks the reporter session that wrote the line, made once by that session (plune-ai/cli#45).
432
+ * A run of 24 tests at a batch of 10 is three lines, and replay opens one run for the lines that share a
433
+ * marker rather than one for each. A line written before the field existed has none, and is one run's
434
+ * worth by itself — nothing in it says otherwise.
400
435
  */
401
436
  interface DeferredBatch {
402
437
  runId: string | null;
403
438
  externalKey: string | null;
439
+ session?: string;
440
+ meta?: RunMeta;
404
441
  results: DeferredResult[];
405
442
  }
406
443
  /**
@@ -496,4 +533,4 @@ interface PlatformClient {
496
533
  }
497
534
  declare function createClient(opts: ClientOptions): PlatformClient;
498
535
 
499
- export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, appendBatch, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
536
+ export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
@@ -200,6 +200,12 @@ interface ExpectedEntry {
200
200
  testCaseId?: string;
201
201
  externalKey?: KeyRef;
202
202
  }
203
+ /**
204
+ * Why the platform would not take the tests offered to it (plune-ai/cli#71): its review queue is at its
205
+ * ceiling, the project is at its case limit, or anything else — a throttle, an outage, a run that is not
206
+ * there. What a person does next depends on which, and only the first two have advice of their own.
207
+ */
208
+ type UnofferedWhy = 'queue' | 'cases' | 'other';
203
209
  /** The run as the platform answers with it. Only `id` is load-bearing for the client. */
204
210
  interface RunRecord {
205
211
  id: string;
@@ -240,6 +246,12 @@ interface RunStats extends SubmitCounts {
240
246
  * queue at capacity means "empty it and import again"; nothing else here means that.
241
247
  */
242
248
  unoffered: number;
249
+ /**
250
+ * Why they were not, said by the platform's own words and present only when `unoffered` is not zero:
251
+ * that is what carries the reason the paragraph above says a subtraction cannot. The platform's line
252
+ * about it is printed with the failure; this is what a command turns into the advice that fits.
253
+ */
254
+ unofferedWhy?: UnofferedWhy;
243
255
  /**
244
256
  * Results not delivered, written to the fallback file instead of the platform (#790): a batch
245
257
  * refused for any reason — its size, the network, the token, a closed run — or a result whose keys
@@ -375,6 +387,20 @@ interface EnvSettings {
375
387
  ignored: string[];
376
388
  }
377
389
  declare function readEnv(env?: NodeJS.ProcessEnv): EnvSettings;
390
+ /**
391
+ * The commit and the branch a run belongs to (plune-ai/plune#927), for `meta.sha` and `meta.branch`.
392
+ *
393
+ * `reported` is what the runner said about its CI — Playwright's `metadata.ci`, or the copy of it in
394
+ * its JSON report — and outranks the job's variables: it is a project's own word where the CI is one
395
+ * Playwright does not know, and it is where the tests ran when another job imports the report. So a
396
+ * report naming a commit other than this job's gets that commit and no branch of this job's: half of
397
+ * each would be a commit on a branch it is not on. Playwright gives no branch on GitHub at all, which
398
+ * is why the job's variables are read there.
399
+ *
400
+ * Each field is present only when something said it: outside a CI there is nothing to say, and nothing
401
+ * is guessed.
402
+ */
403
+ declare function ciCommit(reported?: unknown, env?: NodeJS.ProcessEnv): Pick<RunMeta, 'sha' | 'branch'>;
378
404
 
379
405
  /**
380
406
  * What ends up in a deferred batch.
@@ -397,10 +423,21 @@ declare const DEFAULT_FALLBACK_PATH: string;
397
423
  *
398
424
  * `runId: null` is a real state, not a missing value — the platform was unreachable when the run
399
425
  * was supposed to be created, so these results have no run yet and replay has to make one.
426
+ *
427
+ * `meta` is where that run came from — the commit and branch, the CI run, the runner (plune-ai/plune#927).
428
+ * Recorded so the run replay makes is the run the reporter would have made; replay reads it and nothing
429
+ * else about the machine it runs on. A line written before the field existed has none.
430
+ *
431
+ * `session` marks the reporter session that wrote the line, made once by that session (plune-ai/cli#45).
432
+ * A run of 24 tests at a batch of 10 is three lines, and replay opens one run for the lines that share a
433
+ * marker rather than one for each. A line written before the field existed has none, and is one run's
434
+ * worth by itself — nothing in it says otherwise.
400
435
  */
401
436
  interface DeferredBatch {
402
437
  runId: string | null;
403
438
  externalKey: string | null;
439
+ session?: string;
440
+ meta?: RunMeta;
404
441
  results: DeferredResult[];
405
442
  }
406
443
  /**
@@ -496,4 +533,4 @@ interface PlatformClient {
496
533
  }
497
534
  declare function createClient(opts: ClientOptions): PlatformClient;
498
535
 
499
- export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, appendBatch, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
536
+ export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
@@ -1,5 +1,5 @@
1
1
  // src/reporter-core/session.ts
2
- import { createHash } from "crypto";
2
+ import { createHash, randomUUID } from "crypto";
3
3
  import { promises as disk } from "fs";
4
4
  import { basename as basename2, extname } from "path";
5
5
 
@@ -199,6 +199,28 @@ function defaultRunTitle(env = process.env, now = /* @__PURE__ */ new Date(), cw
199
199
  const where = flag(env, "CI") || flag(env, "GITHUB_ACTIONS") ? "ci" : "local";
200
200
  return `${path.basename(cwd)} \xB7 ${stamp} \xB7 ${where}`;
201
201
  }
202
+ var PROVIDERS = [
203
+ { is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
204
+ { is: "GITLAB_CI", sha: "CI_COMMIT_SHA", branch: ["CI_COMMIT_REF_NAME"] },
205
+ { is: "JENKINS_URL", sha: "GIT_COMMIT", branch: ["GIT_BRANCH"] }
206
+ ];
207
+ function ciCommit(reported, env = process.env) {
208
+ const said = typeof reported === "object" && reported !== null ? reported : {};
209
+ const text = (raw) => typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
210
+ const provider = PROVIDERS.find((p) => value(env, p.is) !== void 0);
211
+ const job = {
212
+ sha: provider === void 0 ? void 0 : value(env, provider.sha),
213
+ branch: provider?.branch.map((name) => value(env, name)).find((v) => v !== void 0)
214
+ };
215
+ const sha = text(said["commitHash"]);
216
+ const sameCommit = sha === void 0 || sha === job.sha;
217
+ const commit = sha ?? job.sha;
218
+ const branch = text(said["branch"]) ?? (sameCommit ? job.branch : void 0);
219
+ return {
220
+ ...commit !== void 0 ? { sha: commit } : {},
221
+ ...branch !== void 0 ? { branch } : {}
222
+ };
223
+ }
202
224
 
203
225
  // src/reporter-core/fallback.ts
204
226
  import * as fs from "fs";
@@ -210,6 +232,8 @@ function appendBatch(file, batch) {
210
232
  ts: (/* @__PURE__ */ new Date()).toISOString(),
211
233
  runId: batch.runId,
212
234
  externalKey: batch.externalKey,
235
+ ...batch.session !== void 0 ? { session: batch.session } : {},
236
+ ...batch.meta !== void 0 ? { meta: batch.meta } : {},
213
237
  results: batch.results
214
238
  };
215
239
  fs.appendFileSync(file, `${JSON.stringify(line)}
@@ -263,6 +287,12 @@ function boundKey(key) {
263
287
  if (/[\uD800-\uDBFF]$/.test(head)) head = head.slice(0, -1);
264
288
  return { ...key, value: `${head}#${digest}` };
265
289
  }
290
+ function whyUnoffered(out) {
291
+ if (out.ok || out.status !== 429) return "other";
292
+ if (/review queue quota reached/i.test(out.detail)) return "queue";
293
+ if (/test case quota reached/i.test(out.detail)) return "cases";
294
+ return "other";
295
+ }
266
296
  function boundTitle(title) {
267
297
  return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
268
298
  }
@@ -306,6 +336,7 @@ async function startRun(passed, declared) {
306
336
  `));
307
337
  const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
308
338
  const externalKey = cfg.externalKey ?? null;
339
+ const sessionMarker = randomUUID();
309
340
  const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
310
341
  const offerDiscovered = cfg.offerDiscovered ?? false;
311
342
  const token = cfg.token ?? loadToken() ?? "";
@@ -357,7 +388,13 @@ async function startRun(passed, declared) {
357
388
  log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
358
389
  }
359
390
  function defer(results) {
360
- appendBatch(fallbackPath, { runId, externalKey, results });
391
+ appendBatch(fallbackPath, {
392
+ runId,
393
+ externalKey,
394
+ session: sessionMarker,
395
+ ...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
396
+ results
397
+ });
361
398
  stats.deferred += results.length;
362
399
  }
363
400
  async function resolveKeys(values) {
@@ -454,6 +491,7 @@ async function startRun(passed, declared) {
454
491
  });
455
492
  if (!out.ok || !Array.isArray(out.body?.results)) {
456
493
  stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
494
+ stats.unofferedWhy = whyUnoffered(out);
457
495
  log(
458
496
  `plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
459
497
  );
@@ -795,6 +833,7 @@ export {
795
833
  RESULT_KEY_MAX,
796
834
  UNSUPPORTED_VARS,
797
835
  appendBatch,
836
+ ciCommit,
798
837
  createClient,
799
838
  errorContextOf,
800
839
  failureOf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plune-ai/cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.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",