@eigenpal/cli 0.4.8 → 0.4.10

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.
@@ -201,6 +211,25 @@
201
211
 
202
212
  `@` path syntax (`--code @file.js`, `--inputs items=@arr.json`) matches `curl -d @file` / `gh --body-file -`. Bare paths and `-` (stdin) work too.
203
213
 
214
+ - 4137eb8: custom-script evaluator: the YAML now carries the entire
215
+ `function scoreScript(expected, actual) { ... }` declaration in a single
216
+ `function` field. Previous formats (`code`, then `body`) wrapped the
217
+ text implicitly; the new shape stores the function declaration verbatim
218
+ so the YAML matches the dashboard editor 1-for-1.
219
+
220
+ `workflow validate` and `workflow evaluators validate` flag YAML where
221
+ the function fails to parse, the signature does not match
222
+ `function scoreScript(expected, actual)` exactly, or the body contains
223
+ neither `return` nor `throw` — same checks the dashboard runs at form
224
+ save. The function must return a number in [0, 1]; throws are caught
225
+ and scored as 0.
226
+
227
+ Existing evaluator YAML must be ported by hand: rename `body` (or
228
+ `code`) to `function`, and add the `function scoreScript(expected,
229
+ actual) { ... }` declaration around your statements. Parameter order is
230
+ load-bearing — swapping `expected` and `actual` would silently invert
231
+ every score.
232
+
204
233
  - 3da0612: All list commands now support `--limit` / `--offset` pagination consistently.
205
234
  - CLI: `workflow execution list`, `workflow version list`, and `workflow step-type list` previously had no `--offset` (or no pagination at all). All six list commands now accept `--limit`/`--offset` with defaults of 50/0.
206
235
  - Server: `GET /api/workflows/:id/versions` now reads `?limit=` and `?offset=` query params (defaults 50/0; max limit 100). The `listVersions` repo method gained an optional `{ limit, offset }` argument.
@@ -273,6 +302,11 @@
273
302
 
274
303
  Interactive profiles are unchanged: when you log in via `eigenpal auth login`, the profile stores both apiKey and baseUrl, and every subsequent command derives baseUrl from that profile. Set `EIGENPAL_PROFILE=<name>` to switch profiles per-shell without modifying state.
275
304
 
305
+ - 542fe0e: CLI: surface evaluator entry-level fields (`name`, `description`, `weight`) so agents and humans can discover the optional `description` field on every evaluator.
306
+ - `workflow evaluator-type get <type>` now emits `entrySchema` alongside `configSchema`. Inspect with `eigenpal workflow evaluator-type get llm-judge | jq '.entrySchema'`.
307
+ - The skill reference now renders a "Common entry fields" table per type and a "Writing descriptions for stakeholders" section explaining the convention: each evaluator's `description` should be a one-sentence, plain-language summary aimed at non-technical reviewers in the dashboard.
308
+ - `init` templates `pdf-extraction` and `text-classification` now ship a `description:` line on their evaluator entries.
309
+
276
310
  - d41ee8b: CLI: fix `eigenpal status` error UX + harden `formatCliError` for real-world connection failures.
277
311
  - `eigenpal status` against an unreachable base URL no longer mis-diagnoses the failure as an auth problem. Previously, `EIGENPAL_BASE_URL=http://localhost:9999 eigenpal status` printed "Authentication check failed; try `eigenpal auth login`" — the wrong rabbit hole. The status command now rethrows non-auth errors (connection refused, DNS NXDOMAIN, timeout, HTML responses, 5xx) so `formatCliError` upstream can surface the right hint: `Could not connect to the server at <url> ... Set EIGENPAL_BASE_URL or pass --base-url <url>.` Only real 401/403 responses are treated as auth failures.
278
312
  - `formatCliError` now detects connection failures wrapped in any Error shape — bare `Error('ENOTFOUND ...')`, `Error` with `code: 'ECONNREFUSED'`, and `TypeError('fetch failed')` whose `.cause` carries the underlying error. Previously only `TypeError` whose message contained `fetch`/`URL` was recognized, so `getaddrinfo`-style errors fell through to the generic raw-message branch and lost the URL context.
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(),
@@ -23325,6 +23327,141 @@ var init_dataset_archive = __esm(() => {
23325
23327
  };
23326
23328
  });
23327
23329
 
23330
+ // ../types/src/eval/score-function.ts
23331
+ function stripLeadingTrivia(text) {
23332
+ let i = 0;
23333
+ while (i < text.length) {
23334
+ const ch = text[i];
23335
+ if (ch === " " || ch === "\t" || ch === `
23336
+ ` || ch === "\r" || ch === "\v" || ch === "\f") {
23337
+ i++;
23338
+ continue;
23339
+ }
23340
+ if (ch === "/" && text[i + 1] === "/") {
23341
+ const nl = text.indexOf(`
23342
+ `, i + 2);
23343
+ if (nl === -1)
23344
+ return "";
23345
+ i = nl + 1;
23346
+ continue;
23347
+ }
23348
+ if (ch === "/" && text[i + 1] === "*") {
23349
+ const end = text.indexOf("*/", i + 2);
23350
+ if (end === -1)
23351
+ break;
23352
+ i = end + 2;
23353
+ continue;
23354
+ }
23355
+ break;
23356
+ }
23357
+ return text.slice(i);
23358
+ }
23359
+ function hasValidScoreSignature(fn) {
23360
+ return STRICT_HEADER_REGEX.test(stripLeadingTrivia(fn));
23361
+ }
23362
+ function isParseableScoreFunction(fn) {
23363
+ try {
23364
+ new Function(fn);
23365
+ return true;
23366
+ } catch {
23367
+ return false;
23368
+ }
23369
+ }
23370
+ function getScoreFunctionRuntimeIssue(fn) {
23371
+ if (/\basync\s+function\s+scoreScript\b/.test(fn)) {
23372
+ return "scoreScript must be synchronous (drop the `async` keyword — the sandbox calls it sync, so a Promise return scores 0)";
23373
+ }
23374
+ if (/\bimport\s*\(/.test(fn)) {
23375
+ return "dynamic `import(...)` is not supported in the score sandbox";
23376
+ }
23377
+ if (/\brequire\s*\(/.test(fn)) {
23378
+ return "`require(...)` is not available in the score sandbox (no Node module system)";
23379
+ }
23380
+ return null;
23381
+ }
23382
+ function extractScoreFunctionBody(fn) {
23383
+ const headerMatch = fn.match(ANY_HEADER_REGEX);
23384
+ if (!headerMatch || headerMatch.index === undefined) {
23385
+ return { body: fn, wrapperOk: false };
23386
+ }
23387
+ const start = headerMatch.index + headerMatch[0].length;
23388
+ let depth = 1;
23389
+ let i = start;
23390
+ let inLine = false;
23391
+ let inBlock = false;
23392
+ let strQuote = null;
23393
+ while (i < fn.length) {
23394
+ const ch = fn[i];
23395
+ const next = fn[i + 1];
23396
+ if (inLine) {
23397
+ if (ch === `
23398
+ `)
23399
+ inLine = false;
23400
+ } else if (inBlock) {
23401
+ if (ch === "*" && next === "/") {
23402
+ inBlock = false;
23403
+ i++;
23404
+ }
23405
+ } else if (strQuote) {
23406
+ if (ch === "\\")
23407
+ i++;
23408
+ else if (ch === strQuote)
23409
+ strQuote = null;
23410
+ } else if (ch === "/" && next === "/") {
23411
+ inLine = true;
23412
+ i++;
23413
+ } else if (ch === "/" && next === "*") {
23414
+ inBlock = true;
23415
+ i++;
23416
+ } else if (ch === '"' || ch === "'" || ch === "`") {
23417
+ strQuote = ch;
23418
+ } else if (ch === "{") {
23419
+ depth++;
23420
+ } else if (ch === "}") {
23421
+ depth--;
23422
+ if (depth === 0) {
23423
+ const inner = fn.slice(start, i).replace(/^\n+|\n+$/g, "");
23424
+ return { body: inner, wrapperOk: true };
23425
+ }
23426
+ }
23427
+ i++;
23428
+ }
23429
+ return { body: fn.slice(start), wrapperOk: false };
23430
+ }
23431
+ function defaultScoreFunction() {
23432
+ return `/**
23433
+ * Score the workflow output against the example's expected output.
23434
+ *
23435
+ * @param {Expected} expected the example's expected output
23436
+ * @param {Actual} actual the workflow's actual output
23437
+ * @returns {number} a score in [0, 1]; throws are caught and scored as 0
23438
+ */
23439
+ function scoreScript(expected, actual) {
23440
+ // Return a number in [0, 1]; throws are caught and scored as 0.
23441
+ return JSON.stringify(actual) === JSON.stringify(expected) ? 1 : 0;
23442
+ }
23443
+ `;
23444
+ }
23445
+ function wrapBodyAsScoreFunction(body) {
23446
+ const trimmed = body.replace(/^\n+|\n+$/g, "");
23447
+ return `/**
23448
+ * Score the workflow output against the example's expected output.
23449
+ *
23450
+ * @param {Expected} expected the example's expected output
23451
+ * @param {Actual} actual the workflow's actual output
23452
+ * @returns {number} a score in [0, 1]; throws are caught and scored as 0
23453
+ */
23454
+ function scoreScript(expected, actual) {
23455
+ ${trimmed}
23456
+ }
23457
+ `;
23458
+ }
23459
+ var STRICT_HEADER_REGEX, ANY_HEADER_REGEX;
23460
+ var init_score_function = __esm(() => {
23461
+ STRICT_HEADER_REGEX = /^(?:async\s+)?function\s+scoreScript\s*\(\s*expected\s*,\s*actual\s*\)\s*\{/;
23462
+ ANY_HEADER_REGEX = /function\s+scoreScript\s*\([^)]*\)\s*\{/;
23463
+ });
23464
+
23328
23465
  // ../types/src/eval/evaluator-config.ts
23329
23466
  function combineEvalScores(results) {
23330
23467
  let numerator = 0;
@@ -23339,9 +23476,10 @@ function combineEvalScores(results) {
23339
23476
  return null;
23340
23477
  return numerator / denominator;
23341
23478
  }
23342
- var EvaluatorTypeSchema, ScoreInUnitInterval, LLM_JUDGE_PROMPT_MIN_LENGTH = 10, ExactDiffConfigSchema, JudgeModeSchema, LabelsRecord, LlmJudgeConfigSchema, CustomScriptConfigSchema, EvaluatorBaseEntrySchema, EvaluatorConfigSchema, EvalConfigYamlSchema, EvaluatorResultSchema;
23479
+ var EvaluatorTypeSchema, ScoreInUnitInterval, LLM_JUDGE_PROMPT_MIN_LENGTH = 10, ExactDiffConfigSchema, JudgeModeSchema, LabelsRecord, LlmJudgeConfigSchema, CUSTOM_SCRIPT_CONTRACT, CUSTOM_SCRIPT_MAX_BYTES = 1e4, CUSTOM_SCRIPT_MAX_TIMEOUT_MS = 30000, CUSTOM_SCRIPT_MAX_MEMORY_MB = 50, CUSTOM_SCRIPT_FUNCTION_DESCRIPTION, CustomScriptConfigSchema, EvaluatorBaseEntrySchema, EvaluatorConfigSchema, EvalConfigYamlSchema, EvaluatorResultSchema;
23343
23480
  var init_evaluator_config = __esm(() => {
23344
23481
  init_zod();
23482
+ init_score_function();
23345
23483
  EvaluatorTypeSchema = exports_external.enum(["exact-diff", "llm-judge", "custom-script"]);
23346
23484
  ScoreInUnitInterval = exports_external.number().min(0, { message: "score must be >= 0" }).max(1, { message: "score must be <= 1" });
23347
23485
  ExactDiffConfigSchema = exports_external.object({
@@ -23370,14 +23508,41 @@ var init_evaluator_config = __esm(() => {
23370
23508
  }
23371
23509
  }
23372
23510
  });
23511
+ CUSTOM_SCRIPT_CONTRACT = {
23512
+ expected: "the example's expected output",
23513
+ actual: "the workflow's actual output",
23514
+ returns: "a number in [0, 1]",
23515
+ throws: "caught and scored as 0"
23516
+ };
23517
+ CUSTOM_SCRIPT_FUNCTION_DESCRIPTION = `Full JavaScript declaration of \`function scoreScript(expected, actual) { ... }\`. ` + `Receives \`expected\` (${CUSTOM_SCRIPT_CONTRACT.expected}) ` + `and \`actual\` (${CUSTOM_SCRIPT_CONTRACT.actual}), returns ${CUSTOM_SCRIPT_CONTRACT.returns}. ` + `Throws are ${CUSTOM_SCRIPT_CONTRACT.throws}.`;
23373
23518
  CustomScriptConfigSchema = exports_external.object({
23374
- code: exports_external.string().min(1, { message: "code required" }).describe("Sandboxed JavaScript. Receives `actual`, `expected`, `input` as variables and must return { score, label? } where score is in [0, 1]."),
23375
- timeoutMs: exports_external.number().int().positive().default(5000).describe("Maximum wall-clock time the script can run before it's killed and the run is marked failed."),
23376
- memoryLimitMb: exports_external.number().int().positive().default(10).describe("Maximum memory the sandbox may allocate. Increase if the script processes large arrays or strings."),
23519
+ function: exports_external.string().min(1, { message: "required" }).max(CUSTOM_SCRIPT_MAX_BYTES, {
23520
+ message: `must be ≤ ${CUSTOM_SCRIPT_MAX_BYTES} bytes`
23521
+ }).describe(CUSTOM_SCRIPT_FUNCTION_DESCRIPTION).refine(isParseableScoreFunction, {
23522
+ message: "must parse as JavaScript"
23523
+ }).refine(hasValidScoreSignature, {
23524
+ message: "must declare `function scoreScript(expected, actual) { ... }` (parameter names + order matter)"
23525
+ }).refine((fn) => {
23526
+ const { body, wrapperOk } = extractScoreFunctionBody(fn);
23527
+ return wrapperOk && /\b(return|throw)\b/.test(body);
23528
+ }, {
23529
+ message: "function body must `return` a number in [0, 1] (or `throw` to score 0)"
23530
+ }).superRefine((fn, ctx) => {
23531
+ const issue2 = getScoreFunctionRuntimeIssue(fn);
23532
+ if (issue2)
23533
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, message: issue2 });
23534
+ }),
23535
+ timeoutMs: exports_external.number().int().positive().max(CUSTOM_SCRIPT_MAX_TIMEOUT_MS, {
23536
+ message: `must be ≤ ${CUSTOM_SCRIPT_MAX_TIMEOUT_MS} ms`
23537
+ }).default(5000).describe("Maximum wall-clock time the script can run before it's killed and the run is marked failed."),
23538
+ memoryLimitMb: exports_external.number().int().positive().max(CUSTOM_SCRIPT_MAX_MEMORY_MB, {
23539
+ message: `must be ≤ ${CUSTOM_SCRIPT_MAX_MEMORY_MB} MB`
23540
+ }).default(10).describe("Maximum memory the sandbox may allocate. Increase if the script processes large arrays or strings."),
23377
23541
  passThreshold: ScoreInUnitInterval.default(1).describe("Minimum script score this evaluator must reach for a run to pass.")
23378
23542
  });
23379
23543
  EvaluatorBaseEntrySchema = exports_external.object({
23380
23544
  name: exports_external.string().min(1).max(64).regex(/^[a-z0-9-]+$/, "name must be lowercase kebab-case").describe("Identifier used in eval results and the weighted-mean formula. Use a short kebab-case slug like header-exact or covenants-recall."),
23545
+ description: exports_external.string().trim().max(500).optional().describe("One-sentence explanation, in business language, of what this evaluator measures and why a stakeholder should care. Shown in the dashboard to non-technical reviewers (legal, ops, finance) who never see the YAML or judge prompt. Skip implementation detail (model names, thresholds, dot-paths); those live in `config`."),
23381
23546
  weight: exports_external.number().min(0).default(1).describe("Relative importance in the weighted mean. The overall score is Σ(weightᵢ × scoreᵢ) / Σweight. Set to 0 to score-only without affecting overall.")
23382
23547
  });
23383
23548
  EvaluatorConfigSchema = exports_external.discriminatedUnion("type", [
@@ -23463,8 +23628,7 @@ async function judgeScoreJson(params) {
23463
23628
  const response = await client.extract(userContent, {
23464
23629
  schema: SCORE_SCHEMA,
23465
23630
  prompt: systemPrompt,
23466
- model,
23467
- temperature: 0
23631
+ model
23468
23632
  });
23469
23633
  const rawScore = response.data.score;
23470
23634
  const score = typeof rawScore === "number" ? rawScore : typeof rawScore === "string" ? parseFloat(rawScore) : NaN;
@@ -23484,8 +23648,7 @@ async function judgeLabelJson(params) {
23484
23648
  const response = await client.extract(userContent, {
23485
23649
  schema: buildLabelSchema(labels),
23486
23650
  prompt: systemPrompt,
23487
- model,
23488
- temperature: 0
23651
+ model
23489
23652
  });
23490
23653
  const label = typeof response.data.label === "string" ? response.data.label.trim() : "";
23491
23654
  if (!labels.includes(label)) {
@@ -23569,6 +23732,7 @@ var init_eval = __esm(() => {
23569
23732
  init_dataset_archive();
23570
23733
  init_evaluator_config();
23571
23734
  init_judge();
23735
+ init_score_function();
23572
23736
  init_workflow_tool_metadata();
23573
23737
  });
23574
23738
 
@@ -23637,6 +23801,7 @@ var init_paths = __esm(() => {
23637
23801
  // ../types/src/index.ts
23638
23802
  var exports_src = {};
23639
23803
  __export(exports_src, {
23804
+ wrapBodyAsScoreFunction: () => wrapBodyAsScoreFunction,
23640
23805
  workflowTriggerConfigSchema: () => workflowTriggerConfigSchema,
23641
23806
  valuesMatch: () => valuesMatch,
23642
23807
  validateStepConfig: () => validateStepConfig,
@@ -23667,10 +23832,12 @@ __export(exports_src, {
23667
23832
  isValidJudgeModel: () => isValidJudgeModel,
23668
23833
  isTerminalExecutionStatus: () => isTerminalExecutionStatus,
23669
23834
  isStepCategory: () => isStepCategory,
23835
+ isParseableScoreFunction: () => isParseableScoreFunction,
23670
23836
  isOutputSchemaUnknown: () => isOutputSchemaUnknown,
23671
23837
  isNonSuccessTerminalExecutionStatus: () => isNonSuccessTerminalExecutionStatus,
23672
23838
  isFilePathDescriptor: () => isFilePathDescriptor,
23673
23839
  isFileOutput: () => isFileOutput,
23840
+ hasValidScoreSignature: () => hasValidScoreSignature,
23674
23841
  hasTriggerMethod: () => hasTriggerMethod,
23675
23842
  goldenFileSatisfiedInResult: () => goldenFileSatisfiedInResult,
23676
23843
  getTriggerMethods: () => getTriggerMethods,
@@ -23678,6 +23845,7 @@ __export(exports_src, {
23678
23845
  getStepSchema: () => getStepSchema,
23679
23846
  getStepOutputSchema: () => getStepOutputSchema,
23680
23847
  getStepConfigSchema: () => getStepConfigSchema,
23848
+ getScoreFunctionRuntimeIssue: () => getScoreFunctionRuntimeIssue,
23681
23849
  getRequiredOutputKeys: () => getRequiredOutputKeys,
23682
23850
  getProcessorSchemas: () => getProcessorSchemas,
23683
23851
  getParserCategory: () => getParserCategory,
@@ -23693,11 +23861,13 @@ __export(exports_src, {
23693
23861
  formatDiffValue: () => formatDiffValue,
23694
23862
  fileFieldNamesFromJsonSchema: () => fileFieldNamesFromJsonSchema,
23695
23863
  fieldStemFromGoldenName: () => fieldStemFromGoldenName,
23864
+ extractScoreFunctionBody: () => extractScoreFunctionBody,
23696
23865
  extractFieldMetadata: () => extractFieldMetadata,
23697
23866
  executionWallClockMs: () => executionWallClockMs,
23698
23867
  evalInputKey: () => evalInputKey,
23699
23868
  evalExpectedKey: () => evalExpectedKey,
23700
23869
  errorHintFor: () => errorHintFor,
23870
+ defaultScoreFunction: () => defaultScoreFunction,
23701
23871
  compareOutput: () => compareOutput,
23702
23872
  compareExpectedValues: () => compareExpectedValues,
23703
23873
  combineEvalScores: () => combineEvalScores,
@@ -23870,6 +24040,7 @@ __export(exports_src, {
23870
24040
  DatasetImportRequestSchema: () => DatasetImportRequestSchema,
23871
24041
  DatasetImportModeSchema: () => DatasetImportModeSchema,
23872
24042
  DURABLE_EXECUTION_TYPES: () => DURABLE_EXECUTION_TYPES,
24043
+ DETERMINISTIC_TEMPERATURE: () => DETERMINISTIC_TEMPERATURE,
23873
24044
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
23874
24045
  DEFAULT_CONCURRENCY: () => DEFAULT_CONCURRENCY,
23875
24046
  DATASET_NAME_PATTERN: () => DATASET_NAME_PATTERN,
@@ -23891,6 +24062,10 @@ __export(exports_src, {
23891
24062
  ControlApprovalOutputSchema: () => ControlApprovalOutputSchema,
23892
24063
  ControlApprovalConfigSchema: () => ControlApprovalConfigSchema,
23893
24064
  ConnectorSchema: () => ConnectorSchema,
24065
+ CUSTOM_SCRIPT_MAX_TIMEOUT_MS: () => CUSTOM_SCRIPT_MAX_TIMEOUT_MS,
24066
+ CUSTOM_SCRIPT_MAX_MEMORY_MB: () => CUSTOM_SCRIPT_MAX_MEMORY_MB,
24067
+ CUSTOM_SCRIPT_MAX_BYTES: () => CUSTOM_SCRIPT_MAX_BYTES,
24068
+ CUSTOM_SCRIPT_CONTRACT: () => CUSTOM_SCRIPT_CONTRACT,
23894
24069
  BoundingRegionSchema: () => BoundingRegionSchema,
23895
24070
  BlockStepSchema: () => BlockStepSchema,
23896
24071
  BaseEntitySchema: () => BaseEntitySchema,
@@ -143405,7 +143580,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
143405
143580
  // package.json
143406
143581
  var package_default = {
143407
143582
  name: "@eigenpal/cli",
143408
- version: "0.4.8",
143583
+ version: "0.4.10",
143409
143584
  description: "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
143410
143585
  keywords: [
143411
143586
  "eigenpal",
@@ -146148,26 +146323,23 @@ function getExampleNames(dir, exampleNamesFilter) {
146148
146323
  function readInput(exampleDir) {
146149
146324
  const argsPath = join6(exampleDir, "input", "arguments.json");
146150
146325
  const inputDir = join6(exampleDir, "input");
146151
- const merged = JSON.parse(readFileSync5(argsPath, "utf-8"));
146326
+ const scalars = JSON.parse(readFileSync5(argsPath, "utf-8"));
146327
+ const files = [];
146152
146328
  for (const entry of readdirSync3(inputDir)) {
146153
146329
  const full = join6(inputDir, entry);
146154
146330
  if (!statSync2(full).isDirectory())
146155
146331
  continue;
146156
- const files = readdirSync3(full).filter((f) => statSync2(join6(full, f)).isFile()).sort();
146157
- if (files.length === 0)
146158
- continue;
146159
- const inlined = files.map((filename) => {
146160
- const buf = readFileSync5(join6(full, filename));
146161
- const mime = guessMimeType(filename);
146162
- return {
146163
- _inline: buf.toString("base64"),
146332
+ const filenames = readdirSync3(full).filter((f) => statSync2(join6(full, f)).isFile()).sort();
146333
+ for (const filename of filenames) {
146334
+ files.push({
146335
+ argument: entry,
146164
146336
  filename,
146165
- ...mime ? { mimeType: mime } : {}
146166
- };
146167
- });
146168
- merged[entry] = files.length === 1 ? inlined[0] : inlined;
146337
+ content: readFileSync5(join6(full, filename)),
146338
+ mimeType: guessMimeType(filename)
146339
+ });
146340
+ }
146169
146341
  }
146170
- return merged;
146342
+ return { scalars, files };
146171
146343
  }
146172
146344
  function readOverrides(exampleDir) {
146173
146345
  const metaPath = join6(exampleDir, "meta.json");
@@ -146181,8 +146353,10 @@ function readOverrides(exampleDir) {
146181
146353
  return null;
146182
146354
  }
146183
146355
  function buildExamplePayload(exampleDir) {
146356
+ const { scalars, files } = readInput(exampleDir);
146184
146357
  return {
146185
- input: readInput(exampleDir),
146358
+ scalars,
146359
+ files,
146186
146360
  overrides: readOverrides(exampleDir)
146187
146361
  };
146188
146362
  }
@@ -146664,11 +146838,19 @@ async function runExec(client, dir, workflowIdOrSlug, exampleNames, options8 = {
146664
146838
  progress.setRunning(index);
146665
146839
  else
146666
146840
  console.log(`${ui.dim("→")} ${name}`);
146667
- const res = await client.post(`/api/v1/workflows/${workflowId}/run`, {
146668
- input: example.input,
146669
- overrides: example.overrides ?? undefined,
146670
- trigger: "cli"
146671
- });
146841
+ const form = new FormData;
146842
+ const sidecar = { trigger: "cli" };
146843
+ if (Object.keys(example.scalars).length > 0)
146844
+ sidecar.input = example.scalars;
146845
+ if (example.overrides)
146846
+ sidecar.overrides = example.overrides;
146847
+ form.append("_json", JSON.stringify(sidecar));
146848
+ for (const file2 of example.files) {
146849
+ form.append(file2.argument, new Blob([file2.content], {
146850
+ type: file2.mimeType ?? "application/octet-stream"
146851
+ }), file2.filename);
146852
+ }
146853
+ const res = await client.postFormData(`/api/v1/workflows/${workflowId}/run`, form);
146672
146854
  if (typeof res?.executionId !== "string") {
146673
146855
  throw new Error("Run API did not return executionId");
146674
146856
  }
@@ -147463,7 +147645,7 @@ var EVALUATOR_REGISTRY = {
147463
147645
  "custom-script": {
147464
147646
  type: "custom-script",
147465
147647
  name: "Custom Script",
147466
- description: "Run sandboxed JavaScript that returns { score, label? }. Receives `actual`, `expected`, `input`.",
147648
+ description: "Score the workflow output with sandboxed JavaScript. Authors a `function scoreScript(expected, actual) { ... }` declaration that returns a number in [0, 1].",
147467
147649
  configSchema: CustomScriptConfigSchema
147468
147650
  }
147469
147651
  };
@@ -147505,6 +147687,7 @@ function registerEvaluatorTypeCommands(parent) {
147505
147687
  type: def.type,
147506
147688
  name: def.name,
147507
147689
  description: def.description,
147690
+ entrySchema: z12.toJSONSchema(EvaluatorBaseEntrySchema, { target: "draft-7" }),
147508
147691
  configSchema: z12.toJSONSchema(def.configSchema, { target: "draft-7" })
147509
147692
  };
147510
147693
  const text = JSON.stringify(result, null, 2);
@@ -147595,10 +147778,47 @@ function buildBatchDiff(args) {
147595
147778
  sharedExamples: sharedExampleSet.size,
147596
147779
  onlyInA,
147597
147780
  onlyInB,
147598
- regressionThreshold
147781
+ regressionThreshold,
147782
+ byEvaluator: aggregateByEvaluator(rows)
147599
147783
  }
147600
147784
  };
147601
147785
  }
147786
+ function aggregateByEvaluator(rows) {
147787
+ const buckets = new Map;
147788
+ for (const r5 of rows) {
147789
+ const list = buckets.get(r5.evaluator);
147790
+ if (list)
147791
+ list.push(r5);
147792
+ else
147793
+ buckets.set(r5.evaluator, [r5]);
147794
+ }
147795
+ const aggregates = [];
147796
+ for (const [evaluator, group2] of buckets) {
147797
+ const comparable = group2.filter((r5) => r5.delta != null);
147798
+ const meanDelta = comparable.length === 0 ? null : comparable.reduce((sum, r5) => sum + r5.delta, 0) / comparable.length;
147799
+ aggregates.push({
147800
+ evaluator,
147801
+ comparable: comparable.length,
147802
+ meanDelta,
147803
+ regressions: group2.filter((r5) => r5.status === "regression").length,
147804
+ improvements: group2.filter((r5) => r5.status === "improvement").length,
147805
+ unchanged: group2.filter((r5) => r5.status === "unchanged").length
147806
+ });
147807
+ }
147808
+ aggregates.sort((a5, b8) => {
147809
+ if (a5.meanDelta == null && b8.meanDelta == null)
147810
+ return a5.evaluator.localeCompare(b8.evaluator);
147811
+ if (a5.meanDelta == null)
147812
+ return 1;
147813
+ if (b8.meanDelta == null)
147814
+ return -1;
147815
+ const diff = Math.abs(b8.meanDelta) - Math.abs(a5.meanDelta);
147816
+ if (diff !== 0)
147817
+ return diff;
147818
+ return a5.evaluator.localeCompare(b8.evaluator);
147819
+ });
147820
+ return aggregates;
147821
+ }
147602
147822
  function sortDiffRows(rows, sort) {
147603
147823
  const tiebreak = (a5, b8) => {
147604
147824
  const exCmp = a5.example.localeCompare(b8.example);
@@ -147684,10 +147904,30 @@ function renderBatchDiffHuman(diff) {
147684
147904
  }
147685
147905
  process.stderr.write(`
147686
147906
  `);
147907
+ if (diff.summary.byEvaluator.length > 0) {
147908
+ process.stderr.write(`${ui.bold("Per-evaluator deltas")} ${ui.dim("(B − A):")}
147909
+ `);
147910
+ console.log(table(diff.summary.byEvaluator, [
147911
+ { key: "evaluator", header: "evaluator" },
147912
+ { key: "comparable", header: "rows", align: "right" },
147913
+ {
147914
+ key: "meanDelta",
147915
+ header: "mean Δ",
147916
+ align: "right",
147917
+ format: (v11) => formatDelta(v11)
147918
+ },
147919
+ { key: "regressions", header: "regressions", align: "right" },
147920
+ { key: "improvements", header: "improvements", align: "right" }
147921
+ ]));
147922
+ process.stderr.write(`
147923
+ `);
147924
+ }
147687
147925
  if (diff.rows.length === 0) {
147688
147926
  process.stderr.write(`${ui.dim("No shared (example, evaluator) pairs to compare.")}
147689
147927
  `);
147690
147928
  } else {
147929
+ process.stderr.write(`${ui.bold("Per-row deltas:")}
147930
+ `);
147691
147931
  console.log(table(diff.rows, [
147692
147932
  { key: "example", header: "example" },
147693
147933
  { key: "evaluator", header: "evaluator" },
@@ -147999,7 +148239,7 @@ flag would otherwise shadow it. Validation failures come back as
147999
148239
  error51(`Cannot update workflow ${ui.bold(workflowId)} — pass --bump <patch|minor|major> or --set-version <X.Y.Z>, or add a top-level \`version: X.Y.Z\` to ${filePath}.`);
148000
148240
  process.exit(1);
148001
148241
  }
148002
- const yamlToSend = cliWantsRewrite ? spliceWorkflowVersion(yamlOnDisk, nextVersion) : yamlOnDisk;
148242
+ const yamlToSend = cliWantsRewrite ? spliceWorkflowVersion2(yamlOnDisk, nextVersion) : yamlOnDisk;
148003
148243
  const result = await client.post(`/api/v1/workflows/${workflowId}/versions`, {
148004
148244
  yaml: yamlToSend,
148005
148245
  version: nextVersion
@@ -148013,7 +148253,7 @@ flag would otherwise shadow it. Validation failures come back as
148013
148253
  if (opts.bump) {
148014
148254
  throw new Error("--bump requires --workflow-id (need a current version to bump from). For a new workflow, use --set-version <X.Y.Z> or omit both.");
148015
148255
  }
148016
- const yamlToSend = opts.setVersion && !extractWorkflowVersion(yamlOnDisk) ? spliceWorkflowVersion(yamlOnDisk, opts.setVersion) : yamlOnDisk;
148256
+ const yamlToSend = opts.setVersion && !extractWorkflowVersion(yamlOnDisk) ? spliceWorkflowVersion2(yamlOnDisk, opts.setVersion) : yamlOnDisk;
148017
148257
  const result = await client.post("/api/v1/workflows", { yaml: yamlToSend });
148018
148258
  if (opts.json) {
148019
148259
  printJson(result);
@@ -148040,7 +148280,7 @@ function bumpSemver(current, level) {
148040
148280
  return `${major}.${minor + 1}.0`;
148041
148281
  return `${major}.${minor}.${patch + 1}`;
148042
148282
  }
148043
- function spliceWorkflowVersion(yaml, next) {
148283
+ function spliceWorkflowVersion2(yaml, next) {
148044
148284
  if (/^version:\s*['"]?(\d+\.\d+\.\d+)['"]?\s*$/m.test(yaml)) {
148045
148285
  return yaml.replace(/^version:.*$/m, `version: ${next}`);
148046
148286
  }
@@ -148444,7 +148684,7 @@ returns immediately with \`{ batchId, total }\`; poll
148444
148684
  return;
148445
148685
  }
148446
148686
  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`)}
148687
+ process.stderr.write(` ${ui.dim(`Watch + auto-pull: eigenpal workflow experiment watch ${workflowId} ${start.batchId}`)}
148448
148688
  `);
148449
148689
  return;
148450
148690
  }
@@ -148461,20 +148701,32 @@ returns immediately with \`{ batchId, total }\`; poll
148461
148701
  await new Promise((r5) => setTimeout(r5, intervalMs));
148462
148702
  }
148463
148703
  }));
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)", "");
148704
+ 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
148705
  addJsonFlag(withBaseUrl(statusCmd)).action(action(async (workflow2, batchId, opts) => {
148706
+ if (opts.short && opts.watch) {
148707
+ error51("--short and --watch are mutually exclusive (short is for spot-check polling).");
148708
+ process.exit(2);
148709
+ }
148466
148710
  const includes = new Set((opts.include ?? "").split(",").map((s5) => s5.trim()).filter(Boolean));
148467
148711
  const includePayload = includes.has("payload");
148468
148712
  const { client, workflowId } = await buildClientForWorkflow(workflow2, opts);
148469
148713
  const url3 = `/api/v1/workflows/${workflowId}/experiments/${batchId}`;
148470
148714
  if (!opts.watch) {
148471
148715
  const result = await client.get(url3);
148716
+ const execs = result.executions ?? [];
148717
+ const summary = summarizeExperimentExecutions(execs);
148472
148718
  if (opts.json) {
148473
- printJson(result);
148719
+ printJson({ ...result, summary: rollupForJson(summary) });
148720
+ return;
148721
+ }
148722
+ if (opts.short) {
148723
+ console.log(formatShortStatus(summary));
148724
+ const failed2 = summary.counts.failed ?? 0;
148725
+ const cancelled2 = summary.counts.cancelled ?? 0;
148726
+ if (summary.done && failed2 + cancelled2 > 0)
148727
+ process.exit(1);
148474
148728
  return;
148475
148729
  }
148476
- const execs = result.executions ?? [];
148477
- const summary = summarizeExperimentExecutions(execs);
148478
148730
  const counts = Object.entries(summary.counts).map(([k9, v11]) => `${k9}=${v11}`).sort().join(" ");
148479
148731
  console.log(`${ui.bold(batchId)} ${summary.terminal}/${summary.total} terminal ${ui.dim(`(${counts || "no executions"})`)}`);
148480
148732
  const failed = summary.counts.failed ?? 0;
@@ -148488,56 +148740,25 @@ returns immediately with \`{ batchId, total }\`; poll
148488
148740
  process.exit(1);
148489
148741
  return;
148490
148742
  }
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
- }
148743
+ const wantPayload = opts.json || includePayload;
148744
+ await pollExperimentUntilTerminal({
148745
+ client,
148746
+ url: url3,
148747
+ interval: opts.interval,
148748
+ maxWait: opts.maxWait,
148749
+ onTerminal: (result, summary) => {
148520
148750
  if (wantPayload) {
148521
- printJson(result);
148751
+ printJson({ ...result, summary: rollupForJson(summary) });
148522
148752
  } else {
148523
148753
  process.stderr.write(`${ui.dim(`use --include payload (or --json) for the full per-execution snapshot`)}
148524
148754
  `);
148525
148755
  }
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.`);
148756
+ },
148757
+ onDeadline: (result, summary) => {
148535
148758
  if (wantPayload)
148536
- printJson(result);
148537
- process.exit(2);
148759
+ printJson({ ...result, summary: rollupForJson(summary) });
148538
148760
  }
148539
- await new Promise((r5) => setTimeout(r5, opts.interval * 1000));
148540
- }
148761
+ });
148541
148762
  }));
148542
148763
  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
148764
  Examples:
@@ -148626,6 +148847,62 @@ batches must belong to the same workflow within the caller's tenant.
148626
148847
  }
148627
148848
  renderBatchDiffHuman(diff);
148628
148849
  }));
148850
+ 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", `
148851
+ Examples:
148852
+ $ eigenpal workflow experiment watch wf_abc123 evb_xyz789
148853
+ $ eigenpal workflow experiment watch wf_abc123 evb_xyz789 --format csv
148854
+ $ eigenpal workflow experiment watch wf_abc123 evb_xyz789 --pull-on-complete ./out/r.json
148855
+ $ eigenpal workflow experiment watch wf_abc123 evb_xyz789 --no-pull
148856
+
148857
+ Behavior:
148858
+ - Polls every --interval seconds; redraws a one-line tick on a TTY,
148859
+ appends a new line per tick when piped.
148860
+ - On terminal: renders the failed-execution detail block (if any), then
148861
+ downloads results to ./results-<batchId>.<format> unless --no-pull
148862
+ or --pull-on-complete <path> says otherwise.
148863
+ - 4xx errors (e.g. unknown batch id) abort immediately rather than spin.
148864
+ - Pre-flight check: the destination directory must exist before the
148865
+ poll loop starts, so a typo doesn't waste a 30-min wait.
148866
+ - Exit codes (matching \`experiment status --watch\`):
148867
+ 0 every execution succeeded (or pull skipped via --no-pull on a clean batch)
148868
+ 1 at least one execution ended in failed/cancelled/rejected,
148869
+ OR the export download failed after a clean batch
148870
+ 2 --max-wait deadline reached — re-run to keep watching;
148871
+ OR misuse (e.g. --no-pull combined with --pull-on-complete)
148872
+ `);
148873
+ withBaseUrl(watchCmd).action(action(async (workflow2, batchId, opts) => {
148874
+ if (!opts.pull && opts.pullOnComplete) {
148875
+ error51("--no-pull and --pull-on-complete are mutually exclusive — pass one or the other.");
148876
+ process.exit(2);
148877
+ }
148878
+ const { client, workflowId } = await buildClientForWorkflow(workflow2, opts);
148879
+ const url3 = `/api/v1/workflows/${workflowId}/experiments/${batchId}`;
148880
+ const dest = opts.pull ? opts.pullOnComplete ?? `./results-${batchId}.${opts.format}` : null;
148881
+ if (dest) {
148882
+ const dir = path15.dirname(dest);
148883
+ if (!existsSync9(dir)) {
148884
+ error51(`Destination directory does not exist: ${dir}. Create it first or pass a path under an existing directory.`);
148885
+ process.exit(2);
148886
+ }
148887
+ }
148888
+ await pollExperimentUntilTerminal({
148889
+ client,
148890
+ url: url3,
148891
+ interval: opts.interval,
148892
+ maxWait: opts.maxWait,
148893
+ onTerminal: async () => {
148894
+ if (!dest)
148895
+ return;
148896
+ const params = new URLSearchParams({ format: opts.format, batchId });
148897
+ try {
148898
+ await downloadStreamToFile(client, `/api/v1/workflows/${workflowId}/eval-results/export?${params.toString()}`, dest);
148899
+ } catch (err2) {
148900
+ error51(`Watch finished but pulling results failed: ${err2 instanceof Error ? err2.message : String(err2)}. Re-run \`experiment results <wf> <batch>\` to retry.`);
148901
+ process.exit(1);
148902
+ }
148903
+ }
148904
+ });
148905
+ }));
148629
148906
  }
148630
148907
  function renderExperimentFailures(execs, totalCount) {
148631
148908
  const failedExecs = execs.filter((e8) => e8.status === "failed" || e8.status === "cancelled" || e8.status === "rejected");
@@ -148664,6 +148941,83 @@ function summarizeExperimentExecutions(execs) {
148664
148941
  done: execs.length > 0 && terminal === execs.length
148665
148942
  };
148666
148943
  }
148944
+ function rollupForJson(summary) {
148945
+ return {
148946
+ total: summary.total,
148947
+ terminal: summary.terminal,
148948
+ complete: summary.done,
148949
+ completedCount: summary.counts.completed ?? 0,
148950
+ failedCount: summary.counts.failed ?? 0,
148951
+ cancelledCount: summary.counts.cancelled ?? 0,
148952
+ rejectedCount: summary.counts.rejected ?? 0,
148953
+ runningCount: summary.counts.running ?? 0,
148954
+ pendingCount: summary.counts.pending ?? 0
148955
+ };
148956
+ }
148957
+ function formatShortStatus(summary) {
148958
+ const state = summary.done ? "done" : "in-progress";
148959
+ return [
148960
+ `${summary.terminal}/${summary.total}`,
148961
+ state,
148962
+ `failed=${summary.counts.failed ?? 0}`,
148963
+ `cancelled=${summary.counts.cancelled ?? 0}`,
148964
+ `rejected=${summary.counts.rejected ?? 0}`
148965
+ ].join(" ");
148966
+ }
148967
+ function renderTickLine(summary) {
148968
+ const counts = Object.entries(summary.counts).map(([k9, v11]) => `${k9}=${v11}`).sort().join(" ");
148969
+ return `${summary.terminal}/${summary.total} terminal ${ui.dim(`(${counts || "no executions"})`)}`;
148970
+ }
148971
+ async function pollExperimentUntilTerminal({
148972
+ client,
148973
+ url: url3,
148974
+ interval,
148975
+ maxWait,
148976
+ onTerminal,
148977
+ onDeadline
148978
+ }) {
148979
+ const deadline = Date.now() + maxWait * 1000;
148980
+ let lastSummary = "";
148981
+ while (true) {
148982
+ const result = await client.get(url3);
148983
+ const execs = result.executions ?? [];
148984
+ const summary = summarizeExperimentExecutions(execs);
148985
+ const line3 = renderTickLine(summary);
148986
+ if (line3 !== lastSummary) {
148987
+ if (process.stderr.isTTY) {
148988
+ process.stderr.write(`\r\x1B[K${line3}`);
148989
+ } else {
148990
+ process.stderr.write(`${line3}
148991
+ `);
148992
+ }
148993
+ lastSummary = line3;
148994
+ }
148995
+ if (summary.done) {
148996
+ if (process.stderr.isTTY)
148997
+ process.stderr.write(`
148998
+ `);
148999
+ const failed = summary.counts.failed ?? 0;
149000
+ const cancelled = summary.counts.cancelled ?? 0;
149001
+ const rejected = summary.counts.rejected ?? 0;
149002
+ const badCount = failed + cancelled + rejected;
149003
+ if (badCount > 0)
149004
+ renderExperimentFailures(execs, summary.total);
149005
+ await onTerminal(result, summary);
149006
+ if (badCount > 0)
149007
+ process.exit(1);
149008
+ return;
149009
+ }
149010
+ if (Date.now() >= deadline) {
149011
+ if (process.stderr.isTTY)
149012
+ process.stderr.write(`
149013
+ `);
149014
+ error51(`Timed out after ${maxWait}s with ${summary.total - summary.terminal} non-terminal execution(s). Re-run to keep watching.`);
149015
+ onDeadline?.(result, summary);
149016
+ process.exit(2);
149017
+ }
149018
+ await new Promise((r5) => setTimeout(r5, interval * 1000));
149019
+ }
149020
+ }
148667
149021
  function registerVersionsCommands(parent) {
148668
149022
  const versions2 = parent.command("versions").description("Inspect and restore historical workflow versions.");
148669
149023
  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)
@@ -163,6 +174,12 @@ eigenpal workflow evaluators validate ./evaluators.yaml
163
174
  eigenpal workflow evaluators push <workflow-id> --file evaluators.yaml
164
175
  ```
165
176
 
177
+ Always set `description:` on every evaluator entry. It's a short, plain-language
178
+ sentence (≤500 chars; aim for one line) explaining what the evaluator measures
179
+ and why it matters, written for a non-technical reviewer in the dashboard.
180
+ Skip model names, thresholds, and dot-paths; those belong in `config`. See
181
+ [`reference/evaluators.md`](reference/evaluators.md#writing-descriptions-for-stakeholders).
182
+
166
183
  ### Build + validate + upload a dataset
167
184
 
168
185
  ```bash
@@ -218,12 +235,64 @@ eigenpal workflow experiment run <workflow-id> # → { bat
218
235
 
219
236
  # Or restrict to one example:
220
237
  eigenpal workflow experiment run <workflow-id> --example-id evx_…
238
+ ```
239
+
240
+ **Recommended:** use `experiment watch` — it polls until terminal AND auto-pulls
241
+ results to disk in one command. Replaces the old "run → status (poll) → results"
242
+ chain so agents don't have to write a custom poller, parse `executions[].status`,
243
+ or chain `monitor + status + pull + score` shell logic.
244
+
245
+ ```bash
246
+ # One command: watch until terminal, then write ./results-<batchId>.json on completion.
247
+ eigenpal workflow experiment watch <workflow-id> <batch-id>
248
+
249
+ # CSV output:
250
+ eigenpal workflow experiment watch <workflow-id> <batch-id> --format csv
251
+
252
+ # Custom destination (overrides the default ./results-<batchId>.<format>):
253
+ eigenpal workflow experiment watch <workflow-id> <batch-id> --pull-on-complete ./out/r.json
254
+
255
+ # Just watch, skip the pull (e.g. you only need the live tick + exit code):
256
+ eigenpal workflow experiment watch <workflow-id> <batch-id> --no-pull
257
+ ```
221
258
 
222
- # Poll until done. `experiment status` auto-detaches after 30 min; just re-run.
223
- eigenpal workflow experiment status <workflow-id> <batch-id>
259
+ Exit codes match `experiment status --watch`: `0` clean, `1` any
260
+ failed/cancelled/rejected execution, `2` `--max-wait` deadline reached
261
+ (30 min default — re-run to keep watching).
224
262
 
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.
263
+ ### Monitoring scripts — use `--short` and the JSON rollup
264
+
265
+ When a script needs to spot-check a batch (no waiting), prefer one of the two
266
+ machine-readable paths instead of folding `executions[].status` yourself.
267
+
268
+ ```bash
269
+ # Single line on stdout, awk-friendly:
270
+ eigenpal workflow experiment status <wf> <batch> --short
271
+ # → 6/6 done failed=0 cancelled=0 rejected=0 (or "in-progress" while pending)
272
+
273
+ # Parse with awk:
274
+ DONE_STATE=$(eigenpal workflow experiment status <wf> <batch> --short | awk '{print $2}')
275
+ [ "$DONE_STATE" = "done" ] || exit 0 # not terminal yet, try again later
276
+ ```
277
+
278
+ ```bash
279
+ # JSON includes a top-level `summary` rollup so you don't count statuses by hand:
280
+ eigenpal workflow experiment status <wf> <batch> --json | jq '.summary'
281
+ # → { total: 6, terminal: 6, complete: true,
282
+ # completedCount: 5, failedCount: 1,
283
+ # cancelledCount: 0, rejectedCount: 0,
284
+ # runningCount: 0, pendingCount: 0 }
285
+
286
+ # `summary.complete` is the canonical poll-completion flag.
287
+ ```
288
+
289
+ `--short` and `--json` are mutually exclusive with `--watch` — for live
290
+ streaming use `experiment watch` instead.
291
+
292
+ If you need the raw eval-results export after watching with `--no-pull`, or
293
+ to refetch results outside a watch loop, fall back to:
294
+
295
+ ```bash
227
296
  eigenpal workflow experiment results <workflow-id> <batch-id> --format csv --out r.csv
228
297
  ```
229
298
 
@@ -433,10 +502,11 @@ eigenpal workflow clear-local # delete local execution artifacts
433
502
 
434
503
  # Experiments (batch eval runs)
435
504
  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
505
+ 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.
506
+ 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
507
  eigenpal workflow experiment results <workflow-id> [batch-id] --format {csv|json} --out r.csv
438
508
  eigenpal workflow experiment list <workflow-id>
439
- eigenpal workflow experiment compare <batch-a> <batch-b> [--regression-threshold] [--sort] # no --workflow-id
509
+ eigenpal workflow experiment compare <batch-a> <batch-b> [--regression-threshold] [--sort] # no --workflow-id; prints per-evaluator aggregate + per-row table
440
510
 
441
511
  # Step-type / evaluator-type introspection
442
512
  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.
@@ -11,16 +11,41 @@ this is the agent-facing summary.
11
11
  ```yaml
12
12
  evaluators: # array of evaluator definitions, ≥1 entry
13
13
  - name: ... # required, unique within this file, [a-z0-9][a-z0-9-_]*
14
+ description: ... # optional, ≤500 chars; one-sentence business explanation (see below)
14
15
  type: ... # required, one of: exact-diff | llm-judge | custom-script
15
16
  config: # required, shape depends on `type`
16
17
  passThreshold: 0.95 # 0..1, optional (default 1.0)
17
18
  ...
18
19
 
19
- # OPTIONAL global config
20
- concurrency: 4 # max examples in parallel during `experiment run` (default 1)
21
- judge: # optional shared model defaults for llm-judge evaluators
22
- model: gpt-4o
23
- temperature: 0
20
+ # OPTIONAL workflow-level pass threshold. A run passes when its weighted-mean
21
+ # score across evaluators clears this number. Defaults to 0.7.
22
+ passThreshold: 0.7
23
+ ```
24
+
25
+ ## Writing descriptions for stakeholders
26
+
27
+ Every evaluator entry takes an optional `description`. Always set it. The
28
+ dashboard surfaces this string to non-technical reviewers (legal, ops,
29
+ finance) who never see the YAML or the judge prompt. Write it for them:
30
+
31
+ - One short sentence, plain language.
32
+ - Name what the evaluator measures and why a business reader should care.
33
+ - Skip implementation detail (model names, thresholds, dot-paths). Those
34
+ belong in `config`.
35
+ - Keep it under ~120 characters when you can; the schema caps it at 500.
36
+
37
+ ```yaml
38
+ # Good. A non-technical reviewer immediately grasps the intent.
39
+ - name: covenants-recall
40
+ description: Checks every covenant from the contract appears in the extraction.
41
+ type: llm-judge
42
+ config: ...
43
+
44
+ # Bad. Restates the config; teaches a stakeholder nothing.
45
+ - name: covenants-recall
46
+ description: llm-judge with passThreshold 0.85 over the covenants array.
47
+ type: llm-judge
48
+ config: ...
24
49
  ```
25
50
 
26
51
  ## Evaluator types
@@ -47,23 +72,34 @@ judge: # optional shared model defaults for llm-judge evaluators
47
72
  - name: covenants-recall
48
73
  type: llm-judge
49
74
  config:
50
- model: gpt-4o # string — the OpenAI model id
51
- temperature: 0 # number — for reproducibility
75
+ model: gpt-4o # OPTIONAL — falls back to the workspace default LLM provider
76
+ mode: continuous # continuous | discrete (default: continuous)
52
77
  passThreshold: 0.85
53
- prompt: |
78
+ docLabel: extraction # what the judge calls the workflow output (default: "output")
79
+ promptExtension: |
54
80
  Score how well the actual output captures every covenant in the
55
- expected output, on a scale of 0..1. Penalise missing covenants;
56
- do not penalise over-extraction. Return JSON { score, rationale }.
57
- ```
81
+ expected output. Penalise missing covenants; do not penalise
82
+ over-extraction. Be strict on covenants that change loan economics.
58
83
 
59
- The harness wraps your `prompt` and feeds:
60
-
61
- - `actual` — the workflow's output
62
- - `expected` — the example's `expected/output.json`
63
- - `meta` — example metadata
84
+ # discrete mode — the judge picks a label and the score table maps to a number
85
+ - name: tone-check
86
+ type: llm-judge
87
+ config:
88
+ mode: discrete
89
+ passThreshold: 0.7
90
+ labels:
91
+ excellent: 1.0
92
+ acceptable: 0.7
93
+ off-tone: 0.0
94
+ promptExtension: |
95
+ Pick the label that best matches the tone of the response.
96
+ ```
64
97
 
65
- It expects the model to return JSON with at least `score` (0..1) and
66
- `rationale` (string). The fixed harness handles parse failures + retries.
98
+ `promptExtension` is the only required field — it's appended to the harness
99
+ prompt and tells the judge what "good" looks like for this workflow. The
100
+ harness handles JSON parsing, retries, and feeds the judge `actual`,
101
+ `expected`, and example metadata. In `discrete` mode the judge MUST return
102
+ one of the keys in `labels`; the harness then looks up the score.
67
103
 
68
104
  ### `custom-script` — JavaScript in the sandbox
69
105
 
@@ -72,16 +108,38 @@ It expects the model to return JSON with at least `score` (0..1) and
72
108
  type: custom-script
73
109
  config:
74
110
  passThreshold: 1.0
75
- code: |
76
- const a = actual.totalAmount ?? 0;
77
- const e = expected.totalAmount ?? 0;
78
- const score = Math.abs(a - e) <= 0.01 ? 1 : 0;
79
- return { score, rationale: `actual=${a}, expected=${e}` };
111
+ function: |
112
+ /**
113
+ * @param {Expected} expected the example's expected output
114
+ * @param {Actual} actual the workflow's actual output
115
+ * @returns {number} a score in [0, 1]; throws are caught and scored as 0
116
+ */
117
+ function scoreScript(expected, actual) {
118
+ const a = actual.totalAmount ?? 0;
119
+ const e = expected.totalAmount ?? 0;
120
+ return Math.abs(a - e) <= 0.01 ? 1 : 0;
121
+ }
80
122
  ```
81
123
 
82
124
  Same WASM sandbox as `transform.script` (5s wall clock, 10 MB heap, no
83
- network). Receives `actual`, `expected`, `meta` in scope. Returns
84
- `{ score: number, label?: string, rationale?: string }`.
125
+ network). The YAML stores the **whole** `function scoreScript(expected,
126
+ actual) { ... }` declaration verbatim — same text the dashboard editor
127
+ shows. The JSDoc preamble is optional but recommended: it pulls the
128
+ workflow's output schema into IntelliSense for `expected.…` /
129
+ `actual.…` in the dashboard.
130
+
131
+ The signature shape is **locked**:
132
+
133
+ - Function name MUST be `scoreScript`.
134
+ - Parameters MUST be `(expected, actual)` in that order. Swapping them
135
+ would silently invert every score.
136
+ - Body MUST `return` a number in `[0, 1]`, or `throw` (which is caught
137
+ and scored as 0).
138
+
139
+ `workflow validate` and `workflow evaluators validate` flag YAML where
140
+ the function fails to parse, the signature doesn't match, or the body
141
+ contains neither `return` nor `throw` — so a hand-edited config errors
142
+ before it reaches the worker.
85
143
 
86
144
  ## Pass / fail vs score
87
145
 
@@ -160,9 +218,11 @@ process — `--out` writes the file directly from the server's storage.
160
218
  LLM-judge scores have variance. To reduce flake:
161
219
 
162
220
  - Pin a specific model id (`gpt-4o-2024-08-06` rather than `gpt-4o`).
163
- - Set `temperature: 0`.
164
- - Keep the prompt narrow — score one dimension per evaluator. Use
221
+ - Keep `promptExtension` narrow — score one dimension per evaluator. Use
165
222
  multiple judges instead of one omnibus prompt.
223
+ - Prefer `mode: discrete` with a small label set when the judgement is
224
+ categorical (good / acceptable / bad) — it eliminates the model's
225
+ free-form numeric drift.
166
226
  - For high-stakes thresholds, run the experiment ≥3 times and average.
167
227
 
168
228
  <!-- GENERATED:EVALUATOR_CATALOG START -->
@@ -177,6 +237,14 @@ _Generated from `EvalConfigYamlSchema` in `@eigenpal/types/src/eval/evaluator-co
177
237
  | `evaluators` | array<object> | no | `[]` | |
178
238
  | `passThreshold` | number | no | `0.7` | |
179
239
 
240
+ ### Common entry fields (every evaluator type)
241
+
242
+ | Field | Type | Required | Default | Description |
243
+ | --- | --- | --- | --- | --- |
244
+ | `name` | string | yes | | Identifier used in eval results and the weighted-mean formula. Use a short kebab-case slug like header-exact or covenants-recall. |
245
+ | `description` | string | no | | One-sentence explanation, in business language, of what this evaluator measures and why a stakeholder should care. Shown in the dashboard to non-technical reviewers (legal, ops, finance) who never see the YAML or judge prompt. Skip implementation detail (model names, thresholds, dot-paths); those live in `config`. |
246
+ | `weight` | number | no | `1` | Relative importance in the weighted mean. The overall score is Σ(weightᵢ × scoreᵢ) / Σweight. Set to 0 to score-only without affecting overall. |
247
+
180
248
  ### `exact-diff` — JSON deep-diff against expected output
181
249
 
182
250
  | Field | Type | Required | Default | Description |
@@ -203,7 +271,7 @@ _Generated from `EvalConfigYamlSchema` in `@eigenpal/types/src/eval/evaluator-co
203
271
 
204
272
  | Field | Type | Required | Default | Description |
205
273
  | --- | --- | --- | --- | --- |
206
- | `code` | string | yes | | Sandboxed JavaScript. Receives `actual`, `expected`, `input` as variables and must return { score, label? } where score is in [0, 1]. |
274
+ | `function` | string | yes | | Full JavaScript declaration of `function scoreScript(expected, actual) { ... }`. Receives `expected` (the example's expected output) and `actual` (the workflow's actual output), returns a number in [0, 1]. Throws are caught and scored as 0. |
207
275
  | `timeoutMs` | integer | no | `5000` | Maximum wall-clock time the script can run before it's killed and the run is marked failed. |
208
276
  | `memoryLimitMb` | integer | no | `10` | Maximum memory the sandbox may allocate. Increase if the script processes large arrays or strings. |
209
277
  | `passThreshold` | number | no | `1` | Minimum script score this evaluator must reach for a run to pass. |
@@ -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
@@ -1,5 +1,6 @@
1
1
  evaluators:
2
2
  - name: exact-match
3
+ description: Checks the extracted fields match the expected output exactly.
3
4
  type: exact-diff
4
5
  config:
5
6
  passThreshold: 1.0
@@ -1,5 +1,6 @@
1
1
  evaluators:
2
2
  - name: label-match
3
+ description: Checks the predicted label matches the expected label.
3
4
  type: exact-diff
4
5
  config:
5
6
  passThreshold: 1.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eigenpal/cli",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
5
5
  "keywords": [
6
6
  "eigenpal",