@eigenpal/cli 0.4.7 → 0.4.9

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/CHANGELOG.md CHANGED
@@ -141,6 +141,16 @@
141
141
 
142
142
  Non-overlapping examples are listed in dedicated `only in A` / `only in B` blocks before the table so |Δ| sort stays meaningful.
143
143
 
144
+ - 9dc84b9: CLI: agent-friendly experiment polling, watching, and comparison.
145
+
146
+ Four small additions so agents (and humans writing monitoring scripts) don't have to roll their own bash pollers, JSON parsers, or aggregators on top of `experiment status`.
147
+ - **`experiment status --json` now includes a top-level `summary` rollup** — `{ total, terminal, complete, completedCount, failedCount, cancelledCount, rejectedCount, runningCount, pendingCount }`. Polling scripts can read `.summary.complete` / `.summary.failedCount` directly instead of folding `executions[].status` themselves. `executions[]` stays as-is for backwards compat.
148
+ - **`experiment status --short`** — single-line, awk-friendly stdout summary (e.g. `6/6 done failed=0 cancelled=0 rejected=0`). Mutually exclusive with `--watch`. Drops the failure detail block + closing tip so monitoring loops stay clean.
149
+ - **New `experiment watch <workflow-id> <batchId>`** — polls until terminal, then auto-pulls eval results to `./results-<batchId>.<format>` in one command. Replaces the old "monitor + status + pull + score" recipe. Flags: `--interval`, `--max-wait`, `--format csv|json`, `--pull-on-complete <path>` to override destination, `--no-pull` to opt out. Exit codes match `experiment status --watch` (0 clean, 1 any failure, 2 deadline).
150
+ - **`experiment compare` adds a per-evaluator aggregate** — a "Per-evaluator deltas" table renders above the existing per-row table with rows, mean Δ, regressions, and improvements per evaluator (sorted by |meanΔ| desc). `--json` exposes it at `summary.byEvaluator[]`.
151
+
152
+ Updated SKILL.md so the canonical "run an experiment + collect results" recipe uses `experiment watch` and adds a "monitoring scripts" section that calls out `--short` and `summary.complete`.
153
+
144
154
  - 3da0612: CLI: pretty default rendering for `list`, `get`, and mutating commands; `--json` toggle for piping.
145
155
  - `workflow definition/dataset/experiment/version/step-type list` and `workflow execution list` render aligned ASCII tables. `--json` restores the legacy raw payload.
146
156
  - `workflow execution get` (no other flags) prints the vertical step list via `renderFrame` instead of dumping 1000+ lines of JSON. `--json` for the full payload; `--output-path` / `--step` / `--include` continue to print filtered JSON.
package/dist/cli.js CHANGED
@@ -21310,7 +21310,6 @@ var init_extract = __esm(() => {
21310
21310
  prompt: exports_external.string().optional().describe("Custom prompt template for extraction"),
21311
21311
  provider: exports_external.string().optional().describe('Provider ID to use (e.g., "openai-gpt4o", "anthropic-claude")'),
21312
21312
  model: exports_external.string().optional().describe("Model to use (overrides provider default)"),
21313
- temperature: exports_external.number().min(0).max(2).default(0).describe("Model temperature (0 = deterministic)"),
21314
21313
  maxInputTokens: exports_external.number().int().optional().describe("Max input tokens. Truncates input text and logs a warning when exceeded. Omit for no limit.")
21315
21314
  });
21316
21315
  });
@@ -21844,6 +21843,9 @@ var init_execution_row = __esm(() => {
21844
21843
  ];
21845
21844
  });
21846
21845
 
21846
+ // ../types/src/llm-determinism.ts
21847
+ var DETERMINISTIC_TEMPERATURE = 0;
21848
+
21847
21849
  // ../types/src/workflow/steps.ts
21848
21850
  function parseStepType(type) {
21849
21851
  const [category, operation] = type.split(".");
@@ -22110,7 +22112,6 @@ var init_step_configs = __esm(() => {
22110
22112
  prompt: exports_external.string().optional().describe("Custom prompt template for extraction"),
22111
22113
  provider: exports_external.string().optional().describe('Provider ID (e.g., "openai-gpt4o")'),
22112
22114
  model: exports_external.string().optional().describe("Model override"),
22113
- temperature: exports_external.number().min(0).max(2).default(0).optional().describe("Model temperature (0 = deterministic)"),
22114
22115
  maxInputTokens: exports_external.number().int().optional().describe("Max input tokens. Truncates input text and logs a warning when exceeded. Omit for no limit.")
22115
22116
  });
22116
22117
  AiExtractOutputSchema = exports_external.record(exports_external.string(), exports_external.unknown()).describe("Extracted structured data matching the provided schema");
@@ -22695,7 +22696,8 @@ var init_workflow = __esm(() => {
22695
22696
  inputs: exports_external.array(WorkflowInputDefSchema).optional(),
22696
22697
  steps: exports_external.array(StepSchema),
22697
22698
  output: exports_external.record(exports_external.string(), exports_external.string()).optional(),
22698
- settings: WorkflowSettingsSchema.optional()
22699
+ settings: WorkflowSettingsSchema.optional(),
22700
+ defaultModel: exports_external.string().optional()
22699
22701
  });
22700
22702
  StoredWorkflowDefinitionSchema = exports_external.object({
22701
22703
  id: exports_external.string(),
@@ -23463,8 +23465,7 @@ async function judgeScoreJson(params) {
23463
23465
  const response = await client.extract(userContent, {
23464
23466
  schema: SCORE_SCHEMA,
23465
23467
  prompt: systemPrompt,
23466
- model,
23467
- temperature: 0
23468
+ model
23468
23469
  });
23469
23470
  const rawScore = response.data.score;
23470
23471
  const score = typeof rawScore === "number" ? rawScore : typeof rawScore === "string" ? parseFloat(rawScore) : NaN;
@@ -23484,8 +23485,7 @@ async function judgeLabelJson(params) {
23484
23485
  const response = await client.extract(userContent, {
23485
23486
  schema: buildLabelSchema(labels),
23486
23487
  prompt: systemPrompt,
23487
- model,
23488
- temperature: 0
23488
+ model
23489
23489
  });
23490
23490
  const label = typeof response.data.label === "string" ? response.data.label.trim() : "";
23491
23491
  if (!labels.includes(label)) {
@@ -23870,6 +23870,7 @@ __export(exports_src, {
23870
23870
  DatasetImportRequestSchema: () => DatasetImportRequestSchema,
23871
23871
  DatasetImportModeSchema: () => DatasetImportModeSchema,
23872
23872
  DURABLE_EXECUTION_TYPES: () => DURABLE_EXECUTION_TYPES,
23873
+ DETERMINISTIC_TEMPERATURE: () => DETERMINISTIC_TEMPERATURE,
23873
23874
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
23874
23875
  DEFAULT_CONCURRENCY: () => DEFAULT_CONCURRENCY,
23875
23876
  DATASET_NAME_PATTERN: () => DATASET_NAME_PATTERN,
@@ -143405,7 +143406,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
143405
143406
  // package.json
143406
143407
  var package_default = {
143407
143408
  name: "@eigenpal/cli",
143408
- version: "0.4.7",
143409
+ version: "0.4.9",
143409
143410
  description: "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
143410
143411
  keywords: [
143411
143412
  "eigenpal",
@@ -147595,10 +147596,47 @@ function buildBatchDiff(args) {
147595
147596
  sharedExamples: sharedExampleSet.size,
147596
147597
  onlyInA,
147597
147598
  onlyInB,
147598
- regressionThreshold
147599
+ regressionThreshold,
147600
+ byEvaluator: aggregateByEvaluator(rows)
147599
147601
  }
147600
147602
  };
147601
147603
  }
147604
+ function aggregateByEvaluator(rows) {
147605
+ const buckets = new Map;
147606
+ for (const r5 of rows) {
147607
+ const list = buckets.get(r5.evaluator);
147608
+ if (list)
147609
+ list.push(r5);
147610
+ else
147611
+ buckets.set(r5.evaluator, [r5]);
147612
+ }
147613
+ const aggregates = [];
147614
+ for (const [evaluator, group2] of buckets) {
147615
+ const comparable = group2.filter((r5) => r5.delta != null);
147616
+ const meanDelta = comparable.length === 0 ? null : comparable.reduce((sum, r5) => sum + r5.delta, 0) / comparable.length;
147617
+ aggregates.push({
147618
+ evaluator,
147619
+ comparable: comparable.length,
147620
+ meanDelta,
147621
+ regressions: group2.filter((r5) => r5.status === "regression").length,
147622
+ improvements: group2.filter((r5) => r5.status === "improvement").length,
147623
+ unchanged: group2.filter((r5) => r5.status === "unchanged").length
147624
+ });
147625
+ }
147626
+ aggregates.sort((a5, b8) => {
147627
+ if (a5.meanDelta == null && b8.meanDelta == null)
147628
+ return a5.evaluator.localeCompare(b8.evaluator);
147629
+ if (a5.meanDelta == null)
147630
+ return 1;
147631
+ if (b8.meanDelta == null)
147632
+ return -1;
147633
+ const diff = Math.abs(b8.meanDelta) - Math.abs(a5.meanDelta);
147634
+ if (diff !== 0)
147635
+ return diff;
147636
+ return a5.evaluator.localeCompare(b8.evaluator);
147637
+ });
147638
+ return aggregates;
147639
+ }
147602
147640
  function sortDiffRows(rows, sort) {
147603
147641
  const tiebreak = (a5, b8) => {
147604
147642
  const exCmp = a5.example.localeCompare(b8.example);
@@ -147684,10 +147722,30 @@ function renderBatchDiffHuman(diff) {
147684
147722
  }
147685
147723
  process.stderr.write(`
147686
147724
  `);
147725
+ if (diff.summary.byEvaluator.length > 0) {
147726
+ process.stderr.write(`${ui.bold("Per-evaluator deltas")} ${ui.dim("(B − A):")}
147727
+ `);
147728
+ console.log(table(diff.summary.byEvaluator, [
147729
+ { key: "evaluator", header: "evaluator" },
147730
+ { key: "comparable", header: "rows", align: "right" },
147731
+ {
147732
+ key: "meanDelta",
147733
+ header: "mean Δ",
147734
+ align: "right",
147735
+ format: (v11) => formatDelta(v11)
147736
+ },
147737
+ { key: "regressions", header: "regressions", align: "right" },
147738
+ { key: "improvements", header: "improvements", align: "right" }
147739
+ ]));
147740
+ process.stderr.write(`
147741
+ `);
147742
+ }
147687
147743
  if (diff.rows.length === 0) {
147688
147744
  process.stderr.write(`${ui.dim("No shared (example, evaluator) pairs to compare.")}
147689
147745
  `);
147690
147746
  } else {
147747
+ process.stderr.write(`${ui.bold("Per-row deltas:")}
147748
+ `);
147691
147749
  console.log(table(diff.rows, [
147692
147750
  { key: "example", header: "example" },
147693
147751
  { key: "evaluator", header: "evaluator" },
@@ -148444,7 +148502,7 @@ returns immediately with \`{ batchId, total }\`; poll
148444
148502
  return;
148445
148503
  }
148446
148504
  success2(`Started experiment ${ui.bold(start.batchId)} ${ui.dim(`(${start.total} execution${start.total === 1 ? "" : "s"} queued)`)}`);
148447
- process.stderr.write(` ${ui.dim(`Watch: eigenpal workflow experiment status ${workflowId} ${start.batchId} --watch`)}
148505
+ process.stderr.write(` ${ui.dim(`Watch + auto-pull: eigenpal workflow experiment watch ${workflowId} ${start.batchId}`)}
148448
148506
  `);
148449
148507
  return;
148450
148508
  }
@@ -148461,20 +148519,32 @@ returns immediately with \`{ batchId, total }\`; poll
148461
148519
  await new Promise((r5) => setTimeout(r5, intervalMs));
148462
148520
  }
148463
148521
  }));
148464
- const statusCmd = experiment.command("status <workflow-id> <batchId>").description(descriptionFor("workflow_get_experiment_status")).option("--watch", "Poll until every execution reaches a terminal state (completed/failed/cancelled/rejected), then exit", false).option("--interval <seconds>", "Poll interval in seconds when --watch is set (default 5)", intArg, 5).option("--max-wait <seconds>", "Hard ceiling for --watch in seconds (default 1800 = 30 min)", intArg, 30 * 60).option("--include <kinds>", "Comma-separated extras to attach when --watch terminates: payload (full per-execution snapshot, can be hundreds of KB)", "");
148522
+ const statusCmd = experiment.command("status <workflow-id> <batchId>").description(descriptionFor("workflow_get_experiment_status")).option("--watch", "Poll until every execution reaches a terminal state (completed/failed/cancelled/rejected), then exit", false).option("--short", "Single-line plain-text summary on stdout (e.g. `6/6 done failed=0 cancelled=0 rejected=0`). Pipe-friendly for monitoring loops; mutually exclusive with --watch.", false).option("--interval <seconds>", "Poll interval in seconds when --watch is set (default 5)", intArg, 5).option("--max-wait <seconds>", "Hard ceiling for --watch in seconds (default 1800 = 30 min)", intArg, 30 * 60).option("--include <kinds>", "Comma-separated extras to attach when --watch terminates: payload (full per-execution snapshot, can be hundreds of KB)", "");
148465
148523
  addJsonFlag(withBaseUrl(statusCmd)).action(action(async (workflow2, batchId, opts) => {
148524
+ if (opts.short && opts.watch) {
148525
+ error51("--short and --watch are mutually exclusive (short is for spot-check polling).");
148526
+ process.exit(2);
148527
+ }
148466
148528
  const includes = new Set((opts.include ?? "").split(",").map((s5) => s5.trim()).filter(Boolean));
148467
148529
  const includePayload = includes.has("payload");
148468
148530
  const { client, workflowId } = await buildClientForWorkflow(workflow2, opts);
148469
148531
  const url3 = `/api/v1/workflows/${workflowId}/experiments/${batchId}`;
148470
148532
  if (!opts.watch) {
148471
148533
  const result = await client.get(url3);
148534
+ const execs = result.executions ?? [];
148535
+ const summary = summarizeExperimentExecutions(execs);
148472
148536
  if (opts.json) {
148473
- printJson(result);
148537
+ printJson({ ...result, summary: rollupForJson(summary) });
148538
+ return;
148539
+ }
148540
+ if (opts.short) {
148541
+ console.log(formatShortStatus(summary));
148542
+ const failed2 = summary.counts.failed ?? 0;
148543
+ const cancelled2 = summary.counts.cancelled ?? 0;
148544
+ if (summary.done && failed2 + cancelled2 > 0)
148545
+ process.exit(1);
148474
148546
  return;
148475
148547
  }
148476
- const execs = result.executions ?? [];
148477
- const summary = summarizeExperimentExecutions(execs);
148478
148548
  const counts = Object.entries(summary.counts).map(([k9, v11]) => `${k9}=${v11}`).sort().join(" ");
148479
148549
  console.log(`${ui.bold(batchId)} ${summary.terminal}/${summary.total} terminal ${ui.dim(`(${counts || "no executions"})`)}`);
148480
148550
  const failed = summary.counts.failed ?? 0;
@@ -148488,56 +148558,25 @@ returns immediately with \`{ batchId, total }\`; poll
148488
148558
  process.exit(1);
148489
148559
  return;
148490
148560
  }
148491
- const deadline = Date.now() + opts.maxWait * 1000;
148492
- let lastSummary = "";
148493
- while (true) {
148494
- const result = await client.get(url3);
148495
- const execs = result.executions ?? [];
148496
- const summary = summarizeExperimentExecutions(execs);
148497
- const counts = Object.entries(summary.counts).map(([k9, v11]) => `${k9}=${v11}`).sort().join(" ");
148498
- const line3 = `${summary.terminal}/${summary.total} terminal ${ui.dim(`(${counts || "no executions"})`)}`;
148499
- if (line3 !== lastSummary) {
148500
- if (process.stderr.isTTY) {
148501
- process.stderr.write(`\r\x1B[K${line3}`);
148502
- } else {
148503
- process.stderr.write(`${line3}
148504
- `);
148505
- }
148506
- lastSummary = line3;
148507
- }
148508
- const wantPayload = opts.json || includePayload;
148509
- if (summary.done) {
148510
- if (process.stderr.isTTY)
148511
- process.stderr.write(`
148512
- `);
148513
- const failed = summary.counts.failed ?? 0;
148514
- const cancelled = summary.counts.cancelled ?? 0;
148515
- const rejected = summary.counts.rejected ?? 0;
148516
- const badCount = failed + cancelled + rejected;
148517
- if (badCount > 0) {
148518
- renderExperimentFailures(execs, summary.total);
148519
- }
148561
+ const wantPayload = opts.json || includePayload;
148562
+ await pollExperimentUntilTerminal({
148563
+ client,
148564
+ url: url3,
148565
+ interval: opts.interval,
148566
+ maxWait: opts.maxWait,
148567
+ onTerminal: (result, summary) => {
148520
148568
  if (wantPayload) {
148521
- printJson(result);
148569
+ printJson({ ...result, summary: rollupForJson(summary) });
148522
148570
  } else {
148523
148571
  process.stderr.write(`${ui.dim(`use --include payload (or --json) for the full per-execution snapshot`)}
148524
148572
  `);
148525
148573
  }
148526
- if (badCount > 0)
148527
- process.exit(1);
148528
- return;
148529
- }
148530
- if (Date.now() >= deadline) {
148531
- if (process.stderr.isTTY)
148532
- process.stderr.write(`
148533
- `);
148534
- error51(`Timed out after ${opts.maxWait}s with ${summary.total - summary.terminal} non-terminal execution(s). Re-run to keep watching.`);
148574
+ },
148575
+ onDeadline: (result, summary) => {
148535
148576
  if (wantPayload)
148536
- printJson(result);
148537
- process.exit(2);
148577
+ printJson({ ...result, summary: rollupForJson(summary) });
148538
148578
  }
148539
- await new Promise((r5) => setTimeout(r5, opts.interval * 1000));
148540
- }
148579
+ });
148541
148580
  }));
148542
148581
  const cancelCmd = experiment.command("cancel <workflow-id> <batchId>").description("Cancel every execution in a batch. Idempotent.").option("--yes", "Required for non-TTY shells (CI, pipes). Acts immediately, no prompt.").addHelpText("after", `
148543
148582
  Examples:
@@ -148626,6 +148665,62 @@ batches must belong to the same workflow within the caller's tenant.
148626
148665
  }
148627
148666
  renderBatchDiffHuman(diff);
148628
148667
  }));
148668
+ const watchCmd = experiment.command("watch <workflow-id> <batchId>").description("Poll until terminal, then auto-pull results — replaces `status --watch` + `results --out`.").option("--interval <seconds>", "Poll interval in seconds (default 5)", intArg, 5).option("--max-wait <seconds>", "Hard ceiling in seconds (default 1800 = 30 min)", intArg, 30 * 60).option("--pull-on-complete <path>", "Destination for the results file. Default: ./results-<batchId>.<format>").option("--format <csv|json>", "Results export format (default json)", parseExportFormat, "json").option("--no-pull", "Skip auto-pulling results on terminal (just watch)").addHelpText("after", `
148669
+ Examples:
148670
+ $ eigenpal workflow experiment watch wf_abc123 evb_xyz789
148671
+ $ eigenpal workflow experiment watch wf_abc123 evb_xyz789 --format csv
148672
+ $ eigenpal workflow experiment watch wf_abc123 evb_xyz789 --pull-on-complete ./out/r.json
148673
+ $ eigenpal workflow experiment watch wf_abc123 evb_xyz789 --no-pull
148674
+
148675
+ Behavior:
148676
+ - Polls every --interval seconds; redraws a one-line tick on a TTY,
148677
+ appends a new line per tick when piped.
148678
+ - On terminal: renders the failed-execution detail block (if any), then
148679
+ downloads results to ./results-<batchId>.<format> unless --no-pull
148680
+ or --pull-on-complete <path> says otherwise.
148681
+ - 4xx errors (e.g. unknown batch id) abort immediately rather than spin.
148682
+ - Pre-flight check: the destination directory must exist before the
148683
+ poll loop starts, so a typo doesn't waste a 30-min wait.
148684
+ - Exit codes (matching \`experiment status --watch\`):
148685
+ 0 every execution succeeded (or pull skipped via --no-pull on a clean batch)
148686
+ 1 at least one execution ended in failed/cancelled/rejected,
148687
+ OR the export download failed after a clean batch
148688
+ 2 --max-wait deadline reached — re-run to keep watching;
148689
+ OR misuse (e.g. --no-pull combined with --pull-on-complete)
148690
+ `);
148691
+ withBaseUrl(watchCmd).action(action(async (workflow2, batchId, opts) => {
148692
+ if (!opts.pull && opts.pullOnComplete) {
148693
+ error51("--no-pull and --pull-on-complete are mutually exclusive — pass one or the other.");
148694
+ process.exit(2);
148695
+ }
148696
+ const { client, workflowId } = await buildClientForWorkflow(workflow2, opts);
148697
+ const url3 = `/api/v1/workflows/${workflowId}/experiments/${batchId}`;
148698
+ const dest = opts.pull ? opts.pullOnComplete ?? `./results-${batchId}.${opts.format}` : null;
148699
+ if (dest) {
148700
+ const dir = path15.dirname(dest);
148701
+ if (!existsSync9(dir)) {
148702
+ error51(`Destination directory does not exist: ${dir}. Create it first or pass a path under an existing directory.`);
148703
+ process.exit(2);
148704
+ }
148705
+ }
148706
+ await pollExperimentUntilTerminal({
148707
+ client,
148708
+ url: url3,
148709
+ interval: opts.interval,
148710
+ maxWait: opts.maxWait,
148711
+ onTerminal: async () => {
148712
+ if (!dest)
148713
+ return;
148714
+ const params = new URLSearchParams({ format: opts.format, batchId });
148715
+ try {
148716
+ await downloadStreamToFile(client, `/api/v1/workflows/${workflowId}/eval-results/export?${params.toString()}`, dest);
148717
+ } catch (err2) {
148718
+ error51(`Watch finished but pulling results failed: ${err2 instanceof Error ? err2.message : String(err2)}. Re-run \`experiment results <wf> <batch>\` to retry.`);
148719
+ process.exit(1);
148720
+ }
148721
+ }
148722
+ });
148723
+ }));
148629
148724
  }
148630
148725
  function renderExperimentFailures(execs, totalCount) {
148631
148726
  const failedExecs = execs.filter((e8) => e8.status === "failed" || e8.status === "cancelled" || e8.status === "rejected");
@@ -148664,6 +148759,83 @@ function summarizeExperimentExecutions(execs) {
148664
148759
  done: execs.length > 0 && terminal === execs.length
148665
148760
  };
148666
148761
  }
148762
+ function rollupForJson(summary) {
148763
+ return {
148764
+ total: summary.total,
148765
+ terminal: summary.terminal,
148766
+ complete: summary.done,
148767
+ completedCount: summary.counts.completed ?? 0,
148768
+ failedCount: summary.counts.failed ?? 0,
148769
+ cancelledCount: summary.counts.cancelled ?? 0,
148770
+ rejectedCount: summary.counts.rejected ?? 0,
148771
+ runningCount: summary.counts.running ?? 0,
148772
+ pendingCount: summary.counts.pending ?? 0
148773
+ };
148774
+ }
148775
+ function formatShortStatus(summary) {
148776
+ const state = summary.done ? "done" : "in-progress";
148777
+ return [
148778
+ `${summary.terminal}/${summary.total}`,
148779
+ state,
148780
+ `failed=${summary.counts.failed ?? 0}`,
148781
+ `cancelled=${summary.counts.cancelled ?? 0}`,
148782
+ `rejected=${summary.counts.rejected ?? 0}`
148783
+ ].join(" ");
148784
+ }
148785
+ function renderTickLine(summary) {
148786
+ const counts = Object.entries(summary.counts).map(([k9, v11]) => `${k9}=${v11}`).sort().join(" ");
148787
+ return `${summary.terminal}/${summary.total} terminal ${ui.dim(`(${counts || "no executions"})`)}`;
148788
+ }
148789
+ async function pollExperimentUntilTerminal({
148790
+ client,
148791
+ url: url3,
148792
+ interval,
148793
+ maxWait,
148794
+ onTerminal,
148795
+ onDeadline
148796
+ }) {
148797
+ const deadline = Date.now() + maxWait * 1000;
148798
+ let lastSummary = "";
148799
+ while (true) {
148800
+ const result = await client.get(url3);
148801
+ const execs = result.executions ?? [];
148802
+ const summary = summarizeExperimentExecutions(execs);
148803
+ const line3 = renderTickLine(summary);
148804
+ if (line3 !== lastSummary) {
148805
+ if (process.stderr.isTTY) {
148806
+ process.stderr.write(`\r\x1B[K${line3}`);
148807
+ } else {
148808
+ process.stderr.write(`${line3}
148809
+ `);
148810
+ }
148811
+ lastSummary = line3;
148812
+ }
148813
+ if (summary.done) {
148814
+ if (process.stderr.isTTY)
148815
+ process.stderr.write(`
148816
+ `);
148817
+ const failed = summary.counts.failed ?? 0;
148818
+ const cancelled = summary.counts.cancelled ?? 0;
148819
+ const rejected = summary.counts.rejected ?? 0;
148820
+ const badCount = failed + cancelled + rejected;
148821
+ if (badCount > 0)
148822
+ renderExperimentFailures(execs, summary.total);
148823
+ await onTerminal(result, summary);
148824
+ if (badCount > 0)
148825
+ process.exit(1);
148826
+ return;
148827
+ }
148828
+ if (Date.now() >= deadline) {
148829
+ if (process.stderr.isTTY)
148830
+ process.stderr.write(`
148831
+ `);
148832
+ error51(`Timed out after ${maxWait}s with ${summary.total - summary.terminal} non-terminal execution(s). Re-run to keep watching.`);
148833
+ onDeadline?.(result, summary);
148834
+ process.exit(2);
148835
+ }
148836
+ await new Promise((r5) => setTimeout(r5, interval * 1000));
148837
+ }
148838
+ }
148667
148839
  function registerVersionsCommands(parent) {
148668
148840
  const versions2 = parent.command("versions").description("Inspect and restore historical workflow versions.");
148669
148841
  const listCmd = versions2.command("list <workflow-id>").description(descriptionFor("workflow_list_versions"));
@@ -145,6 +145,17 @@ to retry.
145
145
  eigenpal workflow experiment compare evb_old evb_new --regression-threshold 0.05
146
146
  ```
147
147
 
148
+ Output is two stacked tables: a per-evaluator aggregate (rows per evaluator
149
+ with mean Δ, regressions, improvements — sorted biggest mover first) followed
150
+ by a per-(example, evaluator) detail table. No need to write a Python
151
+ aggregator on top.
152
+
153
+ ```bash
154
+ # JSON exposes both layers — `summary.byEvaluator` for the rollup,
155
+ # `rows` for the full per-example detail.
156
+ eigenpal workflow experiment compare evb_old evb_new --json | jq '.summary.byEvaluator'
157
+ ```
158
+
148
159
  Unlike its siblings, `compare` takes no `<workflow-id>` — the server resolves
149
160
  the owning workflow from each batch id. Both batches must live in the
150
161
  same workflow within your tenant. See [`reference/debugging.md`](reference/debugging.md#6-compare-two-experiment-batches)
@@ -218,12 +229,64 @@ eigenpal workflow experiment run <workflow-id> # → { bat
218
229
 
219
230
  # Or restrict to one example:
220
231
  eigenpal workflow experiment run <workflow-id> --example-id evx_…
232
+ ```
233
+
234
+ **Recommended:** use `experiment watch` — it polls until terminal AND auto-pulls
235
+ results to disk in one command. Replaces the old "run → status (poll) → results"
236
+ chain so agents don't have to write a custom poller, parse `executions[].status`,
237
+ or chain `monitor + status + pull + score` shell logic.
238
+
239
+ ```bash
240
+ # One command: watch until terminal, then write ./results-<batchId>.json on completion.
241
+ eigenpal workflow experiment watch <workflow-id> <batch-id>
242
+
243
+ # CSV output:
244
+ eigenpal workflow experiment watch <workflow-id> <batch-id> --format csv
245
+
246
+ # Custom destination (overrides the default ./results-<batchId>.<format>):
247
+ eigenpal workflow experiment watch <workflow-id> <batch-id> --pull-on-complete ./out/r.json
248
+
249
+ # Just watch, skip the pull (e.g. you only need the live tick + exit code):
250
+ eigenpal workflow experiment watch <workflow-id> <batch-id> --no-pull
251
+ ```
221
252
 
222
- # Poll until done. `experiment status` auto-detaches after 30 min; just re-run.
223
- eigenpal workflow experiment status <workflow-id> <batch-id>
253
+ Exit codes match `experiment status --watch`: `0` clean, `1` any
254
+ failed/cancelled/rejected execution, `2` `--max-wait` deadline reached
255
+ (30 min default — re-run to keep watching).
224
256
 
225
- # Pull results — `--out` writes the file directly from the server's signed-URL
226
- # storage, so large CSVs don't go through the CLI process.
257
+ ### Monitoring scripts — use `--short` and the JSON rollup
258
+
259
+ When a script needs to spot-check a batch (no waiting), prefer one of the two
260
+ machine-readable paths instead of folding `executions[].status` yourself.
261
+
262
+ ```bash
263
+ # Single line on stdout, awk-friendly:
264
+ eigenpal workflow experiment status <wf> <batch> --short
265
+ # → 6/6 done failed=0 cancelled=0 rejected=0 (or "in-progress" while pending)
266
+
267
+ # Parse with awk:
268
+ DONE_STATE=$(eigenpal workflow experiment status <wf> <batch> --short | awk '{print $2}')
269
+ [ "$DONE_STATE" = "done" ] || exit 0 # not terminal yet, try again later
270
+ ```
271
+
272
+ ```bash
273
+ # JSON includes a top-level `summary` rollup so you don't count statuses by hand:
274
+ eigenpal workflow experiment status <wf> <batch> --json | jq '.summary'
275
+ # → { total: 6, terminal: 6, complete: true,
276
+ # completedCount: 5, failedCount: 1,
277
+ # cancelledCount: 0, rejectedCount: 0,
278
+ # runningCount: 0, pendingCount: 0 }
279
+
280
+ # `summary.complete` is the canonical poll-completion flag.
281
+ ```
282
+
283
+ `--short` and `--json` are mutually exclusive with `--watch` — for live
284
+ streaming use `experiment watch` instead.
285
+
286
+ If you need the raw eval-results export after watching with `--no-pull`, or
287
+ to refetch results outside a watch loop, fall back to:
288
+
289
+ ```bash
227
290
  eigenpal workflow experiment results <workflow-id> <batch-id> --format csv --out r.csv
228
291
  ```
229
292
 
@@ -433,10 +496,11 @@ eigenpal workflow clear-local # delete local execution artifacts
433
496
 
434
497
  # Experiments (batch eval runs)
435
498
  eigenpal workflow experiment run <workflow-id> [--example-id <id> ...]
436
- eigenpal workflow experiment status <workflow-id> <batch-id> [--watch] # exit 1 on terminal w/ failures, 2 on --max-wait
499
+ eigenpal workflow experiment status <workflow-id> <batch-id> [--watch] [--short] [--json] # exit 1 on terminal w/ failures, 2 on --max-wait. --json includes top-level `summary` rollup.
500
+ eigenpal workflow experiment watch <workflow-id> <batch-id> [--format csv|json] [--pull-on-complete <path>] [--no-pull] # poll + auto-pull results in one shot
437
501
  eigenpal workflow experiment results <workflow-id> [batch-id] --format {csv|json} --out r.csv
438
502
  eigenpal workflow experiment list <workflow-id>
439
- eigenpal workflow experiment compare <batch-a> <batch-b> [--regression-threshold] [--sort] # no --workflow-id
503
+ eigenpal workflow experiment compare <batch-a> <batch-b> [--regression-threshold] [--sort] # no --workflow-id; prints per-evaluator aggregate + per-row table
440
504
 
441
505
  # Step-type / evaluator-type introspection
442
506
  eigenpal workflow step-type list
@@ -38,6 +38,7 @@ Manage workflows: push, pull, run, evaluate.
38
38
  - [`eigenpal workflow experiment cancel [options] <workflow-id> <batchId>`](#eigenpal-workflow-experiment-cancel-options-workflow-id-batchid)
39
39
  - [`eigenpal workflow experiment results [options] <workflow-id> [batchId]`](#eigenpal-workflow-experiment-results-options-workflow-id-batchid)
40
40
  - [`eigenpal workflow experiment compare [options] <batchIdA> <batchIdB>`](#eigenpal-workflow-experiment-compare-options-batchida-batchidb)
41
+ - [`eigenpal workflow experiment watch [options] <workflow-id> <batchId>`](#eigenpal-workflow-experiment-watch-options-workflow-id-batchid)
41
42
  - [`eigenpal workflow execution run [options] <workflow-id> [examples...]`](#eigenpal-workflow-execution-run-options-workflow-id-examples)
42
43
  - [`eigenpal workflow execution get [options] <executionId>`](#eigenpal-workflow-execution-get-options-executionid)
43
44
  - [`eigenpal workflow execution list [options] <workflow-id>`](#eigenpal-workflow-execution-list-options-workflow-id)
@@ -79,7 +80,8 @@ workflow
79
80
  │ ├── status <workflow-id> <batchId>
80
81
  │ ├── cancel <workflow-id> <batchId>
81
82
  │ ├── results <workflow-id> [batchId]
82
- │ └── compare <batchIdA> <batchIdB>
83
+ │ ├── compare <batchIdA> <batchIdB>
84
+ │ └── watch <workflow-id> <batchId>
83
85
  ├── execution
84
86
  │ ├── run <workflow-id> [examples...]
85
87
  │ ├── get <executionId>
@@ -137,14 +139,15 @@ workflow
137
139
 
138
140
  ### Experiment
139
141
 
140
- | Command | Description |
141
- | ------------------------------------------------------------------------ | -------------------------------------------------- |
142
- | `eigenpal workflow experiment list [options] <workflow-id>` | List executions for the workflow, newest first. |
143
- | `eigenpal workflow experiment run [options] <workflow-id>` | Start a batch eval against the workflow's dataset. |
144
- | `eigenpal workflow experiment status [options] <workflow-id> <batchId>` | Aggregate progress for a batch by `batchId`. |
145
- | `eigenpal workflow experiment cancel [options] <workflow-id> <batchId>` | Cancel every execution in a batch. Idempotent. |
146
- | `eigenpal workflow experiment results [options] <workflow-id> [batchId]` | Download eval results in CSV or JSON. |
147
- | `eigenpal workflow experiment compare [options] <batchIdA> <batchIdB>` | Diff eval scores between two experiment batches. |
142
+ | Command | Description |
143
+ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
144
+ | `eigenpal workflow experiment list [options] <workflow-id>` | List executions for the workflow, newest first. |
145
+ | `eigenpal workflow experiment run [options] <workflow-id>` | Start a batch eval against the workflow's dataset. |
146
+ | `eigenpal workflow experiment status [options] <workflow-id> <batchId>` | Aggregate progress for a batch by `batchId`. |
147
+ | `eigenpal workflow experiment cancel [options] <workflow-id> <batchId>` | Cancel every execution in a batch. Idempotent. |
148
+ | `eigenpal workflow experiment results [options] <workflow-id> [batchId]` | Download eval results in CSV or JSON. |
149
+ | `eigenpal workflow experiment compare [options] <batchIdA> <batchIdB>` | Diff eval scores between two experiment batches. |
150
+ | `eigenpal workflow experiment watch [options] <workflow-id> <batchId>` | Poll until terminal, then auto-pull results — replaces `status --watch` + `results --out`. |
148
151
 
149
152
  ### Execution
150
153
 
@@ -508,14 +511,15 @@ Aggregate progress for a batch by `batchId`.
508
511
 
509
512
  ### Options
510
513
 
511
- | Flag | Required | Default | Description |
512
- | ---------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
513
- | `--watch` | no | `false` | Poll until every execution reaches a terminal state (completed/failed/cancelled/rejected), then exit |
514
- | `--interval <seconds>` | no | `5` | Poll interval in seconds when --watch is set (default 5) |
515
- | `--max-wait <seconds>` | no | `1800` | Hard ceiling for --watch in seconds (default 1800 = 30 min) |
516
- | `--include <kinds>` | no | `""` | Comma-separated extras to attach when --watch terminates: payload (full per-execution snapshot, can be hundreds of KB) |
517
- | `--base-url <url>` | no | | Server base URL |
518
- | `--json` | no | | Output the raw server response as JSON |
514
+ | Flag | Required | Default | Description |
515
+ | ---------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
516
+ | `--watch` | no | `false` | Poll until every execution reaches a terminal state (completed/failed/cancelled/rejected), then exit |
517
+ | `--short` | no | `false` | Single-line plain-text summary on stdout (e.g. `6/6 done failed=0 cancelled=0 rejected=0`). Pipe-friendly for monitoring loops; mutually exclusive with --watch. |
518
+ | `--interval <seconds>` | no | `5` | Poll interval in seconds when --watch is set (default 5) |
519
+ | `--max-wait <seconds>` | no | `1800` | Hard ceiling for --watch in seconds (default 1800 = 30 min) |
520
+ | `--include <kinds>` | no | `""` | Comma-separated extras to attach when --watch terminates: payload (full per-execution snapshot, can be hundreds of KB) |
521
+ | `--base-url <url>` | no | | Server base URL |
522
+ | `--json` | no | | Output the raw server response as JSON |
519
523
 
520
524
  ### `eigenpal workflow experiment cancel [options] <workflow-id> <batchId>`
521
525
 
@@ -575,6 +579,28 @@ Diff eval scores between two experiment batches.
575
579
  | `--base-url <url>` | no | | Server base URL |
576
580
  | `--json` | no | | Output the raw server response as JSON |
577
581
 
582
+ ### `eigenpal workflow experiment watch [options] <workflow-id> <batchId>`
583
+
584
+ Poll until terminal, then auto-pull results — replaces `status --watch` + `results --out`.
585
+
586
+ ### Arguments
587
+
588
+ | Name | Required | Variadic | Description |
589
+ | ------------- | -------- | -------- | ----------- |
590
+ | `workflow-id` | yes | no | |
591
+ | `batchId` | yes | no | |
592
+
593
+ ### Options
594
+
595
+ | Flag | Required | Default | Description |
596
+ | --------------------------- | -------- | -------- | ----------------------------------------------------------------------- |
597
+ | `--interval <seconds>` | no | `5` | Poll interval in seconds (default 5) |
598
+ | `--max-wait <seconds>` | no | `1800` | Hard ceiling in seconds (default 1800 = 30 min) |
599
+ | `--pull-on-complete <path>` | no | | Destination for the results file. Default: ./results-<batchId>.<format> |
600
+ | `--format <csv\|json>` | no | `"json"` | Results export format (default json) |
601
+ | `--no-pull` | no | | Skip auto-pulling results on terminal (just watch) |
602
+ | `--base-url <url>` | no | | Server base URL |
603
+
578
604
  ### `eigenpal workflow execution run [options] <workflow-id> [examples...]`
579
605
 
580
606
  Run a saved workflow against local dataset examples.
@@ -153,7 +153,6 @@ Extract structured data from text using AI with a JSON schema
153
153
  | `prompt` | string | no | | Custom prompt template for extraction |
154
154
  | `provider` | string | no | | Provider ID (e.g., "openai-gpt4o") |
155
155
  | `model` | string | no | | Model override |
156
- | `temperature` | number | no | `0` | Model temperature (0 = deterministic) |
157
156
  | `maxInputTokens` | integer | no | | Max input tokens. Truncates input text and logs a warning when exceeded. Omit for no limit. |
158
157
 
159
158
  **Output:** `record<string, unknown>`
@@ -387,6 +387,7 @@ _Generated from `WorkflowDefinitionSchema` in `@eigenpal/types/src/workflow/work
387
387
  | `steps` | array<unknown> | yes | | |
388
388
  | `output` | record<string, string> | no | | |
389
389
  | `settings` | object | no | | |
390
+ | `defaultModel` | string | no | | |
390
391
 
391
392
 
392
393
  ## Per-input fields
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eigenpal/cli",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
5
5
  "keywords": [
6
6
  "eigenpal",