@sweny-ai/core 0.1.97 → 0.1.99

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.
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * All errors are swallowed: cloud reporting MUST NEVER block the workflow.
17
17
  */
18
- import type { NodeResult, Observer, Workflow } from "../types.js";
18
+ import type { NodeResult, Observer, Workflow, WorkflowInputs } from "../types.js";
19
19
  export type TriggerSource = "github_action" | "gitlab_ci" | "circleci" | "buildkite" | "other_ci" | "manual" | "cron" | "mcp" | "unknown";
20
20
  export interface CloudReportConfig {
21
21
  cloudToken: string;
@@ -29,6 +29,17 @@ export interface StartRunInput {
29
29
  trigger_metadata?: Record<string, unknown>;
30
30
  metadata?: Record<string, unknown>;
31
31
  run_uuid?: string;
32
+ /**
33
+ * Declared input shape for this run: key name → declared (or observed)
34
+ * type. Values are intentionally NEVER included; per the workflow spec
35
+ * (Telemetry shape), cloud renderers may display the input contract a
36
+ * run was invoked with but must not receive the values themselves. A
37
+ * workflow that takes a token as input therefore never leaks it.
38
+ *
39
+ * Omitted when the caller didn't pass an inputs shape (e.g. legacy
40
+ * call sites pre-dating the inputs contract).
41
+ */
42
+ inputs_shape?: Record<string, string>;
32
43
  }
33
44
  export interface StartRunResult {
34
45
  run_id: string;
@@ -61,10 +72,18 @@ export declare function newRunUuid(): string;
61
72
  /**
62
73
  * Build a run-start payload from a Workflow + env. Pure function; the caller
63
74
  * is responsible for passing it to `startRun()`.
75
+ *
76
+ * `inputsShape` (when provided) ships the key-name → declared-type map for
77
+ * the run. Values are NEVER transmitted; callers compute the shape via
78
+ * `summarizeInputShape` (or pass the workflow's declared `inputs` block
79
+ * and resolved input bag and let this helper compute it). See the
80
+ * Telemetry shape rule in spec/src/content/docs/workflow.mdx.
64
81
  */
65
82
  export declare function buildStartRunPayload(workflow: Pick<Workflow, "id" | "workflow_type">, options: {
66
83
  runUuid: string;
67
84
  env?: NodeJS.ProcessEnv;
85
+ declaredInputs?: WorkflowInputs;
86
+ resolvedInputs?: Record<string, unknown>;
68
87
  }): StartRunInput;
69
88
  /**
70
89
  * POST /api/runs — start a run, returning the run id (or null on failure).
@@ -120,7 +139,10 @@ export interface CloudLifecycleHandle {
120
139
  export declare function beginCloudLifecycle(config: {
121
140
  cloudToken?: string;
122
141
  repository?: string;
123
- }, workflow: Pick<Workflow, "id" | "workflow_type">): Promise<CloudLifecycleHandle | null>;
142
+ }, workflow: Pick<Workflow, "id" | "workflow_type">, options?: {
143
+ declaredInputs?: WorkflowInputs;
144
+ resolvedInputs?: Record<string, unknown>;
145
+ }): Promise<CloudLifecycleHandle | null>;
124
146
  /**
125
147
  * Close a cloud lifecycle session opened by `beginCloudLifecycle`.
126
148
  *
@@ -16,6 +16,7 @@
16
16
  * All errors are swallowed: cloud reporting MUST NEVER block the workflow.
17
17
  */
18
18
  import { randomUUID } from "node:crypto";
19
+ import { summarizeInputShape } from "../inputs.js";
19
20
  const CLOUD_URL_DEFAULT = "https://cloud.sweny.ai";
20
21
  const TIMEOUT_MS = 10_000;
21
22
  // startRun runs on the critical path BEFORE execute(), so a slow cloud
@@ -85,6 +86,12 @@ export function newRunUuid() {
85
86
  /**
86
87
  * Build a run-start payload from a Workflow + env. Pure function; the caller
87
88
  * is responsible for passing it to `startRun()`.
89
+ *
90
+ * `inputsShape` (when provided) ships the key-name → declared-type map for
91
+ * the run. Values are NEVER transmitted; callers compute the shape via
92
+ * `summarizeInputShape` (or pass the workflow's declared `inputs` block
93
+ * and resolved input bag and let this helper compute it). See the
94
+ * Telemetry shape rule in spec/src/content/docs/workflow.mdx.
88
95
  */
89
96
  export function buildStartRunPayload(workflow, options) {
90
97
  const env = options.env ?? process.env;
@@ -97,7 +104,7 @@ export function buildStartRunPayload(workflow, options) {
97
104
  metadata.commit_sha = env.GITHUB_SHA;
98
105
  if (env.GITHUB_ACTOR)
99
106
  metadata.actor = env.GITHUB_ACTOR;
100
- return {
107
+ const payload = {
101
108
  workflow_id: workflow.id,
102
109
  workflow_type: workflow.workflow_type ?? "generic",
103
110
  trigger_source,
@@ -105,6 +112,17 @@ export function buildStartRunPayload(workflow, options) {
105
112
  metadata,
106
113
  run_uuid: options.runUuid,
107
114
  };
115
+ // Compute the shape when caller provided either a declaration or a
116
+ // resolved bag. Omit the key entirely when there's nothing to summarize
117
+ // so old cloud builds that don't model `inputs_shape` aren't surprised
118
+ // by an empty object.
119
+ if (options.declaredInputs || options.resolvedInputs) {
120
+ const shape = summarizeInputShape(options.declaredInputs, options.resolvedInputs);
121
+ if (Object.keys(shape).length > 0) {
122
+ payload.inputs_shape = shape;
123
+ }
124
+ }
125
+ return payload;
108
126
  }
109
127
  /**
110
128
  * POST /api/runs — start a run, returning the run id (or null on failure).
@@ -204,11 +222,15 @@ export function deriveGenericMetrics(results, durationMs) {
204
222
  * Pair every successful call with `finishCloudLifecycle(handle, …)` once the
205
223
  * workflow completes (success or failure).
206
224
  */
207
- export async function beginCloudLifecycle(config, workflow) {
225
+ export async function beginCloudLifecycle(config, workflow, options) {
208
226
  if (!config.cloudToken)
209
227
  return null;
210
228
  const runUuid = newRunUuid();
211
- const payload = buildStartRunPayload(workflow, { runUuid });
229
+ const payload = buildStartRunPayload(workflow, {
230
+ runUuid,
231
+ declaredInputs: options?.declaredInputs,
232
+ resolvedInputs: options?.resolvedInputs,
233
+ });
212
234
  const reportConfig = {
213
235
  cloudToken: config.cloudToken,
214
236
  repository: config.repository,
@@ -52,6 +52,12 @@ export interface CliConfig {
52
52
  repositoryOwner: string;
53
53
  json: boolean;
54
54
  stream: boolean;
55
+ /**
56
+ * When true, print each tool call's input and output inline (human-readable,
57
+ * truncated). Adds a verbose tool observer alongside the spinner/progress
58
+ * observer. Use `stream` if you need full untruncated NDJSON instead.
59
+ */
60
+ verbose: boolean;
55
61
  bell: boolean;
56
62
  cacheDir: string;
57
63
  cacheTtl: number;
@@ -56,6 +56,7 @@ export function registerTriageCommand(program) {
56
56
  .option("--gitlab-base-url <url>", "GitLab base URL (default: https://gitlab.com)")
57
57
  .option("--json", "Output results as JSON", false)
58
58
  .option("--stream", "Stream NDJSON events to stdout (for Studio / automation)", false)
59
+ .option("--verbose", "Print each tool call's input and output inline (human-readable, truncated). Use --stream for full untruncated NDJSON.", false)
59
60
  .option("--bell", "Ring terminal bell on completion", false)
60
61
  .option("--cache-dir <path>", "Step cache directory (default: .sweny/cache)")
61
62
  .option("--cache-ttl <seconds>", "Cache TTL in seconds, 0 = infinite (default: 86400)")
@@ -163,6 +164,7 @@ export function parseCliInputs(options, fileConfig = {}) {
163
164
  // Per-invocation flags: CLI only
164
165
  json: Boolean(options.json),
165
166
  stream: Boolean(options.stream),
167
+ verbose: Boolean(options.verbose),
166
168
  bell: Boolean(options.bell),
167
169
  cacheDir: options.cacheDir || env.SWENY_CACHE_DIR || f("cache-dir") || ".sweny/cache",
168
170
  cacheTtl: parsePositiveInt(options.cacheTtl ?? f("cache-ttl"), 86400),
@@ -638,6 +640,7 @@ export function registerImplementCommand(program) {
638
640
  .option("--review-mode <mode>", "PR merge behavior: auto (GitHub auto-merge when CI passes) | review (human approval, default)", "review")
639
641
  .option("--additional-instructions <text>", "Extra instructions for the coding agent")
640
642
  .option("--stream", "Stream NDJSON events to stdout (for Studio / automation)", false)
643
+ .option("--verbose", "Print each tool call's input and output inline (human-readable, truncated). Use --stream for full untruncated NDJSON.", false)
641
644
  .option("--offline", "Skip URL Sources and fail fast if any are referenced", false);
642
645
  }
643
646
  /**
package/dist/cli/main.js CHANGED
@@ -15,6 +15,7 @@ import { configuredSkills, configuredSkillsWithDiagnostics } from "../skills/cus
15
15
  import { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "../mcp.js";
16
16
  import { loadAdditionalContext } from "../templates.js";
17
17
  import { loadAndValidateWorkflow } from "../loader.js";
18
+ import { validateRuntimeInput } from "../inputs.js";
18
19
  import { stringify as stringifyYaml } from "yaml";
19
20
  import { buildWorkflow, refineWorkflow } from "../workflow-builder.js";
20
21
  import { toMermaidBlock } from "../mermaid.js";
@@ -355,11 +356,18 @@ triageCmd.action(async (options) => {
355
356
  // node-streaming observer can attach events to the correct runId.
356
357
  // Null when the token is unset or startRun fails — cloud reporting
357
358
  // must never block the workflow.
358
- const cloudHandle = await beginCloudLifecycle(config, triageWorkflow);
359
+ //
360
+ // Cloud sees the input *shape* (key names + observed types), never
361
+ // the values. Per workflow spec Telemetry shape rule, a token in the
362
+ // bag never leaves the host.
363
+ const cloudHandle = await beginCloudLifecycle(config, triageWorkflow, {
364
+ declaredInputs: triageWorkflow.inputs,
365
+ resolvedInputs: workflowInput,
366
+ });
359
367
  if (cloudHandle?.dashboardUrl) {
360
368
  console.log(c.subtle(` cloud: ${cloudHandle.dashboardUrl}`));
361
369
  }
362
- const observer = composeObservers(progressObserver, config.stream ? createStreamObserver() : undefined, createCloudStreamObserver(config, cloudHandle));
370
+ const observer = composeObservers(progressObserver, config.verbose ? createVerboseToolObserver() : undefined, config.stream ? createStreamObserver() : undefined, createCloudStreamObserver(config, cloudHandle));
363
371
  try {
364
372
  const { results, trace } = await execute(triageWorkflow, workflowInput, {
365
373
  skills,
@@ -527,12 +535,15 @@ implementCmd.action(async (issueId, options) => {
527
535
  };
528
536
  // Open cloud lifecycle session BEFORE composing observers so the
529
537
  // node-streaming observer can attach to the correct runId. See
530
- // triage path for rationale.
531
- const implCloudHandle = await beginCloudLifecycle(config, implementWorkflow);
538
+ // triage path for rationale. Cloud sees the input *shape* only.
539
+ const implCloudHandle = await beginCloudLifecycle(config, implementWorkflow, {
540
+ declaredInputs: implementWorkflow.inputs,
541
+ resolvedInputs: workflowInput,
542
+ });
532
543
  if (implCloudHandle?.dashboardUrl) {
533
544
  console.log(c.subtle(` cloud: ${implCloudHandle.dashboardUrl}`));
534
545
  }
535
- const observer = composeObservers(implProgressObserver, Boolean(options.stream) ? createStreamObserver() : undefined, createCloudStreamObserver(config, implCloudHandle));
546
+ const observer = composeObservers(implProgressObserver, config.verbose ? createVerboseToolObserver() : undefined, Boolean(options.stream) ? createStreamObserver() : undefined, createCloudStreamObserver(config, implCloudHandle));
536
547
  try {
537
548
  const { results } = await execute(implementWorkflow, workflowInput, {
538
549
  skills,
@@ -720,14 +731,39 @@ export async function workflowRunAction(file, options) {
720
731
  // Build workflow input — prefer --input JSON if provided, else fall back to config-derived input
721
732
  let workflowInput;
722
733
  if (options.input && typeof options.input === "string") {
734
+ let parsed;
723
735
  try {
724
- workflowInput = JSON.parse(options.input);
736
+ parsed = JSON.parse(options.input);
725
737
  }
726
738
  catch {
727
739
  console.error(chalk.red(" --input must be valid JSON"));
728
740
  process.exit(1);
729
741
  return;
730
742
  }
743
+ // Validate against the workflow's declared `inputs` contract (when present).
744
+ // Workflows without an `inputs` block pass through unchanged.
745
+ const validated = validateRuntimeInput(workflow.inputs, parsed);
746
+ if (!validated.ok) {
747
+ console.error(chalk.red(`\n --input does not match workflow "${workflow.id}" inputs contract:\n`));
748
+ for (const err of validated.errors) {
749
+ console.error(chalk.red(` ✗ ${err.field}: ${err.message}`));
750
+ }
751
+ if (workflow.inputs) {
752
+ console.error(chalk.dim(`\n Declared inputs:`));
753
+ for (const [name, field] of Object.entries(workflow.inputs)) {
754
+ const flags = [field.type];
755
+ if (field.required)
756
+ flags.push("required");
757
+ if (field.default !== undefined)
758
+ flags.push(`default=${JSON.stringify(field.default)}`);
759
+ console.error(chalk.dim(` - ${name} (${flags.join(", ")})${field.description ? `: ${field.description}` : ""}`));
760
+ }
761
+ }
762
+ console.error("");
763
+ process.exit(1);
764
+ return;
765
+ }
766
+ workflowInput = validated.value;
731
767
  }
732
768
  else {
733
769
  workflowInput = {
@@ -748,11 +784,33 @@ export async function workflowRunAction(file, options) {
748
784
  }),
749
785
  context: buildProviderCtx(config, mcpServers),
750
786
  };
787
+ // If the workflow declared `inputs`, apply defaults to the config-derived bag.
788
+ // This lets a CI caller omit --input entirely and still get the documented
789
+ // defaults filled in (e.g. since_tag/until_tag pulled from a release-notes
790
+ // workflow's declaration).
791
+ if (workflow.inputs) {
792
+ const validated = validateRuntimeInput(workflow.inputs, workflowInput);
793
+ if (!validated.ok) {
794
+ console.error(chalk.red(`\n Workflow "${workflow.id}" declares required inputs not satisfied by default config:\n`));
795
+ for (const err of validated.errors) {
796
+ console.error(chalk.red(` ✗ ${err.field}: ${err.message}`));
797
+ }
798
+ console.error(chalk.dim(`\n Pass them via --input '{ ... }'.\n`));
799
+ process.exit(1);
800
+ return;
801
+ }
802
+ workflowInput = validated.value;
803
+ }
751
804
  }
752
805
  // Open cloud lifecycle session BEFORE composing observers so the
753
806
  // node-streaming observer can attach to the correct runId. See
754
- // triage path for rationale.
755
- const wfCloudHandle = await beginCloudLifecycle(config, workflow);
807
+ // triage path for rationale. Cloud sees the input *shape* only,
808
+ // never the values. Enforces the workflow spec Telemetry rule
809
+ // even when the workflow's declared inputs include secrets.
810
+ const wfCloudHandle = await beginCloudLifecycle(config, workflow, {
811
+ declaredInputs: workflow.inputs,
812
+ resolvedInputs: workflowInput,
813
+ });
756
814
  if (wfCloudHandle?.dashboardUrl && !isJson) {
757
815
  console.log(c.subtle(` cloud: ${wfCloudHandle.dashboardUrl}`));
758
816
  }
package/dist/index.d.ts CHANGED
@@ -21,7 +21,8 @@
21
21
  * })
22
22
  * ```
23
23
  */
24
- export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, } from "./types.js";
24
+ export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, } from "./types.js";
25
+ export { WORKFLOW_INPUT_TYPES } from "./types.js";
25
26
  export { consoleLogger } from "./types.js";
26
27
  export { execute } from "./executor.js";
27
28
  export type { ExecuteOptions } from "./executor.js";
@@ -30,8 +31,9 @@ export type { ClaudeClientOptions } from "./claude.js";
30
31
  export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
31
32
  export type { SkillValidationResult } from "./skills/index.js";
32
33
  export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
33
- export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "./schema.js";
34
+ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, workflowInputsZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "./schema.js";
34
35
  export type { WorkflowError } from "./schema.js";
36
+ export { validateRuntimeInput, summarizeInputShape } from "./inputs.js";
35
37
  export { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "./mcp.js";
36
38
  export type { ProviderContextOptions, SkillMcpOptions } from "./mcp.js";
37
39
  export type { McpServerConfig, McpAutoConfig } from "./types.js";
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@
21
21
  * })
22
22
  * ```
23
23
  */
24
+ export { WORKFLOW_INPUT_TYPES } from "./types.js";
24
25
  export { consoleLogger } from "./types.js";
25
26
  // Executor
26
27
  export { execute } from "./executor.js";
@@ -31,7 +32,9 @@ export { github, linear, slack, sentry, datadog, notification, supabase, builtin
31
32
  // Node-only skill discovery (filesystem-based)
32
33
  export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
33
34
  // Schema & validation
34
- export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "./schema.js";
35
+ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, sourceZ, workflowInputsZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "./schema.js";
36
+ // Workflow input validation
37
+ export { validateRuntimeInput, summarizeInputShape } from "./inputs.js";
35
38
  // MCP auto-injection
36
39
  export { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "./mcp.js";
37
40
  // Workflow builder
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Workflow inputs — declared per-run parameter contract.
3
+ *
4
+ * A workflow YAML may declare a top-level `inputs` block describing the
5
+ * per-run parameters it accepts. The CLI validates `--input <json>` against
6
+ * this declaration, applies defaults, and rejects malformed runs at the
7
+ * boundary so node prompts and conditional routing receive a well-shaped
8
+ * `input` value.
9
+ *
10
+ * Design notes (the contract that matters for downstream consumers):
11
+ *
12
+ * 1. The `inputs` block is OPTIONAL. Workflows without one accept any
13
+ * JSON object (back-compat with every workflow shipped before this
14
+ * change). Author opts in by adding the block.
15
+ * 2. Types are the JSON-native primitives we can validate without a
16
+ * schema engine: `string`, `number`, `boolean`, plus `string[]` for
17
+ * multi-value cases (labels, files, etc.). This is intentionally
18
+ * narrower than full JSON Schema. Authors who need richer shapes can
19
+ * still pass an arbitrary object; declaring it just opts into the
20
+ * validator. We can widen later without breaking existing YAML.
21
+ * 3. `required: true` makes a field mandatory. Missing fields surface a
22
+ * single grouped error listing every gap, not one error per call.
23
+ * 4. `default` fills in the value when the caller omits it. The default
24
+ * is type-checked against the field's `type` at parse time, so
25
+ * authors can't ship a YAML that injects a string into a number
26
+ * field at run time. `required: true` and `default` are mutually
27
+ * exclusive: declaring both is incoherent (the default either
28
+ * satisfies the required check, making it vestigial, or it doesn't,
29
+ * making the default dead code), so the schema rejects the
30
+ * combination at parse time.
31
+ * 5. Telemetry redaction: the executor only ever sees the resolved
32
+ * `input` map. Cloud observers see the *shape* (key names + types)
33
+ * via `summarizeInputShape`, never the values, so a workflow that
34
+ * takes API tokens as input never leaks them.
35
+ *
36
+ * Out of scope on purpose:
37
+ * - No templating language (`{{input.foo}}` etc.). Workflow instructions
38
+ * are natural-language and the LLM already sees `input` in its context
39
+ * map; that path is good enough for v1 and avoids inventing a dialect.
40
+ * - No nested object schemas. If a single workflow needs deep input,
41
+ * pass a JSON blob through and document it; we can layer schemas in
42
+ * later. Most production use cases are flat (since_tag, until_tag,
43
+ * dry_run, repo, time_range, etc.).
44
+ */
45
+ import { z } from "zod";
46
+ /**
47
+ * Supported input field types.
48
+ *
49
+ * Kept narrow on purpose. See module docstring for rationale.
50
+ */
51
+ export declare const WORKFLOW_INPUT_TYPES: readonly ["string", "number", "boolean", "string[]"];
52
+ export type WorkflowInputType = (typeof WORKFLOW_INPUT_TYPES)[number];
53
+ /** A single declared input field. */
54
+ export interface WorkflowInputField {
55
+ /** JSON-native type. See {@link WORKFLOW_INPUT_TYPES}. */
56
+ type: WorkflowInputType;
57
+ /** Human-readable description. Surfaced by CLI help and cloud renderers. */
58
+ description?: string;
59
+ /** When true, the caller MUST provide a value. */
60
+ required?: boolean;
61
+ /**
62
+ * Default applied when the caller omits the field. Type-checked against
63
+ * `type` at YAML parse time (a string default on a number field is a
64
+ * schema error, not a runtime surprise).
65
+ */
66
+ default?: unknown;
67
+ /** Optional set of allowed values. Validated after type-check. */
68
+ enum?: unknown[];
69
+ }
70
+ /** Declared input contract for a workflow. Keys are field names. */
71
+ export type WorkflowInputs = Record<string, WorkflowInputField>;
72
+ export declare const workflowInputsZ: z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
73
+ type: z.ZodEnum<["string", "number", "boolean", "string[]"]>;
74
+ description: z.ZodOptional<z.ZodString>;
75
+ required: z.ZodOptional<z.ZodBoolean>;
76
+ default: z.ZodOptional<z.ZodUnknown>;
77
+ enum: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
78
+ }, "strict", z.ZodTypeAny, {
79
+ type: "string" | "number" | "boolean" | "string[]";
80
+ description?: string | undefined;
81
+ required?: boolean | undefined;
82
+ default?: unknown;
83
+ enum?: unknown[] | undefined;
84
+ }, {
85
+ type: "string" | "number" | "boolean" | "string[]";
86
+ description?: string | undefined;
87
+ required?: boolean | undefined;
88
+ default?: unknown;
89
+ enum?: unknown[] | undefined;
90
+ }>, {
91
+ type: "string" | "number" | "boolean" | "string[]";
92
+ description?: string | undefined;
93
+ required?: boolean | undefined;
94
+ default?: unknown;
95
+ enum?: unknown[] | undefined;
96
+ }, {
97
+ type: "string" | "number" | "boolean" | "string[]";
98
+ description?: string | undefined;
99
+ required?: boolean | undefined;
100
+ default?: unknown;
101
+ enum?: unknown[] | undefined;
102
+ }>>;
103
+ /** A single validation error against the declared input contract. */
104
+ export interface InputValidationError {
105
+ field: string;
106
+ message: string;
107
+ }
108
+ /** Tagged result of validating raw runtime input against a declared contract. */
109
+ export type InputValidationResult = {
110
+ ok: true;
111
+ value: Record<string, unknown>;
112
+ } | {
113
+ ok: false;
114
+ errors: InputValidationError[];
115
+ };
116
+ /**
117
+ * Validate the caller's raw input object against the workflow's declared
118
+ * `inputs` block. Applies defaults for omitted optional fields. Surfaces
119
+ * every problem in a single response so the CLI prints a useful message
120
+ * instead of failing on the first error.
121
+ *
122
+ * When no `inputs` block is declared, returns the caller's object verbatim
123
+ * (back-compat: every existing workflow passes through unchanged).
124
+ */
125
+ export declare function validateRuntimeInput(declared: WorkflowInputs | undefined, raw: unknown): InputValidationResult;
126
+ /**
127
+ * Summary of an input bag suitable for telemetry: keys + observed types,
128
+ * never values. Cloud renderers use this to display the input contract a
129
+ * run was invoked with, without leaking the values themselves.
130
+ *
131
+ * `declared` shapes the output when present (so the summary stays stable
132
+ * across runs even when the caller omits optional fields). Falls back to
133
+ * the resolved values' runtime shape when no declaration exists.
134
+ */
135
+ export declare function summarizeInputShape(declared: WorkflowInputs | undefined, resolved: Record<string, unknown> | undefined): Record<string, WorkflowInputType | "object" | "null">;
package/dist/inputs.js ADDED
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Workflow inputs — declared per-run parameter contract.
3
+ *
4
+ * A workflow YAML may declare a top-level `inputs` block describing the
5
+ * per-run parameters it accepts. The CLI validates `--input <json>` against
6
+ * this declaration, applies defaults, and rejects malformed runs at the
7
+ * boundary so node prompts and conditional routing receive a well-shaped
8
+ * `input` value.
9
+ *
10
+ * Design notes (the contract that matters for downstream consumers):
11
+ *
12
+ * 1. The `inputs` block is OPTIONAL. Workflows without one accept any
13
+ * JSON object (back-compat with every workflow shipped before this
14
+ * change). Author opts in by adding the block.
15
+ * 2. Types are the JSON-native primitives we can validate without a
16
+ * schema engine: `string`, `number`, `boolean`, plus `string[]` for
17
+ * multi-value cases (labels, files, etc.). This is intentionally
18
+ * narrower than full JSON Schema. Authors who need richer shapes can
19
+ * still pass an arbitrary object; declaring it just opts into the
20
+ * validator. We can widen later without breaking existing YAML.
21
+ * 3. `required: true` makes a field mandatory. Missing fields surface a
22
+ * single grouped error listing every gap, not one error per call.
23
+ * 4. `default` fills in the value when the caller omits it. The default
24
+ * is type-checked against the field's `type` at parse time, so
25
+ * authors can't ship a YAML that injects a string into a number
26
+ * field at run time. `required: true` and `default` are mutually
27
+ * exclusive: declaring both is incoherent (the default either
28
+ * satisfies the required check, making it vestigial, or it doesn't,
29
+ * making the default dead code), so the schema rejects the
30
+ * combination at parse time.
31
+ * 5. Telemetry redaction: the executor only ever sees the resolved
32
+ * `input` map. Cloud observers see the *shape* (key names + types)
33
+ * via `summarizeInputShape`, never the values, so a workflow that
34
+ * takes API tokens as input never leaks them.
35
+ *
36
+ * Out of scope on purpose:
37
+ * - No templating language (`{{input.foo}}` etc.). Workflow instructions
38
+ * are natural-language and the LLM already sees `input` in its context
39
+ * map; that path is good enough for v1 and avoids inventing a dialect.
40
+ * - No nested object schemas. If a single workflow needs deep input,
41
+ * pass a JSON blob through and document it; we can layer schemas in
42
+ * later. Most production use cases are flat (since_tag, until_tag,
43
+ * dry_run, repo, time_range, etc.).
44
+ */
45
+ import { z } from "zod";
46
+ /**
47
+ * Supported input field types.
48
+ *
49
+ * Kept narrow on purpose. See module docstring for rationale.
50
+ */
51
+ export const WORKFLOW_INPUT_TYPES = ["string", "number", "boolean", "string[]"];
52
+ // ─── Zod schema ─────────────────────────────────────────────────
53
+ const workflowInputTypeZ = z.enum(WORKFLOW_INPUT_TYPES);
54
+ /**
55
+ * Per-field validator. Cross-field checks (default matches type, enum
56
+ * values match type) run in superRefine so all problems on a single field
57
+ * surface together rather than one-at-a-time.
58
+ */
59
+ const workflowInputFieldZ = z
60
+ .object({
61
+ type: workflowInputTypeZ,
62
+ description: z.string().optional(),
63
+ required: z.boolean().optional(),
64
+ default: z.unknown().optional(),
65
+ enum: z.array(z.unknown()).min(1).optional(),
66
+ })
67
+ .strict()
68
+ .superRefine((field, ctx) => {
69
+ // `required: true` together with a `default` is incoherent: if the
70
+ // default substitutes for an omitted value, the field is effectively
71
+ // optional; if the required check fires, the default is dead code.
72
+ // Reject the combination at parse time so authors fix the YAML
73
+ // instead of relying on undocumented runtime tie-breaking.
74
+ if (field.required === true && field.default !== undefined) {
75
+ ctx.addIssue({
76
+ code: z.ZodIssueCode.custom,
77
+ message: "field cannot declare both `required: true` and `default`; pick one (a default makes the field optional)",
78
+ path: ["required"],
79
+ });
80
+ }
81
+ if (field.default !== undefined) {
82
+ const err = checkValueType(field.default, field.type);
83
+ if (err) {
84
+ ctx.addIssue({
85
+ code: z.ZodIssueCode.custom,
86
+ message: `default ${err}`,
87
+ path: ["default"],
88
+ });
89
+ }
90
+ }
91
+ if (field.enum) {
92
+ for (let i = 0; i < field.enum.length; i++) {
93
+ const err = checkValueType(field.enum[i], field.type);
94
+ if (err) {
95
+ ctx.addIssue({
96
+ code: z.ZodIssueCode.custom,
97
+ message: `enum[${i}] ${err}`,
98
+ path: ["enum", i],
99
+ });
100
+ }
101
+ }
102
+ }
103
+ });
104
+ export const workflowInputsZ = z.record(workflowInputFieldZ);
105
+ /**
106
+ * Validate the caller's raw input object against the workflow's declared
107
+ * `inputs` block. Applies defaults for omitted optional fields. Surfaces
108
+ * every problem in a single response so the CLI prints a useful message
109
+ * instead of failing on the first error.
110
+ *
111
+ * When no `inputs` block is declared, returns the caller's object verbatim
112
+ * (back-compat: every existing workflow passes through unchanged).
113
+ */
114
+ export function validateRuntimeInput(declared, raw) {
115
+ // Back-compat: no declaration → accept anything.
116
+ if (!declared || Object.keys(declared).length === 0) {
117
+ if (raw == null)
118
+ return { ok: true, value: {} };
119
+ if (typeof raw !== "object" || Array.isArray(raw)) {
120
+ return { ok: false, errors: [{ field: "(root)", message: "input must be a JSON object" }] };
121
+ }
122
+ return { ok: true, value: raw };
123
+ }
124
+ if (raw != null && (typeof raw !== "object" || Array.isArray(raw))) {
125
+ return { ok: false, errors: [{ field: "(root)", message: "input must be a JSON object" }] };
126
+ }
127
+ const provided = (raw ?? {});
128
+ const errors = [];
129
+ const out = {};
130
+ for (const [name, field] of Object.entries(declared)) {
131
+ const has = Object.prototype.hasOwnProperty.call(provided, name);
132
+ let value = has ? provided[name] : undefined;
133
+ if (!has || value === undefined || value === null) {
134
+ if (field.default !== undefined) {
135
+ out[name] = field.default;
136
+ continue;
137
+ }
138
+ if (field.required) {
139
+ errors.push({ field: name, message: "required but not provided" });
140
+ }
141
+ // Optional and no default: leave unset. Downstream `priorNode.input.X`
142
+ // reads will see `undefined`, which JSON-serializes as absent.
143
+ continue;
144
+ }
145
+ const typeErr = checkValueType(value, field.type);
146
+ if (typeErr) {
147
+ errors.push({ field: name, message: typeErr });
148
+ continue;
149
+ }
150
+ if (field.enum && !field.enum.some((v) => deepEqual(v, value))) {
151
+ errors.push({
152
+ field: name,
153
+ message: `must be one of: ${field.enum.map((v) => JSON.stringify(v)).join(", ")}`,
154
+ });
155
+ continue;
156
+ }
157
+ out[name] = value;
158
+ }
159
+ // Pass through caller-provided keys that aren't in the declaration.
160
+ // This is intentional: existing CLI flags (timeRange, dryRun, etc.)
161
+ // and the cascade `rules`/`context` mechanism inject extra keys into
162
+ // the input bag. The declaration narrows the contract for documented
163
+ // fields without locking down the whole object.
164
+ for (const [k, v] of Object.entries(provided)) {
165
+ if (!Object.prototype.hasOwnProperty.call(declared, k)) {
166
+ out[k] = v;
167
+ }
168
+ }
169
+ if (errors.length > 0)
170
+ return { ok: false, errors };
171
+ return { ok: true, value: out };
172
+ }
173
+ /**
174
+ * Summary of an input bag suitable for telemetry: keys + observed types,
175
+ * never values. Cloud renderers use this to display the input contract a
176
+ * run was invoked with, without leaking the values themselves.
177
+ *
178
+ * `declared` shapes the output when present (so the summary stays stable
179
+ * across runs even when the caller omits optional fields). Falls back to
180
+ * the resolved values' runtime shape when no declaration exists.
181
+ */
182
+ export function summarizeInputShape(declared, resolved) {
183
+ const shape = {};
184
+ if (declared) {
185
+ for (const [name, field] of Object.entries(declared)) {
186
+ shape[name] = field.type;
187
+ }
188
+ return shape;
189
+ }
190
+ if (!resolved)
191
+ return shape;
192
+ for (const [name, value] of Object.entries(resolved)) {
193
+ shape[name] = observedType(value);
194
+ }
195
+ return shape;
196
+ }
197
+ // ─── Internals ──────────────────────────────────────────────────
198
+ function checkValueType(value, type) {
199
+ switch (type) {
200
+ case "string":
201
+ return typeof value === "string" ? null : `must be a string (got ${observedType(value)})`;
202
+ case "number":
203
+ return typeof value === "number" && Number.isFinite(value)
204
+ ? null
205
+ : `must be a finite number (got ${observedType(value)})`;
206
+ case "boolean":
207
+ return typeof value === "boolean" ? null : `must be a boolean (got ${observedType(value)})`;
208
+ case "string[]":
209
+ if (!Array.isArray(value))
210
+ return `must be an array of strings (got ${observedType(value)})`;
211
+ for (let i = 0; i < value.length; i++) {
212
+ if (typeof value[i] !== "string") {
213
+ return `must be an array of strings (element ${i} is ${observedType(value[i])})`;
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+ }
219
+ function observedType(value) {
220
+ if (value === null)
221
+ return "null";
222
+ if (Array.isArray(value)) {
223
+ return value.every((v) => typeof v === "string") ? "string[]" : "object";
224
+ }
225
+ switch (typeof value) {
226
+ case "string":
227
+ return "string";
228
+ case "number":
229
+ return "number";
230
+ case "boolean":
231
+ return "boolean";
232
+ default:
233
+ return "object";
234
+ }
235
+ }
236
+ function deepEqual(a, b) {
237
+ if (a === b)
238
+ return true;
239
+ if (typeof a !== typeof b)
240
+ return false;
241
+ if (a == null || b == null)
242
+ return false;
243
+ if (Array.isArray(a) && Array.isArray(b)) {
244
+ if (a.length !== b.length)
245
+ return false;
246
+ return a.every((v, i) => deepEqual(v, b[i]));
247
+ }
248
+ if (typeof a === "object" && typeof b === "object") {
249
+ const ak = Object.keys(a);
250
+ const bk = Object.keys(b);
251
+ if (ak.length !== bk.length)
252
+ return false;
253
+ return ak.every((k) => deepEqual(a[k], b[k]));
254
+ }
255
+ return false;
256
+ }
package/dist/schema.d.ts CHANGED
@@ -9,7 +9,9 @@
9
9
  import { z } from "zod";
10
10
  import type { Workflow } from "./types.js";
11
11
  import { sourceZ } from "./sources.js";
12
+ import { workflowInputsZ } from "./inputs.js";
12
13
  export { sourceZ };
14
+ export { workflowInputsZ };
13
15
  export declare const jsonSchemaZ: z.ZodRecord<z.ZodString, z.ZodUnknown>;
14
16
  export declare const configFieldZ: z.ZodObject<{
15
17
  description: z.ZodString;
@@ -34,12 +36,12 @@ export declare const toolZ: z.ZodObject<{
34
36
  description: z.ZodString;
35
37
  input_schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
36
38
  }, "strip", z.ZodTypeAny, {
37
- name: string;
38
39
  description: string;
40
+ name: string;
39
41
  input_schema: Record<string, unknown>;
40
42
  }, {
41
- name: string;
42
43
  description: string;
44
+ name: string;
43
45
  input_schema: Record<string, unknown>;
44
46
  }>;
45
47
  export declare const skillCategoryZ: z.ZodEnum<["general", "git", "tasks", "notification", "observability", "data"]>;
@@ -120,8 +122,8 @@ export declare const skillDefinitionZ: z.ZodEffects<z.ZodObject<{
120
122
  args?: string[] | undefined;
121
123
  }>>;
122
124
  }, "strict", z.ZodTypeAny, {
123
- name?: string | undefined;
124
125
  description?: string | undefined;
126
+ name?: string | undefined;
125
127
  instruction?: string | undefined;
126
128
  mcp?: {
127
129
  url?: string | undefined;
@@ -132,8 +134,8 @@ export declare const skillDefinitionZ: z.ZodEffects<z.ZodObject<{
132
134
  args?: string[] | undefined;
133
135
  } | undefined;
134
136
  }, {
135
- name?: string | undefined;
136
137
  description?: string | undefined;
138
+ name?: string | undefined;
137
139
  instruction?: string | undefined;
138
140
  mcp?: {
139
141
  url?: string | undefined;
@@ -144,8 +146,8 @@ export declare const skillDefinitionZ: z.ZodEffects<z.ZodObject<{
144
146
  args?: string[] | undefined;
145
147
  } | undefined;
146
148
  }>, {
147
- name?: string | undefined;
148
149
  description?: string | undefined;
150
+ name?: string | undefined;
149
151
  instruction?: string | undefined;
150
152
  mcp?: {
151
153
  url?: string | undefined;
@@ -156,8 +158,8 @@ export declare const skillDefinitionZ: z.ZodEffects<z.ZodObject<{
156
158
  args?: string[] | undefined;
157
159
  } | undefined;
158
160
  }, {
159
- name?: string | undefined;
160
161
  description?: string | undefined;
162
+ name?: string | undefined;
161
163
  instruction?: string | undefined;
162
164
  mcp?: {
163
165
  url?: string | undefined;
@@ -191,12 +193,12 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
191
193
  description: z.ZodString;
192
194
  input_schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
193
195
  }, "strip", z.ZodTypeAny, {
194
- name: string;
195
196
  description: string;
197
+ name: string;
196
198
  input_schema: Record<string, unknown>;
197
199
  }, {
198
- name: string;
199
200
  description: string;
201
+ name: string;
200
202
  input_schema: Record<string, unknown>;
201
203
  }>, "many">>;
202
204
  instruction: z.ZodOptional<z.ZodString>;
@@ -238,9 +240,9 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
238
240
  }>>;
239
241
  mcpAliases: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>>;
240
242
  }, "strip", z.ZodTypeAny, {
241
- id: string;
242
- name: string;
243
243
  description: string;
244
+ name: string;
245
+ id: string;
244
246
  category: "general" | "git" | "tasks" | "notification" | "observability" | "data";
245
247
  config: Record<string, {
246
248
  description: string;
@@ -248,8 +250,8 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
248
250
  env?: string | undefined;
249
251
  }>;
250
252
  tools: {
251
- name: string;
252
253
  description: string;
254
+ name: string;
253
255
  input_schema: Record<string, unknown>;
254
256
  }[];
255
257
  instruction?: string | undefined;
@@ -263,9 +265,9 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
263
265
  } | undefined;
264
266
  mcpAliases?: Record<string, string[]> | undefined;
265
267
  }, {
266
- id: string;
267
- name: string;
268
268
  description: string;
269
+ name: string;
270
+ id: string;
269
271
  category: "general" | "git" | "tasks" | "notification" | "observability" | "data";
270
272
  instruction?: string | undefined;
271
273
  mcp?: {
@@ -282,15 +284,15 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
282
284
  env?: string | undefined;
283
285
  }> | undefined;
284
286
  tools?: {
285
- name: string;
286
287
  description: string;
288
+ name: string;
287
289
  input_schema: Record<string, unknown>;
288
290
  }[] | undefined;
289
291
  mcpAliases?: Record<string, string[]> | undefined;
290
292
  }>, {
291
- id: string;
292
- name: string;
293
293
  description: string;
294
+ name: string;
295
+ id: string;
294
296
  category: "general" | "git" | "tasks" | "notification" | "observability" | "data";
295
297
  config: Record<string, {
296
298
  description: string;
@@ -298,8 +300,8 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
298
300
  env?: string | undefined;
299
301
  }>;
300
302
  tools: {
301
- name: string;
302
303
  description: string;
304
+ name: string;
303
305
  input_schema: Record<string, unknown>;
304
306
  }[];
305
307
  instruction?: string | undefined;
@@ -313,9 +315,9 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
313
315
  } | undefined;
314
316
  mcpAliases?: Record<string, string[]> | undefined;
315
317
  }, {
316
- id: string;
317
- name: string;
318
318
  description: string;
319
+ name: string;
320
+ id: string;
319
321
  category: "general" | "git" | "tasks" | "notification" | "observability" | "data";
320
322
  instruction?: string | undefined;
321
323
  mcp?: {
@@ -332,8 +334,8 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
332
334
  env?: string | undefined;
333
335
  }> | undefined;
334
336
  tools?: {
335
- name: string;
336
337
  description: string;
338
+ name: string;
337
339
  input_schema: Record<string, unknown>;
338
340
  }[] | undefined;
339
341
  mcpAliases?: Record<string, string[]> | undefined;
@@ -2041,8 +2043,8 @@ export declare const workflowZ: z.ZodObject<{
2041
2043
  args?: string[] | undefined;
2042
2044
  }>>;
2043
2045
  }, "strict", z.ZodTypeAny, {
2044
- name?: string | undefined;
2045
2046
  description?: string | undefined;
2047
+ name?: string | undefined;
2046
2048
  instruction?: string | undefined;
2047
2049
  mcp?: {
2048
2050
  url?: string | undefined;
@@ -2053,8 +2055,8 @@ export declare const workflowZ: z.ZodObject<{
2053
2055
  args?: string[] | undefined;
2054
2056
  } | undefined;
2055
2057
  }, {
2056
- name?: string | undefined;
2057
2058
  description?: string | undefined;
2059
+ name?: string | undefined;
2058
2060
  instruction?: string | undefined;
2059
2061
  mcp?: {
2060
2062
  url?: string | undefined;
@@ -2065,8 +2067,8 @@ export declare const workflowZ: z.ZodObject<{
2065
2067
  args?: string[] | undefined;
2066
2068
  } | undefined;
2067
2069
  }>, {
2068
- name?: string | undefined;
2069
2070
  description?: string | undefined;
2071
+ name?: string | undefined;
2070
2072
  instruction?: string | undefined;
2071
2073
  mcp?: {
2072
2074
  url?: string | undefined;
@@ -2077,8 +2079,8 @@ export declare const workflowZ: z.ZodObject<{
2077
2079
  args?: string[] | undefined;
2078
2080
  } | undefined;
2079
2081
  }, {
2080
- name?: string | undefined;
2081
2082
  description?: string | undefined;
2083
+ name?: string | undefined;
2082
2084
  instruction?: string | undefined;
2083
2085
  mcp?: {
2084
2086
  url?: string | undefined;
@@ -2135,13 +2137,44 @@ export declare const workflowZ: z.ZodObject<{
2135
2137
  }>]>, "many">>;
2136
2138
  judge_model: z.ZodOptional<z.ZodString>;
2137
2139
  judge_budget: z.ZodOptional<z.ZodNumber>;
2140
+ inputs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
2141
+ type: z.ZodEnum<["string", "number", "boolean", "string[]"]>;
2142
+ description: z.ZodOptional<z.ZodString>;
2143
+ required: z.ZodOptional<z.ZodBoolean>;
2144
+ default: z.ZodOptional<z.ZodUnknown>;
2145
+ enum: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
2146
+ }, "strict", z.ZodTypeAny, {
2147
+ type: "string" | "number" | "boolean" | "string[]";
2148
+ description?: string | undefined;
2149
+ required?: boolean | undefined;
2150
+ default?: unknown;
2151
+ enum?: unknown[] | undefined;
2152
+ }, {
2153
+ type: "string" | "number" | "boolean" | "string[]";
2154
+ description?: string | undefined;
2155
+ required?: boolean | undefined;
2156
+ default?: unknown;
2157
+ enum?: unknown[] | undefined;
2158
+ }>, {
2159
+ type: "string" | "number" | "boolean" | "string[]";
2160
+ description?: string | undefined;
2161
+ required?: boolean | undefined;
2162
+ default?: unknown;
2163
+ enum?: unknown[] | undefined;
2164
+ }, {
2165
+ type: "string" | "number" | "boolean" | "string[]";
2166
+ description?: string | undefined;
2167
+ required?: boolean | undefined;
2168
+ default?: unknown;
2169
+ enum?: unknown[] | undefined;
2170
+ }>>>;
2138
2171
  }, "strip", z.ZodTypeAny, {
2139
- id: string;
2140
- name: string;
2141
2172
  description: string;
2173
+ name: string;
2174
+ id: string;
2142
2175
  skills: Record<string, {
2143
- name?: string | undefined;
2144
2176
  description?: string | undefined;
2177
+ name?: string | undefined;
2145
2178
  instruction?: string | undefined;
2146
2179
  mcp?: {
2147
2180
  url?: string | undefined;
@@ -2267,9 +2300,16 @@ export declare const workflowZ: z.ZodObject<{
2267
2300
  judge_model?: string | undefined;
2268
2301
  workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
2269
2302
  judge_budget?: number | undefined;
2303
+ inputs?: Record<string, {
2304
+ type: "string" | "number" | "boolean" | "string[]";
2305
+ description?: string | undefined;
2306
+ required?: boolean | undefined;
2307
+ default?: unknown;
2308
+ enum?: unknown[] | undefined;
2309
+ }> | undefined;
2270
2310
  }, {
2271
- id: string;
2272
2311
  name: string;
2312
+ id: string;
2273
2313
  nodes: Record<string, {
2274
2314
  name: string;
2275
2315
  instruction: string | {
@@ -2368,8 +2408,8 @@ export declare const workflowZ: z.ZodObject<{
2368
2408
  entry: string;
2369
2409
  description?: string | undefined;
2370
2410
  skills?: Record<string, {
2371
- name?: string | undefined;
2372
2411
  description?: string | undefined;
2412
+ name?: string | undefined;
2373
2413
  instruction?: string | undefined;
2374
2414
  mcp?: {
2375
2415
  url?: string | undefined;
@@ -2399,15 +2439,22 @@ export declare const workflowZ: z.ZodObject<{
2399
2439
  judge_model?: string | undefined;
2400
2440
  workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
2401
2441
  judge_budget?: number | undefined;
2442
+ inputs?: Record<string, {
2443
+ type: "string" | "number" | "boolean" | "string[]";
2444
+ description?: string | undefined;
2445
+ required?: boolean | undefined;
2446
+ default?: unknown;
2447
+ enum?: unknown[] | undefined;
2448
+ }> | undefined;
2402
2449
  }>;
2403
2450
  /** Parse + validate a raw object as a Workflow. Throws on invalid input. */
2404
2451
  export declare function parseWorkflow(raw: unknown): {
2405
- id: string;
2406
- name: string;
2407
2452
  description: string;
2453
+ name: string;
2454
+ id: string;
2408
2455
  skills: Record<string, {
2409
- name?: string | undefined;
2410
2456
  description?: string | undefined;
2457
+ name?: string | undefined;
2411
2458
  instruction?: string | undefined;
2412
2459
  mcp?: {
2413
2460
  url?: string | undefined;
@@ -2533,6 +2580,13 @@ export declare function parseWorkflow(raw: unknown): {
2533
2580
  judge_model?: string | undefined;
2534
2581
  workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
2535
2582
  judge_budget?: number | undefined;
2583
+ inputs?: Record<string, {
2584
+ type: "string" | "number" | "boolean" | "string[]";
2585
+ description?: string | undefined;
2586
+ required?: boolean | undefined;
2587
+ default?: unknown;
2588
+ enum?: unknown[] | undefined;
2589
+ }> | undefined;
2536
2590
  };
2537
2591
  export interface WorkflowError {
2538
2592
  code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "INVALID_INLINE_SKILL";
@@ -2802,6 +2856,39 @@ export declare const workflowJsonSchema: {
2802
2856
  readonly default: 50;
2803
2857
  readonly description: "Soft cap on expected judge calls per workflow run. Executor warns at load time if exceeded; not a hard runtime cap in v1.";
2804
2858
  };
2859
+ readonly inputs: {
2860
+ readonly type: "object";
2861
+ readonly description: "Declared per-run input contract. Each entry names a parameter the caller may supply via --input. The CLI validates against this declaration, applies defaults for omitted optional fields, and rejects unknown types before the executor runs. Optional; workflows without an inputs block accept any JSON object (back-compat).";
2862
+ readonly additionalProperties: {
2863
+ readonly type: "object";
2864
+ readonly required: readonly ["type"];
2865
+ readonly additionalProperties: false;
2866
+ readonly properties: {
2867
+ readonly type: {
2868
+ readonly type: "string";
2869
+ readonly enum: readonly ["string", "number", "boolean", "string[]"];
2870
+ readonly description: "JSON-native type. Use a flat object if you need richer shapes.";
2871
+ };
2872
+ readonly description: {
2873
+ readonly type: "string";
2874
+ readonly description: "Human-readable purpose. Shown in CLI help and cloud renderers.";
2875
+ };
2876
+ readonly required: {
2877
+ readonly type: "boolean";
2878
+ readonly default: false;
2879
+ readonly description: "When true, the caller must provide a value.";
2880
+ };
2881
+ readonly default: {
2882
+ readonly description: "Value applied when the caller omits the field. Type-checked at parse time against 'type'.";
2883
+ };
2884
+ readonly enum: {
2885
+ readonly type: "array";
2886
+ readonly minItems: 1;
2887
+ readonly description: "Optional set of allowed values. Validated after the type check.";
2888
+ };
2889
+ };
2890
+ };
2891
+ };
2805
2892
  readonly nodes: {
2806
2893
  readonly type: "object";
2807
2894
  readonly additionalProperties: {
package/dist/schema.js CHANGED
@@ -9,7 +9,9 @@
9
9
  import { z } from "zod";
10
10
  import { EVALUATOR_KINDS, EVAL_POLICIES, MCP_TRANSPORTS, REQUIRES_ON_FAIL, SKILL_CATEGORIES, SKILL_ID_MAX_LENGTH, SKILL_ID_PATTERN, } from "./types.js";
11
11
  import { sourceZ } from "./sources.js";
12
+ import { workflowInputsZ, WORKFLOW_INPUT_TYPES } from "./inputs.js";
12
13
  export { sourceZ };
14
+ export { workflowInputsZ };
13
15
  // ─── Zod Schemas ─────────────────────────────────────────────────
14
16
  export const jsonSchemaZ = z.record(z.unknown());
15
17
  export const configFieldZ = z.object({
@@ -241,6 +243,7 @@ export const workflowZ = z.object({
241
243
  context: z.array(sourceZ).optional(),
242
244
  judge_model: z.string().min(1).optional(),
243
245
  judge_budget: z.number().int().min(0).optional(),
246
+ inputs: workflowInputsZ.optional(),
244
247
  });
245
248
  const LEGACY_VERIFY_MESSAGE = "uses the legacy 'verify:' field. It was renamed to 'eval:' in @sweny-ai/core v0.2.0 with a different shape (named evaluators with kind: value | function | judge). " +
246
249
  "Migration guide: https://spec.sweny.ai/nodes/#eval";
@@ -572,6 +575,39 @@ export const workflowJsonSchema = {
572
575
  default: 50,
573
576
  description: "Soft cap on expected judge calls per workflow run. Executor warns at load time if exceeded; not a hard runtime cap in v1.",
574
577
  },
578
+ inputs: {
579
+ type: "object",
580
+ description: "Declared per-run input contract. Each entry names a parameter the caller may supply via --input. The CLI validates against this declaration, applies defaults for omitted optional fields, and rejects unknown types before the executor runs. Optional; workflows without an inputs block accept any JSON object (back-compat).",
581
+ additionalProperties: {
582
+ type: "object",
583
+ required: ["type"],
584
+ additionalProperties: false,
585
+ properties: {
586
+ type: {
587
+ type: "string",
588
+ enum: [...WORKFLOW_INPUT_TYPES],
589
+ description: "JSON-native type. Use a flat object if you need richer shapes.",
590
+ },
591
+ description: {
592
+ type: "string",
593
+ description: "Human-readable purpose. Shown in CLI help and cloud renderers.",
594
+ },
595
+ required: {
596
+ type: "boolean",
597
+ default: false,
598
+ description: "When true, the caller must provide a value.",
599
+ },
600
+ default: {
601
+ description: "Value applied when the caller omits the field. Type-checked at parse time against 'type'.",
602
+ },
603
+ enum: {
604
+ type: "array",
605
+ minItems: 1,
606
+ description: "Optional set of allowed values. Validated after the type check.",
607
+ },
608
+ },
609
+ },
610
+ },
575
611
  nodes: {
576
612
  type: "object",
577
613
  additionalProperties: {
package/dist/types.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export type { Source, ResolvedSource, SourceKind, SourceResolutionMap } from "./sources.js";
2
+ export type { WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, } from "./inputs.js";
3
+ export { WORKFLOW_INPUT_TYPES } from "./inputs.js";
2
4
  import type { Source as _Source, ResolvedSource as _ResolvedSource } from "./sources.js";
3
5
  export type JSONSchema = Record<string, unknown>;
4
6
  /** Context passed to tool handlers at execution time */
@@ -311,6 +313,16 @@ export interface Workflow {
311
313
  judge_model?: string;
312
314
  /** Soft cap on expected judge calls per workflow run. Warning at load time when exceeded. */
313
315
  judge_budget?: number;
316
+ /**
317
+ * Declared per-run input contract. When present, the CLI validates the
318
+ * caller-provided `--input` JSON against this declaration, applies defaults
319
+ * for omitted optional fields, and rejects malformed input before the
320
+ * executor runs. Workflows without an `inputs` block accept any JSON
321
+ * object (back-compat).
322
+ *
323
+ * See `src/inputs.ts` for the field shape and validation rules.
324
+ */
325
+ inputs?: import("./inputs.js").WorkflowInputs;
314
326
  }
315
327
  /**
316
328
  * Inline skill definition in a workflow's `skills` block.
package/dist/types.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { WORKFLOW_INPUT_TYPES } from "./inputs.js";
1
2
  /**
2
3
  * Skill categories: used for per-node validation and grouping.
3
4
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.97",
3
+ "version": "0.1.99",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"