@eigenpal/cli 0.5.8 → 0.5.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
@@ -1,6 +1,12 @@
1
1
  # @eigenpal/cli
2
2
 
3
- ## 1.0.0
3
+ ## 0.5.10
4
+
5
+ ### Patch Changes
6
+
7
+ - aee7e0c: Add hidden Git source commands for editing trigger policy and encrypted secrets.
8
+
9
+ ## 0.5.9
4
10
 
5
11
  ### Major Changes
6
12
 
@@ -333,6 +339,10 @@ actual) { ... }` declaration around your statements. Parameter order is
333
339
 
334
340
  Also scrubs internal references from CLI source ahead of open-sourcing — generic placeholder names in docstrings and test fixtures, internal-tracker IDs stripped from comments, and one user-facing error message updated to point at the public issue tracker.
335
341
 
342
+ - 75caaec: `eigenpal workflow push` and `eigenpal workflow validate` now surface non-fatal **schema-quality warnings** when an output schema is loose enough to hurt downstream typing: categorical fields like `category`/`status`/`kind` declared as plain strings (no `enum`), `transform.script` functions returning `any`/`unknown`, untyped objects (`type: object` with no `properties`), and untyped arrays (`type: array` with no `items`). Warnings print to stderr and never block the push.
343
+
344
+ Updated the bundled CLI skill (`workflow-yaml.md`) with a "Be specific with types" section teaching `enum` fields, per-value meanings inline in `description`, and how `transform.script` TS literal unions auto-produce JSON Schema enums.
345
+
336
346
  - be693a3: `EIGENPAL_API_KEY` is now a complete profile bypass — when set, the active profile's `baseUrl` is also ignored (previously the env key was paired with the profile's URL, which is always wrong because an API key is provisioned for one server).
337
347
 
338
348
  In CI:
package/dist/cli.js CHANGED
@@ -28107,7 +28107,8 @@ var init_parser = __esm(() => {
28107
28107
  maxConcurrency: exports_external.number().min(1).max(30).default(3),
28108
28108
  pagesPerBatch: exports_external.number().min(1).max(20).default(5),
28109
28109
  prompt: exports_external.string().optional().describe("Custom prompt for LLM-based parsing"),
28110
- languages: exports_external.array(exports_external.string()).optional().describe("OCR language hints")
28110
+ languages: exports_external.array(exports_external.string()).optional().describe("OCR language hints"),
28111
+ tenantId: exports_external.string().optional()
28111
28112
  });
28112
28113
  ParserInputSchema = exports_external.object({
28113
28114
  data: exports_external.instanceof(Buffer).or(exports_external.instanceof(Uint8Array)),
@@ -65788,8 +65789,16 @@ function convertTsTypeToJsonSchema(node, ctx) {
65788
65789
  case "TSVoidKeyword":
65789
65790
  return {};
65790
65791
  case "TSAnyKeyword":
65791
- case "TSUnknownKeyword":
65792
+ case "TSUnknownKeyword": {
65793
+ const keyword = node.type === "TSAnyKeyword" ? "any" : "unknown";
65794
+ ctx.warnings.push({
65795
+ code: "weak-return-type",
65796
+ message: `\`${keyword}\` produces an empty schema, so downstream steps can't autocomplete this field or its descendants. Describe the actual shape (\`{ name: string; total: number }\`) or a literal union (\`"low" | "medium" | "high"\`) for the strongest downstream typing.`,
65797
+ start: node.start ?? undefined,
65798
+ end: node.end ?? undefined
65799
+ });
65792
65800
  return {};
65801
+ }
65793
65802
  case "TSNeverKeyword":
65794
65803
  return { not: {} };
65795
65804
  case "TSArrayType":
@@ -66138,13 +66147,15 @@ function compileTypedScript(opts) {
66138
66147
  });
66139
66148
  }
66140
66149
  const convertIssues = [];
66150
+ const convertWarnings = [];
66141
66151
  const inScopeSchemas = new Map(Object.entries(opts.inScopeSchemas ?? {}));
66142
66152
  const localAliases = collectLocalAliases(aliasDecls);
66143
66153
  const returnSchema = opts.kind === "evaluator" ? { type: "number" } : convertTsTypeToJsonSchema(returnTsType, {
66144
66154
  localAliases,
66145
66155
  inScopeSchemas,
66146
66156
  resolving: new Set,
66147
- issues: convertIssues
66157
+ issues: convertIssues,
66158
+ warnings: convertWarnings
66148
66159
  });
66149
66160
  if (convertIssues.length > 0) {
66150
66161
  return {
@@ -66157,10 +66168,7 @@ function compileTypedScript(opts) {
66157
66168
  }
66158
66169
  let stripped;
66159
66170
  try {
66160
- const out = import_sucrase.transform(opts.source, {
66161
- transforms: ["typescript"]
66162
- });
66163
- stripped = out.code;
66171
+ stripped = import_sucrase.transform(opts.source, { transforms: ["typescript"] }).code;
66164
66172
  } catch (err) {
66165
66173
  const e2 = err;
66166
66174
  return failWith({
@@ -66174,7 +66182,8 @@ function compileTypedScript(opts) {
66174
66182
  js: stripped,
66175
66183
  returnSchema,
66176
66184
  paramNames: actualParamNames
66177
- }
66185
+ },
66186
+ warnings: convertWarnings
66178
66187
  };
66179
66188
  }
66180
66189
  function failWith(issue2) {
@@ -68012,7 +68021,63 @@ var init_eval = __esm(() => {
68012
68021
  init_workflow_tool_metadata();
68013
68022
  });
68014
68023
 
68024
+ // ../types/src/git-source/cutover.ts
68025
+ function resolveAgentSourceCutover(settings) {
68026
+ const parsed = AgentSourceCutoverSettingsSchema.safeParse(settings ?? {});
68027
+ if (!parsed.success)
68028
+ return { mode: "legacy", reason: "invalid_settings" };
68029
+ const agents = parsed.data.gitBackedSource?.agents;
68030
+ if (!agents)
68031
+ return { mode: "legacy", reason: "settings_missing" };
68032
+ if (agents.migrationState === "rolled_back")
68033
+ return { mode: "legacy", reason: "rolled_back" };
68034
+ if (agents.runtime !== "git")
68035
+ return { mode: "legacy", reason: "runtime_legacy" };
68036
+ if (agents.migrationState !== "cutover") {
68037
+ return { mode: "legacy", reason: "migration_not_cutover" };
68038
+ }
68039
+ return { mode: "git", reason: "cutover_enabled" };
68040
+ }
68041
+ function isGitBackedAgentSourceEnabled(settings) {
68042
+ return resolveAgentSourceCutover(settings).mode === "git";
68043
+ }
68044
+ var AgentSourceRuntimeModeSchema, AgentSourceMigrationStateSchema, AgentSourceCutoverSettingsSchema;
68045
+ var init_cutover = __esm(() => {
68046
+ init_zod();
68047
+ AgentSourceRuntimeModeSchema = exports_external.enum(["legacy", "git"]);
68048
+ AgentSourceMigrationStateSchema = exports_external.enum([
68049
+ "disabled",
68050
+ "shadow",
68051
+ "validated",
68052
+ "cutover",
68053
+ "rolled_back"
68054
+ ]);
68055
+ AgentSourceCutoverSettingsSchema = exports_external.object({
68056
+ gitBackedSource: exports_external.object({
68057
+ agents: exports_external.object({
68058
+ runtime: AgentSourceRuntimeModeSchema.optional(),
68059
+ migrationState: AgentSourceMigrationStateSchema.optional(),
68060
+ legacySourceRetained: exports_external.boolean().optional(),
68061
+ rollbackReason: exports_external.string().optional()
68062
+ }).passthrough().optional()
68063
+ }).passthrough().optional()
68064
+ }).passthrough();
68065
+ });
68066
+
68015
68067
  // ../types/src/git-source/grammar.ts
68068
+ function parseAutomationTarget(target) {
68069
+ const parsed = AutomationTargetSchema.parse(target);
68070
+ const [rawPackageName, rawRef] = parsed.split("@");
68071
+ const packagePath = dottedPackageNameToPath(rawPackageName);
68072
+ const [type, ...slugParts] = packagePath.split("/");
68073
+ return {
68074
+ packageName: DottedPackageNameSchema.parse(rawPackageName),
68075
+ packagePath,
68076
+ type,
68077
+ slug: slugParts.join("/"),
68078
+ ref: SourceVersionRefSchema.parse(rawRef ?? "latest")
68079
+ };
68080
+ }
68016
68081
  function pathToDottedPackageName(path2) {
68017
68082
  const parsed = SourcePackagePathSchema.parse(path2);
68018
68083
  return DottedPackageNameSchema.parse(parsed.split("/").join("."));
@@ -68344,7 +68409,16 @@ var init_manifests = __esm(() => {
68344
68409
  api: exports_external.boolean().optional(),
68345
68410
  email: exports_external.object({
68346
68411
  enabled: exports_external.boolean().default(true),
68347
- aliases: exports_external.array(exports_external.string().email()).default([])
68412
+ aliases: exports_external.array(exports_external.union([
68413
+ exports_external.string().includes("@"),
68414
+ exports_external.object({
68415
+ address: exports_external.string().includes("@"),
68416
+ allowlist: exports_external.array(exports_external.string()).default([]),
68417
+ replyConfig: exports_external.record(exports_external.string(), exports_external.unknown()).default({}),
68418
+ requireSenderAuth: exports_external.boolean().default(true),
68419
+ enabled: exports_external.boolean().optional()
68420
+ }).strict()
68421
+ ])).default([])
68348
68422
  }).strict().optional()
68349
68423
  }).strict();
68350
68424
  CommonPackageManifestFields = {
@@ -68413,10 +68487,63 @@ var init_manifests = __esm(() => {
68413
68487
  SOURCE_TEMPLATE_FILE_PATTERN = /\.(?:docx|xlsx)$/i;
68414
68488
  });
68415
68489
 
68490
+ // ../types/src/git-source/secrets.ts
68491
+ var SOURCE_SECRETS_FILENAME = "secrets.enc.yaml", SecretNameSchema, Base64LikeSchema, EncryptedSecretValueSchema, SourceSecretEntrySchema, SourceSecretsFileSchema;
68492
+ var init_secrets = __esm(() => {
68493
+ init_zod();
68494
+ init_manifests();
68495
+ SecretNameSchema = exports_external.string().regex(/^[A-Z][A-Z0-9_]*$/, "Secret names must be uppercase env-style identifiers");
68496
+ Base64LikeSchema = exports_external.string().min(1).regex(/^[A-Za-z0-9+/=_-]+$/, {
68497
+ message: "Encrypted secret fields must be encoded strings"
68498
+ });
68499
+ EncryptedSecretValueSchema = exports_external.object({
68500
+ algorithm: exports_external.literal("aes-256-gcm"),
68501
+ keyId: exports_external.string().min(1),
68502
+ nonce: Base64LikeSchema,
68503
+ ciphertext: Base64LikeSchema,
68504
+ tag: Base64LikeSchema
68505
+ }).strict();
68506
+ SourceSecretEntrySchema = exports_external.object({
68507
+ description: exports_external.string().optional(),
68508
+ encrypted: EncryptedSecretValueSchema.optional()
68509
+ }).passthrough().superRefine((entry, ctx) => {
68510
+ const raw = entry;
68511
+ if ("value" in raw || "plaintext" in raw || "secret" in raw) {
68512
+ ctx.addIssue({
68513
+ code: "custom",
68514
+ message: "secrets.enc.yaml must not contain plaintext secret values"
68515
+ });
68516
+ }
68517
+ if (!raw.encrypted) {
68518
+ ctx.addIssue({
68519
+ code: "custom",
68520
+ path: ["encrypted"],
68521
+ message: "Encrypted secret values are required"
68522
+ });
68523
+ }
68524
+ const extraKeys = Object.keys(raw).filter((key) => key !== "description" && key !== "encrypted");
68525
+ for (const key of extraKeys) {
68526
+ if (key === "value" || key === "plaintext" || key === "secret")
68527
+ continue;
68528
+ ctx.addIssue({
68529
+ code: "custom",
68530
+ path: [key],
68531
+ message: `Unknown secret metadata field: ${key}`
68532
+ });
68533
+ }
68534
+ });
68535
+ SourceSecretsFileSchema = exports_external.object({
68536
+ schemaVersion: SourceManifestSchemaVersionSchema,
68537
+ secrets: exports_external.record(SecretNameSchema, SourceSecretEntrySchema)
68538
+ }).strict();
68539
+ });
68540
+
68416
68541
  // ../types/src/git-source/index.ts
68417
68542
  var init_git_source = __esm(() => {
68543
+ init_cutover();
68418
68544
  init_grammar();
68419
68545
  init_manifests();
68546
+ init_secrets();
68420
68547
  });
68421
68548
 
68422
68549
  // ../types/src/storage/paths.ts
@@ -68720,11 +68847,13 @@ __export(exports_src, {
68720
68847
  runInputKey: () => runInputKey,
68721
68848
  rootS3Path: () => rootS3Path,
68722
68849
  resolveEffectiveMimeType: () => resolveEffectiveMimeType,
68850
+ resolveAgentSourceCutover: () => resolveAgentSourceCutover,
68723
68851
  requiresSpecializedParser: () => requiresSpecializedParser,
68724
68852
  pathToDottedPackageName: () => pathToDottedPackageName,
68725
68853
  parseStepType: () => parseStepType,
68726
68854
  parseReleaseTag: () => parseReleaseTag,
68727
68855
  parseFileKey: () => parseFileKey,
68856
+ parseAutomationTarget: () => parseAutomationTarget,
68728
68857
  noopTracingContext: () => noopTracingContext,
68729
68858
  mapOCRAttributes: () => mapOCRAttributes,
68730
68859
  mapLLMAttributes: () => mapLLMAttributes,
@@ -68739,6 +68868,7 @@ __export(exports_src, {
68739
68868
  isOutputSchemaUnknown: () => isOutputSchemaUnknown,
68740
68869
  isNonSuccessTerminalExecutionStatus: () => isNonSuccessTerminalExecutionStatus,
68741
68870
  isLocalAgentResourcePath: () => isLocalAgentResourcePath,
68871
+ isGitBackedAgentSourceEnabled: () => isGitBackedAgentSourceEnabled,
68742
68872
  isFilePathDescriptor: () => isFilePathDescriptor,
68743
68873
  isFileOutput: () => isFileOutput,
68744
68874
  hasTriggerMethod: () => hasTriggerMethod,
@@ -68854,6 +68984,7 @@ __export(exports_src, {
68854
68984
  SpanKindMap: () => SpanKindMap,
68855
68985
  SpanKind: () => import_api.SpanKind,
68856
68986
  SourceVersionRefSchema: () => SourceVersionRefSchema,
68987
+ SourceSecretsFileSchema: () => SourceSecretsFileSchema,
68857
68988
  SourcePackageTypeSchema: () => SourcePackageTypeSchema,
68858
68989
  SourcePackageSegmentSchema: () => SourcePackageSegmentSchema,
68859
68990
  SourcePackagePathSchema: () => SourcePackagePathSchema,
@@ -68863,6 +68994,7 @@ __export(exports_src, {
68863
68994
  SharedResourcePackageManifestSchema: () => SharedResourcePackageManifestSchema,
68864
68995
  SharedEvaluatorReferenceSchema: () => SharedEvaluatorReferenceSchema,
68865
68996
  SharedEvaluatorCompositionItemSchema: () => SharedEvaluatorCompositionItemSchema,
68997
+ SecretNameSchema: () => SecretNameSchema,
68866
68998
  ScopeStackSchema: () => ScopeStackSchema,
68867
68999
  ScopeEntryTypeSchema: () => ScopeEntryTypeSchema,
68868
69000
  ScopeEntrySchema: () => ScopeEntrySchema,
@@ -68871,6 +69003,7 @@ __export(exports_src, {
68871
69003
  STEP_EXECUTION_STATUSES: () => STEP_EXECUTION_STATUSES,
68872
69004
  SOURCE_VERSION_RANGE_PATTERN: () => SOURCE_VERSION_RANGE_PATTERN,
68873
69005
  SOURCE_SEMVER_PATTERN: () => SOURCE_SEMVER_PATTERN,
69006
+ SOURCE_SECRETS_FILENAME: () => SOURCE_SECRETS_FILENAME,
68874
69007
  SOURCE_PACKAGE_SEGMENT_PATTERN: () => SOURCE_PACKAGE_SEGMENT_PATTERN,
68875
69008
  SOURCE_MANIFEST_FILENAME: () => SOURCE_MANIFEST_FILENAME,
68876
69009
  SOURCE_COMMIT_SHA_PATTERN: () => SOURCE_COMMIT_SHA_PATTERN,
@@ -68954,6 +69087,7 @@ __export(exports_src, {
68954
69087
  EvalOverridesSchema: () => EvalOverridesSchema,
68955
69088
  EvalExampleMetaSchema: () => EvalExampleMetaSchema,
68956
69089
  EvalConfigYamlSchema: () => EvalConfigYamlSchema,
69090
+ EncryptedSecretValueSchema: () => EncryptedSecretValueSchema,
68957
69091
  EmailTriggerMethodSchema: () => EmailTriggerMethodSchema,
68958
69092
  Eigenpal: () => Eigenpal,
68959
69093
  EXECUTION_TYPES: () => EXECUTION_TYPES,
@@ -69016,6 +69150,9 @@ __export(exports_src, {
69016
69150
  AiParseConfigSchema: () => AiParseConfigSchema,
69017
69151
  AiExtractOutputSchema: () => AiExtractOutputSchema,
69018
69152
  AiExtractConfigSchema: () => AiExtractConfigSchema,
69153
+ AgentSourceRuntimeModeSchema: () => AgentSourceRuntimeModeSchema,
69154
+ AgentSourceMigrationStateSchema: () => AgentSourceMigrationStateSchema,
69155
+ AgentSourceCutoverSettingsSchema: () => AgentSourceCutoverSettingsSchema,
69019
69156
  AgentPackageManifestSchema: () => AgentPackageManifestSchema,
69020
69157
  ActionTypeValue: () => ActionTypeValue,
69021
69158
  ActionTypeSchema: () => ActionTypeSchema,
@@ -181601,7 +181738,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
181601
181738
  // package.json
181602
181739
  var package_default = {
181603
181740
  name: "@eigenpal/cli",
181604
- version: "0.5.8",
181741
+ version: "0.5.10",
181605
181742
  description: "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
181606
181743
  keywords: [
181607
181744
  "eigenpal",
@@ -181641,6 +181778,8 @@ var package_default = {
181641
181778
  scripts: {
181642
181779
  dev: "bun src/cli.ts",
181643
181780
  build: "bun run generate:skill && bun build src/cli.ts --outdir dist --target node --format esm && rm -rf dist/templates dist/skill && cp -R src/templates dist/templates && cp -R src/skill dist/skill",
181781
+ generate: "bun run generate:cli-docs && bun run generate:skill",
181782
+ "generate:check": "bun run generate:cli-docs:check && bun run generate:skill:check",
181644
181783
  "generate:skill": "bun scripts/generate-skill-reference.ts",
181645
181784
  "generate:skill:check": "bun scripts/generate-skill-reference.ts --check",
181646
181785
  "generate:cli-docs": "bun scripts/generate-cli-docs.ts",
@@ -184106,7 +184245,7 @@ init_src();
184106
184245
  init_zod();
184107
184246
  var import_yaml2 = __toESM(require_dist(), 1);
184108
184247
  import { spawnSync as spawnSync2 } from "node:child_process";
184109
- import { createHash as createHash2 } from "node:crypto";
184248
+ import { createCipheriv, createHash as createHash2, randomBytes } from "node:crypto";
184110
184249
  import {
184111
184250
  existsSync as existsSync3,
184112
184251
  mkdirSync as mkdirSync2,
@@ -184269,6 +184408,9 @@ function listRelativeFiles(dir) {
184269
184408
  function readYamlFile(filePath) {
184270
184409
  return import_yaml2.default.parse(readFileSync3(filePath, "utf8"));
184271
184410
  }
184411
+ function writeYamlFile(filePath, value) {
184412
+ writeFileSync2(filePath, import_yaml2.default.stringify(value));
184413
+ }
184272
184414
  function packageManifestExists(gitRoot, packagePath) {
184273
184415
  return existsSync3(path2.join(gitRoot, packagePath, "eigenpal.yaml"));
184274
184416
  }
@@ -184279,6 +184421,28 @@ function releaseTagExists(gitRoot, packagePath, version2) {
184279
184421
  function readPackageManifest(packageRoot) {
184280
184422
  return SourcePackageManifestSchema.parse(readYamlFile(path2.join(packageRoot, "eigenpal.yaml")));
184281
184423
  }
184424
+ function requirePackageContext(opts = {}) {
184425
+ const context = resolveGitSourceContext(opts);
184426
+ if (!context.gitRoot || !context.packageRoot || !context.packagePath) {
184427
+ throw new Error("Run this command inside a source package, or pass --dir.");
184428
+ }
184429
+ return {
184430
+ gitRoot: context.gitRoot,
184431
+ packageRoot: context.packageRoot,
184432
+ packagePath: context.packagePath
184433
+ };
184434
+ }
184435
+ function readMutablePackageManifest(packageRoot) {
184436
+ const manifest = readYamlFile(path2.join(packageRoot, "eigenpal.yaml"));
184437
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
184438
+ throw new Error("Package eigenpal.yaml must be a YAML object.");
184439
+ }
184440
+ return manifest;
184441
+ }
184442
+ function writeMutablePackageManifest(packageRoot, manifest) {
184443
+ SourcePackageManifestSchema.parse(manifest);
184444
+ writeYamlFile(path2.join(packageRoot, "eigenpal.yaml"), manifest);
184445
+ }
184282
184446
  function parsePackageRef(input) {
184283
184447
  const [rawPackage, rawRef, extra] = input.split("@");
184284
184448
  if (!rawPackage || extra !== undefined) {
@@ -185087,6 +185251,144 @@ async function sync(target, opts) {
185087
185251
  }
185088
185252
  await syncLatestAutomation(new ApiClient(resolveConfig(opts)), packagePath);
185089
185253
  }
185254
+ function setEmailTriggerAlias(email3, opts) {
185255
+ const context = requirePackageContext(opts);
185256
+ const manifest = readMutablePackageManifest(context.packageRoot);
185257
+ const triggers = { ...manifest.triggers ?? {} };
185258
+ const emailConfig = { ...triggers.email ?? {} };
185259
+ const aliases = Array.isArray(emailConfig.aliases) ? [...emailConfig.aliases] : [];
185260
+ const nextAlias = {
185261
+ address: email3,
185262
+ enabled: !opts.disabled,
185263
+ allowlist: opts.allow ?? [],
185264
+ replyConfig: {
185265
+ ...opts.replyOn ? { on: opts.replyOn } : {},
185266
+ ...opts.replyMode ? { mode: opts.replyMode } : {}
185267
+ },
185268
+ requireSenderAuth: opts.requireSenderAuth ?? true
185269
+ };
185270
+ const filtered = aliases.filter((alias) => typeof alias === "string" ? alias.toLowerCase() !== email3.toLowerCase() : alias.address?.toLowerCase() !== email3.toLowerCase());
185271
+ manifest.triggers = {
185272
+ ...triggers,
185273
+ email: { ...emailConfig, enabled: true, aliases: [...filtered, nextAlias] }
185274
+ };
185275
+ writeMutablePackageManifest(context.packageRoot, manifest);
185276
+ success2(`Updated email trigger ${ui.bold(email3)} in eigenpal.yaml.`);
185277
+ }
185278
+ function setApiTrigger(enabled, opts) {
185279
+ const context = requirePackageContext(opts);
185280
+ const manifest = readMutablePackageManifest(context.packageRoot);
185281
+ const triggers = { ...manifest.triggers ?? {} };
185282
+ manifest.triggers = { ...triggers, api: enabled };
185283
+ writeMutablePackageManifest(context.packageRoot, manifest);
185284
+ success2(`${enabled ? "Enabled" : "Disabled"} API trigger in eigenpal.yaml.`);
185285
+ }
185286
+ function encryptLocalSecret(input) {
185287
+ if (input.key.byteLength !== 32)
185288
+ throw new Error("Secret encryption key must be 32 bytes.");
185289
+ const nonce = randomBytes(12);
185290
+ const cipher = createCipheriv("aes-256-gcm", input.key, nonce);
185291
+ cipher.setAAD(Buffer.from(JSON.stringify({
185292
+ keyId: input.keyId,
185293
+ organizationId: input.organizationId,
185294
+ sourcePath: input.sourcePath,
185295
+ secretName: input.secretName
185296
+ })));
185297
+ const ciphertext = Buffer.concat([cipher.update(input.plaintext, "utf8"), cipher.final()]);
185298
+ return {
185299
+ algorithm: "aes-256-gcm",
185300
+ keyId: input.keyId,
185301
+ nonce: nonce.toString("base64url"),
185302
+ ciphertext: ciphertext.toString("base64url"),
185303
+ tag: cipher.getAuthTag().toString("base64url")
185304
+ };
185305
+ }
185306
+ function readSecretsFile(packageRoot) {
185307
+ const filePath = path2.join(packageRoot, SOURCE_SECRETS_FILENAME);
185308
+ if (!existsSync3(filePath))
185309
+ return { schemaVersion: 1, secrets: {} };
185310
+ return SourceSecretsFileSchema.parse(readYamlFile(filePath));
185311
+ }
185312
+ function writeSecretsFile(packageRoot, value) {
185313
+ const secretsFile = SourceSecretsFileSchema.parse(value);
185314
+ writeYamlFile(path2.join(packageRoot, SOURCE_SECRETS_FILENAME), secretsFile);
185315
+ }
185316
+ async function readSecretInput(opts) {
185317
+ const selected = [opts.stdin, opts.valueFile].filter(Boolean).length;
185318
+ if (selected > 1)
185319
+ throw new Error("Pass only one of --stdin or --value-file.");
185320
+ if (opts.stdin)
185321
+ return readFileSync3(0, "utf8").replace(/\n$/, "");
185322
+ if (opts.valueFile)
185323
+ return readFileSync3(opts.valueFile, "utf8").replace(/\n$/, "");
185324
+ if (process.stdin.isTTY && process.stdout.isTTY) {
185325
+ const { password, isCancel, cancel } = await Promise.resolve().then(() => (init_dist4(), exports_dist));
185326
+ const answer = await password({ message: "Secret value" });
185327
+ if (isCancel(answer)) {
185328
+ cancel("Cancelled");
185329
+ process.exit(1);
185330
+ }
185331
+ return String(answer);
185332
+ }
185333
+ throw new Error("Secret value input is required in noninteractive mode.");
185334
+ }
185335
+ async function setSecret(name, opts) {
185336
+ const context = requirePackageContext(opts);
185337
+ const sourcePath = `${context.packagePath}/${SOURCE_SECRETS_FILENAME}`;
185338
+ const secretsFile = readSecretsFile(context.packageRoot);
185339
+ const secrets = { ...secretsFile.secrets };
185340
+ secrets[name] = {
185341
+ ...opts.description ? { description: opts.description } : {},
185342
+ encrypted: encryptLocalSecret({
185343
+ plaintext: await readSecretInput(opts),
185344
+ keyId: opts.keyId,
185345
+ key: readSecretKeyFile(opts.keyFile),
185346
+ organizationId: opts.organizationId,
185347
+ sourcePath,
185348
+ secretName: name
185349
+ })
185350
+ };
185351
+ writeSecretsFile(context.packageRoot, { schemaVersion: 1, secrets });
185352
+ success2(`Encrypted ${ui.bold(name)} into secrets.enc.yaml.`);
185353
+ }
185354
+ function unsetSecret(name, opts) {
185355
+ const context = requirePackageContext(opts);
185356
+ const secretsFile = readSecretsFile(context.packageRoot);
185357
+ const secrets = { ...secretsFile.secrets };
185358
+ delete secrets[name];
185359
+ writeSecretsFile(context.packageRoot, { schemaVersion: 1, secrets });
185360
+ success2(`Removed ${ui.bold(name)} from secrets.enc.yaml.`);
185361
+ }
185362
+ async function importSecrets(envFile, opts) {
185363
+ const content = readFileSync3(envFile, "utf8");
185364
+ for (const line of content.split(/\r?\n/)) {
185365
+ const trimmed = line.trim();
185366
+ if (!trimmed || trimmed.startsWith("#"))
185367
+ continue;
185368
+ const idx = trimmed.indexOf("=");
185369
+ if (idx === -1)
185370
+ continue;
185371
+ const context = requirePackageContext(opts);
185372
+ const sourcePath = `${context.packagePath}/${SOURCE_SECRETS_FILENAME}`;
185373
+ const secretsFile = readSecretsFile(context.packageRoot);
185374
+ const secrets = { ...secretsFile.secrets };
185375
+ const name = trimmed.slice(0, idx);
185376
+ secrets[name] = {
185377
+ encrypted: encryptLocalSecret({
185378
+ plaintext: trimmed.slice(idx + 1),
185379
+ keyId: opts.keyId,
185380
+ key: readSecretKeyFile(opts.keyFile),
185381
+ organizationId: opts.organizationId,
185382
+ sourcePath,
185383
+ secretName: name
185384
+ })
185385
+ };
185386
+ writeSecretsFile(context.packageRoot, { schemaVersion: 1, secrets });
185387
+ }
185388
+ }
185389
+ function readSecretKeyFile(keyFile) {
185390
+ return Buffer.from(readFileSync3(keyFile, "utf8").trim(), "base64url");
185391
+ }
185090
185392
  async function install(packageRef, opts) {
185091
185393
  const config2 = resolveConfig(opts);
185092
185394
  const context = resolveGitSourceContext();
@@ -185390,6 +185692,16 @@ function parseReleaseVersion(value) {
185390
185692
  }
185391
185693
  return value;
185392
185694
  }
185695
+ function parseBoolean(value) {
185696
+ if (value === "true" || value === "enabled" || value === "on")
185697
+ return true;
185698
+ if (value === "false" || value === "disabled" || value === "off")
185699
+ return false;
185700
+ throw new InvalidArgumentError("Expected true/false, enabled/disabled, or on/off");
185701
+ }
185702
+ function collect2(value, previous) {
185703
+ return [...previous, value];
185704
+ }
185393
185705
  function parseType(value) {
185394
185706
  if (!["agent", "workflow"].includes(value))
185395
185707
  throw new InvalidArgumentError("type must be agent or workflow");
@@ -185420,6 +185732,14 @@ function registerGitCommands(program2) {
185420
185732
  addJsonFlag(withBaseUrl(git.command("versions <package>"))).description("List package release versions.").action(action(versions2));
185421
185733
  withBaseUrl(git.command("release")).description("Create and push a package release tag.").argument("<version>", "Version or bump level", parseReleaseVersion).argument("[dir]", "Package directory").addOption(new Option("-m, --message <message>", "Annotated tag message").makeOptionMandatory()).action(action(async (versionOrBump, dir, opts) => release(versionOrBump, dir, opts)));
185422
185734
  withBaseUrl(git.command("sync [automation]")).description("Sync an automation from the latest Git source release.").option("--dir <dir>", "Directory to inspect").action(action(sync));
185735
+ const trigger = git.command("trigger").description("Edit trigger policy in eigenpal.yaml.");
185736
+ trigger.command("api <enabled>").description("Enable or disable API trigger policy in eigenpal.yaml.").option("--dir <dir>", "Directory to inspect").action(action(async (enabled, opts) => setApiTrigger(parseBoolean(enabled), opts)));
185737
+ const triggerEmail = trigger.command("email").description("Edit email trigger policy.");
185738
+ triggerEmail.command("set <email>").description("Add or update an email trigger alias in eigenpal.yaml.").option("--dir <dir>", "Directory to inspect").option("--disabled", "Write the alias as disabled").option("--allow <entry>", "Allowed sender entry; repeatable", collect2, []).option("--reply-on <never|always>", "Reply behavior").option("--reply-mode <sender|all>", "Reply recipient mode").option("--no-require-sender-auth", "Disable sender-auth checks for this alias").action(action(async (email3, opts) => setEmailTriggerAlias(email3, opts)));
185739
+ const secret = git.command("secret").description("Edit encrypted secrets.enc.yaml.");
185740
+ secret.command("set <name>").description("Encrypt and set a secret value in secrets.enc.yaml.").option("--dir <dir>", "Directory to inspect").option("--stdin", "Read the secret value from stdin").option("--value-file <path>", "Read the secret value from a file").requiredOption("--key-id <id>", "Organization decrypt key id").requiredOption("--key-file <path>", "File containing the base64url-encoded organization key").requiredOption("--organization-id <id>", "Organization id for authenticated data").option("--description <text>", "Secret description").action(action(setSecret));
185741
+ secret.command("unset <name>").description("Remove a secret from secrets.enc.yaml.").option("--dir <dir>", "Directory to inspect").action(action(async (name, opts) => unsetSecret(name, opts)));
185742
+ secret.command("import <env-file>").description("Import KEY=value entries from an env file into secrets.enc.yaml.").option("--dir <dir>", "Directory to inspect").requiredOption("--key-id <id>", "Organization decrypt key id").requiredOption("--key-file <path>", "File containing the base64url-encoded organization key").requiredOption("--organization-id <id>", "Organization id for authenticated data").action(action(importSecrets));
185423
185743
  }
185424
185744
 
185425
185745
  // src/commands/init.ts
@@ -186018,15 +186338,15 @@ function pruneEmptyDirs(root) {
186018
186338
  if (!existsSync5(root))
186019
186339
  return;
186020
186340
  const stack = [];
186021
- const collect2 = (dir) => {
186341
+ const collect3 = (dir) => {
186022
186342
  for (const entry of readdirSync3(dir)) {
186023
186343
  const full = join3(dir, entry);
186024
186344
  if (lstatSync(full).isDirectory())
186025
- collect2(full);
186345
+ collect3(full);
186026
186346
  }
186027
186347
  stack.push(dir);
186028
186348
  };
186029
- collect2(root);
186349
+ collect3(root);
186030
186350
  for (const dir of stack) {
186031
186351
  try {
186032
186352
  if (readdirSync3(dir).length === 0)
@@ -188043,6 +188363,155 @@ function parseWorkflow(yaml) {
188043
188363
  }
188044
188364
  return result.data;
188045
188365
  }
188366
+ // ../workflow-yaml/src/validate-quality.ts
188367
+ init_src();
188368
+ var CATEGORICAL_NAMES = new Set([
188369
+ "category",
188370
+ "subcategory",
188371
+ "kind",
188372
+ "status",
188373
+ "type",
188374
+ "subtype",
188375
+ "level",
188376
+ "severity",
188377
+ "priority",
188378
+ "state",
188379
+ "stage",
188380
+ "tier",
188381
+ "classification",
188382
+ "monitoring",
188383
+ "frequency",
188384
+ "reference",
188385
+ "sectionname",
188386
+ "code",
188387
+ "flag",
188388
+ "signal",
188389
+ "mode",
188390
+ "phase"
188391
+ ]);
188392
+ var SEPARATOR_LIST_RE = /(?:^|[\s(])((?:[A-Za-z0-9_-]{1,20})(?:\s*[|,/]\s*[A-Za-z0-9_-]{1,20}){1,7})(?:$|[\s).])/;
188393
+ function validateSchemaQuality(workflow2) {
188394
+ const warnings = [];
188395
+ for (let i5 = 0;i5 < workflow2.steps.length; i5++) {
188396
+ const step = workflow2.steps[i5];
188397
+ if (!step)
188398
+ continue;
188399
+ const basePath = `steps.${i5}`;
188400
+ walkStep(step, basePath, warnings);
188401
+ }
188402
+ return warnings;
188403
+ }
188404
+ function walkStep(step, basePath, out) {
188405
+ if (step.type === "ai.extract") {
188406
+ const schema = step.with?.schema;
188407
+ if (schema && typeof schema === "object") {
188408
+ walkSchema(schema, `${basePath}.with.schema`, out);
188409
+ }
188410
+ return;
188411
+ }
188412
+ if (step.type === "transform.script") {
188413
+ const cfg = step.with;
188414
+ const source2 = typeof cfg?.function === "string" ? cfg.function : null;
188415
+ if (!source2)
188416
+ return;
188417
+ const paramNames = cfg?.inputs && typeof cfg.inputs === "object" && !Array.isArray(cfg.inputs) ? Object.keys(cfg.inputs) : [];
188418
+ const compiled = compileTypedScript({ kind: "transform", source: source2, paramNames });
188419
+ if (!compiled.ok)
188420
+ return;
188421
+ for (const w10 of compiled.warnings) {
188422
+ if (w10.code === "weak-return-type") {
188423
+ out.push({
188424
+ code: "weak-script-return",
188425
+ field: `${basePath}.with.function`,
188426
+ message: `transform.script return type is too loose: ${w10.message}`,
188427
+ hint: 'Replace `unknown`/`any` with a concrete shape, or use a TS literal union (e.g. `"low" | "medium" | "high"`) when the value belongs to a closed set.'
188428
+ });
188429
+ }
188430
+ }
188431
+ const schema = compiled.result.returnSchema;
188432
+ if (schema.type === "object" && (!schema.properties || Object.keys(schema.properties).length === 0) && schema.additionalProperties !== false) {
188433
+ out.push({
188434
+ code: "untyped-object",
188435
+ field: `${basePath}.with.function`,
188436
+ message: "transform.script returns an untyped object. Downstream steps cannot autocomplete its fields.",
188437
+ hint: "Describe the actual shape in the return annotation: `function script(...): { name: string; total: number }`."
188438
+ });
188439
+ }
188440
+ return;
188441
+ }
188442
+ const withAny = step.with;
188443
+ const nestedSteps = withAny?.steps;
188444
+ if (Array.isArray(nestedSteps)) {
188445
+ for (let i5 = 0;i5 < nestedSteps.length; i5++) {
188446
+ const s5 = nestedSteps[i5];
188447
+ if (s5)
188448
+ walkStep(s5, `${basePath}.with.steps.${i5}`, out);
188449
+ }
188450
+ }
188451
+ const nestedBranches = withAny?.branches;
188452
+ if (Array.isArray(nestedBranches)) {
188453
+ for (let b8 = 0;b8 < nestedBranches.length; b8++) {
188454
+ const branch = nestedBranches[b8];
188455
+ if (branch && Array.isArray(branch.steps)) {
188456
+ for (let i5 = 0;i5 < branch.steps.length; i5++) {
188457
+ const s5 = branch.steps[i5];
188458
+ if (s5)
188459
+ walkStep(s5, `${basePath}.with.branches.${b8}.steps.${i5}`, out);
188460
+ }
188461
+ }
188462
+ }
188463
+ }
188464
+ }
188465
+ function walkSchema(schema, basePath, out) {
188466
+ const type = schema.type;
188467
+ if (type === "object" && (!schema.properties || Object.keys(schema.properties).length === 0) && schema.additionalProperties !== false) {
188468
+ out.push({
188469
+ code: "untyped-object",
188470
+ field: basePath,
188471
+ message: "Object schema has no properties. The LLM has no guidance on what fields to emit and downstream autocomplete is empty.",
188472
+ hint: "Add concrete fields under `properties`, or set `additionalProperties: false` if the object is intentionally open."
188473
+ });
188474
+ }
188475
+ if (type === "array" && (schema.items === undefined || isEmptyObject(schema.items))) {
188476
+ out.push({
188477
+ code: "untyped-array-items",
188478
+ field: basePath,
188479
+ message: "Array items are untyped. The LLM does not know what shape each element should take.",
188480
+ hint: "Add an `items:` block describing the element type, e.g. `items: { type: object, properties: { ... } }`."
188481
+ });
188482
+ }
188483
+ if (schema.properties && typeof schema.properties === "object") {
188484
+ for (const [name, raw] of Object.entries(schema.properties)) {
188485
+ if (!raw || typeof raw !== "object")
188486
+ continue;
188487
+ const prop = raw;
188488
+ const propType = prop.type;
188489
+ const propEnum = Array.isArray(prop.enum) && prop.enum.length > 0;
188490
+ const lower = name.toLowerCase();
188491
+ const description = typeof prop.description === "string" ? prop.description : "";
188492
+ const looksCategoricalByName = CATEGORICAL_NAMES.has(lower);
188493
+ const looksCategoricalByDescription = description.length > 0 && SEPARATOR_LIST_RE.test(description);
188494
+ if (propType === "string" && !propEnum && (looksCategoricalByName || looksCategoricalByDescription) && !looksLikeFreeform(prop)) {
188495
+ out.push({
188496
+ code: "categorical-missing-enum",
188497
+ field: `${basePath}.properties.${name}`,
188498
+ message: `Field \`${name}\` looks categorical but has no \`enum\`. Coded values extract more reliably when the LLM is constrained to a closed set.`,
188499
+ hint: "In the workflow builder, switch the field type to `Enum` and list the allowed values. In raw YAML, add `enum: ['value1', 'value2', ...]` to the field, and write the per-value meanings into the field's `description`."
188500
+ });
188501
+ }
188502
+ walkSchema(prop, `${basePath}.properties.${name}`, out);
188503
+ }
188504
+ }
188505
+ if (schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
188506
+ walkSchema(schema.items, `${basePath}.items`, out);
188507
+ }
188508
+ }
188509
+ function isEmptyObject(v11) {
188510
+ return typeof v11 === "object" && v11 !== null && !Array.isArray(v11) && Object.keys(v11).length === 0;
188511
+ }
188512
+ function looksLikeFreeform(prop) {
188513
+ return prop.pattern !== undefined || prop.format !== undefined;
188514
+ }
188046
188515
  // src/commands/validate.ts
188047
188516
  var import_yaml6 = __toESM(require_dist(), 1);
188048
188517
  import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
@@ -188061,24 +188530,46 @@ function isDirSafe(path15, issues, field) {
188061
188530
  }
188062
188531
  function validateWorkflowYaml(path15) {
188063
188532
  if (!existsSync10(path15)) {
188064
- return [{ field: path15, message: "File not found." }];
188533
+ return { issues: [{ field: path15, message: "File not found." }], warnings: [] };
188065
188534
  }
188066
188535
  const text = readFileSync10(path15, "utf-8");
188067
188536
  try {
188068
- parseWorkflow(text);
188069
- return [];
188537
+ const definition = parseWorkflow(text);
188538
+ let warnings = [];
188539
+ try {
188540
+ warnings = validateSchemaQuality(definition);
188541
+ } catch {
188542
+ warnings = [];
188543
+ }
188544
+ return { issues: [], warnings };
188070
188545
  } catch (err2) {
188071
188546
  if (err2 instanceof YamlParseError) {
188072
188547
  const where = err2.line !== undefined ? ` (line ${err2.line})` : "";
188073
- return [{ field: "yaml", message: `${err2.message}${where}` }];
188548
+ return { issues: [{ field: "yaml", message: `${err2.message}${where}` }], warnings: [] };
188074
188549
  }
188075
188550
  if (err2 instanceof WorkflowValidationError) {
188076
- return err2.errors.map((e8) => ({
188077
- field: e8.path.length > 0 ? e8.path.map(String).join(".") : "(root)",
188078
- message: e8.message
188079
- }));
188551
+ return {
188552
+ issues: err2.errors.map((e8) => ({
188553
+ field: e8.path.length > 0 ? e8.path.map(String).join(".") : "(root)",
188554
+ message: e8.message
188555
+ })),
188556
+ warnings: []
188557
+ };
188080
188558
  }
188081
- return [{ field: "(root)", message: err2 instanceof Error ? err2.message : String(err2) }];
188559
+ return {
188560
+ issues: [{ field: "(root)", message: err2 instanceof Error ? err2.message : String(err2) }],
188561
+ warnings: []
188562
+ };
188563
+ }
188564
+ }
188565
+ function printSchemaQualityWarnings(path15, warnings) {
188566
+ if (warnings.length === 0)
188567
+ return;
188568
+ warn(`workflow.yaml ${ui.dim(`(${path15})`)}: ${warnings.length} schema-quality warning${warnings.length === 1 ? "" : "s"}`);
188569
+ for (const w10 of warnings) {
188570
+ warn(` ${ui.dim(w10.field)} ${w10.message}`);
188571
+ if (w10.hint)
188572
+ warn(` ${ui.dim("hint:")} ${w10.hint}`);
188082
188573
  }
188083
188574
  }
188084
188575
  function validateEvaluatorsYaml(path15) {
@@ -188261,9 +188752,23 @@ function rootDir(opts) {
188261
188752
  return resolve6(opts.dir ?? process.cwd());
188262
188753
  }
188263
188754
  function registerAllValidateCommand(parent) {
188264
- parent.command("validate").description("Local-only validation against the templated project layout: ./workflow.yaml + ./evaluators.yaml + ./dataset/. For targeted validation use `evaluators validate` or `dataset validate`.").option("--dir <path>", "Project root (defaults to cwd; resolves the three default paths from here)").action((opts) => {
188755
+ parent.command("validate [path]").description("Local-only validation. Without [path]: runs the templated three-way check (./workflow.yaml + ./evaluators.yaml + ./dataset/). With [path] pointing at a YAML file: validates just that workflow.yaml. For per-noun targeting use `evaluators validate` or `dataset validate`.").option("--dir <path>", "Project root (defaults to cwd; resolves the three default paths from here)").action((path15, opts) => {
188756
+ if (path15) {
188757
+ const target = resolve6(path15);
188758
+ const wfResult2 = validateWorkflowYaml(target);
188759
+ const wfOk2 = printIssues("workflow.yaml", target, wfResult2.issues);
188760
+ if (wfOk2)
188761
+ printSchemaQualityWarnings(target, wfResult2.warnings);
188762
+ if (!wfOk2)
188763
+ process.exit(1);
188764
+ return;
188765
+ }
188265
188766
  const root2 = rootDir(opts);
188266
- const wfOk = printIssues("workflow.yaml", relPath(root2, join12(root2, "workflow.yaml")), validateWorkflowYaml(join12(root2, "workflow.yaml")));
188767
+ const wfResult = validateWorkflowYaml(join12(root2, "workflow.yaml"));
188768
+ const wfPath = relPath(root2, join12(root2, "workflow.yaml"));
188769
+ const wfOk = printIssues("workflow.yaml", wfPath, wfResult.issues);
188770
+ if (wfOk)
188771
+ printSchemaQualityWarnings(wfPath, wfResult.warnings);
188267
188772
  const evOk = printIssues("evaluators.yaml", relPath(root2, join12(root2, "evaluators.yaml")), validateEvaluatorsYaml(join12(root2, "evaluators.yaml")));
188268
188773
  const dsOk = printIssues("dataset/", relPath(root2, join12(root2, "dataset")), validateDatasetFolder(join12(root2, "dataset")));
188269
188774
  if (!wfOk || !evOk || !dsOk)
@@ -188916,6 +189421,7 @@ flag would otherwise shadow it. Validation failures come back as
188916
189421
  return;
188917
189422
  }
188918
189423
  success2(`Pushed ${ui.bold(workflowId)} (v${result.version ?? nextVersion})`);
189424
+ printSchemaQualityWarnings2(result.warnings);
188919
189425
  } else {
188920
189426
  if (opts.bump) {
188921
189427
  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.");
@@ -188933,6 +189439,7 @@ flag would otherwise shadow it. Validation failures come back as
188933
189439
  const id2 = result.workflow?.id ?? result.id ?? "?";
188934
189440
  const ver = result.version?.version ?? result.workflow?.currentVersion?.version ?? result.currentVersion?.version ?? "?";
188935
189441
  success2(`Created ${ui.bold(id2)} (v${ver})`);
189442
+ printSchemaQualityWarnings2(result.warnings);
188936
189443
  }
188937
189444
  }));
188938
189445
  const moveCmd = parent.command("move <workflow-id>").description("Move a workflow to a folder path, creating folders as needed").requiredOption("--folder <path>", "Target folder path (`/` for root)");
@@ -188950,6 +189457,16 @@ flag would otherwise shadow it. Validation failures come back as
188950
189457
  }));
188951
189458
  }
188952
189459
  var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
189460
+ function printSchemaQualityWarnings2(warnings) {
189461
+ if (!warnings || warnings.length === 0)
189462
+ return;
189463
+ warn(`${warnings.length} schema-quality warning${warnings.length === 1 ? "" : "s"} (push succeeded; sharper types make downstream steps easier to wire):`);
189464
+ for (const w10 of warnings) {
189465
+ warn(` ${ui.dim(w10.field)} ${w10.message}`);
189466
+ if (w10.hint)
189467
+ warn(` ${ui.dim("hint:")} ${w10.hint}`);
189468
+ }
189469
+ }
188953
189470
  function isBumpLevel(value) {
188954
189471
  return value === "patch" || value === "minor" || value === "major";
188955
189472
  }
@@ -20,7 +20,7 @@ Manage workflows: push, pull, run, evaluate.
20
20
  - [`eigenpal workflow pull [options] <workflow-id>`](#eigenpal-workflow-pull-options-workflow-id)
21
21
  - [`eigenpal workflow push [options]`](#eigenpal-workflow-push-options)
22
22
  - [`eigenpal workflow move [options] <workflow-id>`](#eigenpal-workflow-move-options-workflow-id)
23
- - [`eigenpal workflow validate [options]`](#eigenpal-workflow-validate-options)
23
+ - [`eigenpal workflow validate [options] [path]`](#eigenpal-workflow-validate-options-path)
24
24
  - [`eigenpal workflow clear-local [options] [examples...]`](#eigenpal-workflow-clear-local-options-examples)
25
25
  - [`eigenpal workflow evaluators pull [options] <workflow-id>`](#eigenpal-workflow-evaluators-pull-options-workflow-id)
26
26
  - [`eigenpal workflow evaluators push [options] <workflow-id>`](#eigenpal-workflow-evaluators-push-options-workflow-id)
@@ -100,7 +100,7 @@ workflow
100
100
  ├── evaluator-type
101
101
  │ ├── list|ls
102
102
  │ └── get <type>
103
- ├── validate
103
+ ├── validate [path]
104
104
  ├── clear-local [examples...]
105
105
  └── step
106
106
  └── exec <type>
@@ -110,14 +110,14 @@ workflow
110
110
 
111
111
  ### Core
112
112
 
113
- | Command | Description |
114
- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
115
- | `eigenpal workflow list | ls [options]` | List workflows the caller can read. |
116
- | `eigenpal workflow pull [options] <workflow-id>` | Download the YAML definition of the workflow at its current version. |
117
- | `eigenpal workflow push [options]` | Create or update a workflow from a YAML file. |
118
- | `eigenpal workflow move [options] <workflow-id>` | Move a workflow to a folder path, creating folders as needed |
119
- | `eigenpal workflow validate [options]` | Local-only validation against the templated project layout: ./workflow.yaml + ./evaluators.yaml + ./dataset/. For targeted validation use `evaluators validate` or `dataset validate`. |
120
- | `eigenpal workflow clear-local [options] [examples...]` | Delete local execution artifacts under ./dataset/examples/. Keeps the latest run per example by default. |
113
+ | Command | Description |
114
+ | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
115
+ | `eigenpal workflow list | ls [options]` | List workflows the caller can read. |
116
+ | `eigenpal workflow pull [options] <workflow-id>` | Download the YAML definition of the workflow at its current version. |
117
+ | `eigenpal workflow push [options]` | Create or update a workflow from a YAML file. |
118
+ | `eigenpal workflow move [options] <workflow-id>` | Move a workflow to a folder path, creating folders as needed |
119
+ | `eigenpal workflow validate [options] [path]` | Local-only validation. Without [path]: runs the templated three-way check (./workflow.yaml + ./evaluators.yaml + ./dataset/). With [path] pointing at a YAML file: validates just that workflow.yaml. For per-noun targeting use `evaluators validate` or `dataset validate`. |
120
+ | `eigenpal workflow clear-local [options] [examples...]` | Delete local execution artifacts under ./dataset/examples/. Keeps the latest run per example by default. |
121
121
 
122
122
  ### Evaluators
123
123
 
@@ -256,9 +256,15 @@ Move a workflow to a folder path, creating folders as needed
256
256
  | `--base-url <url>` | no | | Server base URL |
257
257
  | `--json` | no | | Output the raw server response as JSON |
258
258
 
259
- ### `eigenpal workflow validate [options]`
259
+ ### `eigenpal workflow validate [options] [path]`
260
260
 
261
- Local-only validation against the templated project layout: ./workflow.yaml + ./evaluators.yaml + ./dataset/. For targeted validation use `evaluators validate` or `dataset validate`.
261
+ Local-only validation. Without [path]: runs the templated three-way check (./workflow.yaml + ./evaluators.yaml + ./dataset/). With [path] pointing at a YAML file: validates just that workflow.yaml. For per-noun targeting use `evaluators validate` or `dataset validate`.
262
+
263
+ ### Arguments
264
+
265
+ | Name | Required | Variadic | Description |
266
+ | ------ | -------- | -------- | ----------- |
267
+ | `path` | no | no | |
262
268
 
263
269
  ### Options
264
270
 
@@ -191,6 +191,76 @@ The script runs in a WASM sandbox: 5s wall clock, 10 MB heap, no network,
191
191
  no filesystem. Trips show up as `script_timeout` / `script_memory_limit`
192
192
  on the step execution.
193
193
 
194
+ ## Be specific with types
195
+
196
+ Every output schema you write is read three times: by the LLM (for
197
+ `ai.extract`), by downstream steps (autocomplete + template resolution),
198
+ and by the runtime validator. **The more specific the type, the better
199
+ the workflow runs.**
200
+
201
+ ### Use `enum` for coded values
202
+
203
+ Categorical fields like `category`, `status`, `kind`, `severity`,
204
+ `monitoring`, `frequency` should be enums whenever the value set is
205
+ closed. The LLM is constrained to emit only allowed values, and
206
+ downstream `transform.script` steps know exactly what they'll see.
207
+
208
+ ```yaml
209
+ - name: classify
210
+ type: ai.extract
211
+ with:
212
+ input: '{{ input.text }}'
213
+ schema:
214
+ type: object
215
+ properties:
216
+ category:
217
+ type: string
218
+ enum: ['901', '902']
219
+ description: |
220
+ Covenant category.
221
+ 901 = Odkladacie podmienky (conditions precedent).
222
+ 902 = Následné podmienky (subsequent / ongoing covenants).
223
+ required: [category]
224
+ ```
225
+
226
+ Per-value meanings go inline in the field's `description`. The LLM picks
227
+ them up reliably from there, and the scope browser renders the allowed
228
+ values when downstream steps reference the field.
229
+
230
+ ### `transform.script` — TS literal unions become JSON Schema enums
231
+
232
+ A TS literal-union return type compiles to a JSON-Schema enum
233
+ automatically. Use it when the function returns a value from a closed set:
234
+
235
+ ```yaml
236
+ - name: severity
237
+ type: transform.script
238
+ with:
239
+ inputs:
240
+ score: '{{ steps.score.output.value }}'
241
+ function: |
242
+ function script(score: number): { level: 'low' | 'medium' | 'high' } {
243
+ return { level: score > 0.8 ? 'high' : score > 0.4 ? 'medium' : 'low' };
244
+ }
245
+ ```
246
+
247
+ Downstream steps autocomplete `low | medium | high` for `level`.
248
+
249
+ ### `eigenpal workflow push` warns on weak types
250
+
251
+ Push surfaces non-fatal **schema-quality warnings** when it sees patterns
252
+ that hurt downstream typing. Common ones:
253
+
254
+ | Warning code | What it means |
255
+ | ---------------------------- | -------------------------------------------------------------------------------------------- |
256
+ | `categorical-missing-enum` | A field named like `category` / `status` / `kind` is a plain `string`. Consider declaring an `enum`. |
257
+ | `weak-script-return` | A `transform.script` returns `any` or `unknown`. Replace with a concrete shape or a literal union. |
258
+ | `untyped-object` | A `type: object` field has no `properties`. The LLM has no guidance on what to emit. |
259
+ | `untyped-array-items` | A `type: array` field has no `items:` declared. |
260
+
261
+ Warnings do not block the push. They print to stderr as `! …` lines. Run
262
+ `eigenpal workflow validate` locally to surface them before pushing.
263
+
194
264
  ## Common patterns
195
265
 
196
266
  ### Conditional branches
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eigenpal/cli",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
5
5
  "keywords": [
6
6
  "eigenpal",
@@ -40,6 +40,8 @@
40
40
  "scripts": {
41
41
  "dev": "bun src/cli.ts",
42
42
  "build": "bun run generate:skill && bun build src/cli.ts --outdir dist --target node --format esm && rm -rf dist/templates dist/skill && cp -R src/templates dist/templates && cp -R src/skill dist/skill",
43
+ "generate": "bun run generate:cli-docs && bun run generate:skill",
44
+ "generate:check": "bun run generate:cli-docs:check && bun run generate:skill:check",
43
45
  "generate:skill": "bun scripts/generate-skill-reference.ts",
44
46
  "generate:skill:check": "bun scripts/generate-skill-reference.ts --check",
45
47
  "generate:cli-docs": "bun scripts/generate-cli-docs.ts",