Skip to content

feat(workflows)!: TypeBox-native input/output schemas (replace Zod) - #1181

Merged
lavaman131 merged 3 commits into
mainfrom
feat/workflows-typebox-schemas
Jun 2, 2026
Merged

feat(workflows)!: TypeBox-native input/output schemas (replace Zod)#1181
lavaman131 merged 3 commits into
mainfrom
feat/workflows-typebox-schemas

Conversation

@lavaman131

@lavaman131 lavaman131 commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the legacy { type, required, default, choices } input/output descriptor with TypeBox-native schemas, removes the zod dependency, provides precise Static<> typing for ctx.inputs, .run() return values, and ctx.workflow(child).outputs with full runtime Value validation, and enables noUnusedLocals/noUnusedParameters repo-wide.

Key Changes

Authoring surface

Authors now declare inputs and outputs with TypeBox schemas:

import { defineWorkflow, Type } from "@bastani/workflows";

defineWorkflow("x")
  .input("prompt", Type.String({ description: "Topic to research" }))
  .input("count",  Type.Number({ default: 2 }))
  .input("flavor", Type.Union([Type.Literal("a"), Type.Literal("b")], { default: "a" }))
  .output("packet", Type.Object({ topic: Type.String(), score: Type.Number() }))
  .run(async (ctx) => ({ packet: { topic: ctx.inputs.prompt, score: ctx.inputs.count } }))
  .compile();

Type.Optional(...) marks an optional field. Type, Static, and TSchema are re-exported from @bastani/workflows for single-import authoring.

Static typing

  • ctx.inputs typed as Static<TInputSchemaMap>
  • .run() return typed against the declared .output(...) contract
  • ctx.workflow(child).outputs typed from the child definition's output contract
  • Missing-required and wrong-type inputs caught at compile time; excess keys rejected at runtime

Runtime validation

  • Inputs (with defaults applied) and outputs validated via TypeBox Value with precise path errors
  • serializable.ts reimplemented on TypeBox using Type.Cyclic for the recursive JSON-serializable schema, replacing Zod
  • zod removed from @bastani/workflows

Picker / UI compatibility preserved

New schema-introspection.ts derives the legacy normalized { type, choices, default, required } descriptor from a TypeBox schema — TUnion<TLiteral[]>select, TBooleanboolean, default/Type.Optional honored. The inputs picker UI, validation, and dispatch are unchanged.

Repo-wide TypeScript strictness

  • Enabled noUnusedLocals and noUnusedParameters in the root tsconfig.json
  • Added .atomic/workflows/**/* to the TypeScript project so contract fixtures are type-checked
  • Cleaned up all resulting unused locals across builtins, contract fixtures, and the full test suite

Migrated surfaces

  • All builtin workflows (goal, deep-research-codebase, ralph, open-claude-design) migrated to TypeBox API
  • All contract fixtures (.atomic/workflows/) and the full test suite migrated
  • Docs (packages/coding-agent/docs/workflows.md, packages/workflows/README.md) and CHANGELOG updated

Breaking Changes

The { type, required, default, choices } input/output descriptor is removed. Workflows must declare inputs/outputs with TypeBox schemas.

Before After
.input("prompt", { type: "text", required: true }) .input("prompt", Type.String())
.input("count", { type: "number", default: 2 }) .input("count", Type.Number({ default: 2 }))
.input("flavor", { type: "select", choices: ["a","b"], default: "a" }) .input("flavor", Type.Union([Type.Literal("a"), Type.Literal("b")], { default: "a" }))

This feature was unreleased, so there is no migration path needed for shipped consumers.

Quality

  • bun run typecheck: 0 errors
  • bun run lint: 0 warnings
  • CLAUDECODE=1 bun run test:unit: 1945 pass / 0 fail
  • 66 files changed, net −12 lines — the descriptor subsystem collapses into TypeBox + one introspection adapter

Workflow inputs and outputs are now declared with TypeBox schemas instead
of the legacy { type, required, default, choices } descriptor, and Zod is
removed entirely.

- Authoring: .input("prompt", Type.String()), .input("count", Type.Number({ default: 2 })),
  .input("flavor", Type.Union([Type.Literal("a"), Type.Literal("b")])),
  .output("packet", Type.Object({ topic: Type.String(), score: Type.Number() })).
  Type.Optional(...) marks an optional field. Type is re-exported from
  @bastani/workflows (with the Static and TSchema types) for single-import.
- Typing: ctx.inputs, the .run() return, and ctx.workflow(child).outputs are
  precisely typed via Static<> (missing-required, wrong-type, and nested-shape
  mismatches are caught statically; excess keys are enforced at runtime).
- Runtime: inputs and outputs validate via TypeBox Value (defaults applied,
  precise path errors); serializable.ts reimplemented on TypeBox, dropping zod.
- A schema-introspection adapter derives the normalized field descriptor
  (type/choices/default/required) from a TypeBox schema, so the inputs picker
  UI, validation, and dispatch keep working without being rewritten
  (TUnion<TLiteral> -> select, TBoolean -> toggle, TNumber -> number, etc.).
- Migrated all builtin and contract workflows and the test suite to the
  TypeBox API; updated docs (coding-agent + workflows) and the CHANGELOG.

BREAKING CHANGE: the { type, required, default, choices } input/output
descriptor is removed; workflows must declare inputs/outputs with TypeBox
schemas. This feature was unreleased, so there is no migration path for
shipped consumers.

Assistant-model: Claude Opus 4.8
@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown

Code Review — TypeBox-native workflow schemas

Reviewed against CLAUDE.md style/conventions and the workflows package design. Overall: this is a clean, well-staged migration — the schema-introspection.ts adapter is the right move and saves a rewrite of every UI/dispatch surface, the builder split (WorkflowBuilder vs CompletedWorkflowBuilder) properly sequences .run() before .compile(), and removing Zod is a real dep-trim win. The Static<> threading gives authors precise types on ctx.inputs, .run() return, and ctx.workflow(child).outputs without leaking schema-engine types into authoring callsites. 1945 unit tests is also great coverage for a breaking change of this scope.

Findings below, roughly in order of importance.

Correctness / bugs

1. Output default silently allows omission — but defaults aren't applied to outputs.
assertWorkflowOutputsExplicit in packages/workflows/src/runs/foreground/executor.ts:1715-1723 calls schemaIsRequired(schema) to decide whether a missing declared output is an error. schemaIsRequired (schema-introspection.ts:96-98) returns false when the schema has a default. But Value.Default is only applied to inputs (resolveInputs at executor.ts:215), never outputs. Net effect: .output(\"score\", Type.Number({ default: 0 })) lets the workflow return {} without complaint, and child.outputs.score arrives at the parent as undefined despite the static type saying number. Two reasonable fixes:

  • Apply Value.Default to outputs at the executor boundary so the type contract matches runtime behavior, or
  • Use IsOptional(schema) (not schemaIsRequired) for the output-existence check, so a defaulted output is still required to be returned.

I'd lean toward the first — it preserves the "defaults always materialize" mental model and matches input semantics.

2. Nested schema mismatches lose path info in output validation.
assertWorkflowOutputsExplicit (executor.ts:1726-1736) catches !Value.Check(schema, value) and throws output \"packet\" expected object, got object when a declared Type.Object({ topic: Type.String() }) gets { topic: 42 }. The error doesn't say which nested field failed. validate-inputs.ts:103-107 already does this correctly via [...Value.Errors(def, value)][0] — the output path should mirror it. Right now an author returning a malformed nested output sees a tautological message and has to grep their .run() return manually.

3. isPlainObjectValue predicate accepts arrays/primitives unconditionally.
packages/workflows/src/shared/serializable.ts:9-13:

function isPlainObjectValue(value: unknown): boolean {
  if (value === null || typeof value !== \"object\" || Array.isArray(value)) return true;
  const proto = Object.getPrototypeOf(value);
  return proto === Object.prototype || proto === null;
}

It's wired as a Refine predicate on Type.Record(Type.String(), ...). The base schema would normally filter non-objects, but TypeBox's Type.Record historically matches arrays (they pass typeof === \"object\"). If that's the case here, an array sneaks through both the Record AND the refinement and is silently accepted as a "serializable object." Worth either:

  • Tightening to if (Array.isArray(value)) return false;, or
  • A unit test that explicitly checks Value.Check(workflowSerializableObjectSchema, [\"a\", \"b\"]) returns false.

The current code's intent is right; the predicate just has the wrong polarity for arrays.

4. workflow-runner.ts:264 duplicates schemaDefault().

const declaredDefault = (def as { default?: unknown }).default;

This is exactly what schemaDefault(def) does. Replace the cast with the helper — same behavior, no as, and schemaDefault is already imported in validate-inputs.ts/schema-introspection.ts callsites so the pattern is established.

Style / conventions (per CLAUDE.md)

5. Import ordering in serializable.ts.
The import { Value } from \"typebox/value\"; at line 14 sits after the isPlainObjectValue function declaration. The CLAUDE.md "no extra abstractions" guidance is satisfied, but module hygiene wants top-of-file imports. Hoist it next to the typebox import.

6. Type.Unsafe<T>(Type.Object({}, { additionalProperties: true })) pattern is brittle.
Used in .atomic/workflows/contract-*.ts and goal.ts to tell TS "it's ComplexPacket" while telling runtime "any shape." This is a deliberate escape hatch for the contract-validation workflows, but it's an unsigned check that the TS type and runtime type stay in sync as ComplexPacket evolves. A one-line // runtime accepts any object — TS type is documentary comment at the first use would save the next author from "why doesn't this catch a wrong shape?".

7. Re-export comment in index.ts:15 says // Note: \Type` / `Static` / `TSchema` are re-exported via ./sdk-surface.js.— good. Worth a similar pointer inREADME.mdso consumers don't import fromtypebox` directly and accidentally diverge versions.

Performance

8. resolveInputs rebuilds a synthetic Type.Object per dispatch.
executor.ts:215-218:

const withDefaults = Value.Default(
  Type.Object(schema as Record<string, TSchema>, { additionalProperties: true }),
  resolved,
) as Record<string, WorkflowSerializableValue>;

The synthetic object schema is rebuilt every run. For workflows dispatched in tight loops (contract/runtime tests, parent→child fanout) this is wasted work. Easy improvement: memoize the wrapped object schema on the compiled WorkflowDefinition (e.g. lazy-cache in a WeakMap<WorkflowDefinition, TSchema>).

Not a blocker — input validation isn't a hot path — but worth noting before this pattern spreads.

Security

No new concerns. The TypeBox Value checker is the same trust boundary the legacy descriptor + Zod had. JSON-serializable enforcement still catches function/Date/circular values, and the Refine for plain objects is the right shape (modulo finding #3).

Test coverage

9. Excellent breadth. Migration covered: define-workflow.test.ts, validate-inputs.test.ts, builtin-workflows.test.ts, executor.test.ts, and contract workflows. The TypeBox-derived field-kind assertions (e.g. assert.equal(fieldKind(d.inputs[\"max_partitions\"]), \"number\")) are a nice indirection that future-proofs against minor TypeBox API drift.

Gaps worth a follow-up test (none blocking):

Nits

  • define-workflow.ts:41 type SimplifyWorkflowOutputs<T> = Simplify<T>; is a one-line alias to Simplify. If it's there for readability at the run-fn signature, keep it; otherwise inline.
  • assertWorkflowOutputsExplicit accepts an empty declarations ?? {}, which means an undeclared-output workflow accepts any .run() return. The undeclared-key check at lines 1708-1713 handles that correctly, but the iteration on Object.entries(declarations) makes the empty case a no-op — minor, just worth a one-liner comment that {} declarations = strict-empty contract.

Summary

The schema is sound, the typing chain is real, and the adapter strategy is the right call to avoid rewriting half the package. The two findings I'd address before merge are #1 (output defaults silently absent) and #2 (lost path in nested-output errors) — both are user-facing correctness regressions vs. what an author would reasonably expect from a TypeBox-native API. Everything else is polish or follow-up.

Nice work — this is a meaningful tightening of the workflow contract surface.

The TypeBox migration inlined select choices into Type.Union([Type.Literal(...)]),
leaving the original const choice arrays (FLAVORS, VARIANTS, INVALID_MODES)
unused, plus a stale reviewerErrorResult(iteration) param and an unused
erasedDef binding. These tripped the project's noUnusedLocals/noUnusedParameters
standard (enforced by the IDE) even though the root tsconfig does not set the
flags. Remove them so the migrated built-in and .atomic contract workflows are
clean.

Assistant-model: Claude Opus 4.8
@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown

PR Review — TypeBox-native input/output schemas

Solid migration — Static<>-driven typing for ctx.inputs, .run() returns, and child.outputs is a genuine ergonomic win, the schema-introspection.ts adapter cleanly preserves the picker/dispatch surface, and the removal of defensive helpers like numberOrZero/stringOrFallback/objectOrEmpty from the contract-* fixtures is a great demonstration of what the typing improvement buys. Test coverage tracks the new authoring surface. A few items worth a second look below.

Code quality

  • Misplaced import in packages/workflows/src/shared/serializable.ts. import { Value } from \"typebox/value\"; appears after the isPlainObjectValue function declaration rather than at the top with the other imports. ES module imports get hoisted, so it works at runtime, but it's a clear style issue and easy to miss in review. Please move it up next to the other typebox import.

  • isPlainObjectValue semantics are slightly misleading. The early return true for null / non-objects / arrays is unreachable in practice because Type.Record already filters those out before the Refine runs. The name implies a general "is this a plain object" predicate, but it's really "passes the extra check on top of Record". Either a one-line comment explaining the Record gate, or splitting into a narrower predicate that only handles the reachable case, would make intent obvious.

  • workflow-runner.ts:withResolvedDefaults bypasses the schema-introspection adapter. It reaches into the schema with (def as { default?: unknown }).default rather than calling schemaDefault(def). The rest of the codebase consistently goes through schemaIsRequired / schemaDefault / schemaFieldKind. Using the helper here too keeps the introspection surface as the single source of truth for "what does a TypeBox schema mean to us".

  • Per-call Type.Object(...) allocation in resolveInputs. Type.Object(schema as Record<string, TSchema>, { additionalProperties: true }) is constructed on every invocation. For workflows fanning out many child runs this allocates a fresh TypeBox schema per call. Worth either caching on the compiled definition (e.g. lazily memoized on WorkflowDefinition) or hoisting at compile time, since the schema map is already frozen.

  • Cast in extension/index.ts:workflowGetResult. inputs: inputs as unknown as WorkflowSerializableValue[] papers over a type mismatch on a serializable-boundary. If WorkflowInputEntry.default can carry anything a TypeBox schema accepts, the cast can smuggle a non-serializable default into the result. Either narrow WorkflowInputEntry.default to WorkflowSerializableValue (consistent with the rest of the contract) or run the entries through workflowSerializableObjectValidationError before the cast.

Potential bugs / behavior

  • open-claude-design.ts loses literal narrowing on inputs. The input declaration uses [...OUTPUT_TYPES].map((value) => Type.Literal(value)). Depending on TS inference, the .map callback can widen value to string and the resulting union schema's Static<> could be string rather than \"page\" | \"component\" | .... Worth either (a) writing the union inline (Type.Union([Type.Literal(\"page\"), ...])) or (b) adding a typed helper that preserves the const tuple so ctx.inputs.output_type keeps its literal-union type. A quick Expect<Equal<...>> test would lock this down.

  • open-claude-design.ts output_type widened to plain string. The input is a literal union; the output is Type.Optional(Type.String()). Consumers of open-claude-design as a child workflow won't get the same narrowed type on child.outputs.output_type as they would for the input. If intentional (forward-compat for new output types), a comment would help; otherwise mirror the input union.

  • Type.Object({}, { additionalProperties: true }) accepts class instances. A Date/Map/etc. structurally satisfies the empty-shape "object" output declaration via Value.Check. It's then correctly caught by the recursive serializable check, but the error reaches users through the serializable path rather than the declared-output path — so the message can be a little oblique. Not blocking, but consider rejecting non-plain objects in the kind-check branch so the error is colocated with the declared contract.

  • Dependency name vs CLAUDE.md. CLAUDE.md lists @sinclair/typebox; this PR introduces typebox (the new 1.x repackage). They're not the same npm package — please update CLAUDE.md so the documented dep matches reality and future contributors don't add @sinclair/typebox back alongside typebox.

Performance

  • The recursive Type.Cyclic + Refine JSON-serializable schema is structurally fine; TypeBox Value.Check will be hotter than Zod's safeParse was for small payloads, but should be a wash or better for larger ones. The main hot-spot to watch is the per-call Type.Object allocation noted above and the fresh Value.Default call on every resolveInputs.

Security

  • No new attack surface introduced. Input validation is strict (no coercion), unknown keys are rejected, and select/literal-union membership is enforced. The Refine-gated plain-object check is the right call for rejecting prototype-bearing objects from JSON output.

Test coverage

  • Migration of all unit/integration tests to the TypeBox API is thorough, and define-workflow.test.ts exercises both the Static<> typing assertions and the runtime descriptor view.
  • Suggested additions:
    • A tsd-style or Expect<Equal<...>> test that pins ctx.inputs.output_type to the literal union in open-claude-design (regression guard for the .map(Type.Literal) pattern above).
    • A round-trip test for serializable.ts that asserts Date/Map/RegExp instances are rejected via the Refine (verifies the only-reachable branch in isPlainObjectValue).
    • A test asserting schemaIsRequired(Type.Optional(Type.Union([Type.Literal(\"a\"), Type.Literal(\"b\")]))) returns false and that schemaChoices still surfaces [\"a\",\"b\"] through the Optional wrapper — this is the exact shape several validate-inputs tests exercise, and a direct unit test for the introspector would protect it.

Nice work overall — the typed child.outputs flow is the right north star.

…resh docs

Post-migration polish for the TypeBox-native workflow schemas:

- Precise output types where the shape is known, replacing loose
  Type.Array(Type.Unknown())/additionalProperties schemas: deep-research
  'partitions' -> Type.Array(Type.String()); goal 'receipts' -> a precise
  Type.Object array; and the contract workflows (contract-valid echo/items,
  contract-hil-basic, contract-hil-nested-root, contract-parent,
  contract-complex-root) now declare precise Type.Object/Type.Array schemas
  or Type.Unsafe<Interface>(...) so ctx.inputs/.run() return/child.outputs
  are precisely typed and runtime validation enforces the real shape.
- Enable noUnusedLocals/noUnusedParameters in the root tsconfig (the repo
  standard per AGENTS.md) so 'bun run typecheck'/lint/CI match the IDE and
  this can't drift again, and remove the dead locals/imports it surfaced
  across workflows, tests, coding-agent, subagents, and mcp. Exhaustiveness
  checks (const _x: never = ...) are preserved via an explicit reference.
- Docs: packages/coding-agent/docs/workflows.md and packages/workflows
  README now steer authors toward precise TypeBox schemas (with a
  schema->Static table and a loose-vs-precise example) and document the
  Type.Unsafe<T> escape hatch for genuinely dynamic data.

Assistant-model: Claude Opus 4.8
@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review — TypeBox-native input/output schemas

Solid migration overall. The schema-introspection.ts adapter cleanly bridges TypeBox to the legacy descriptor consumers without rewriting the picker/render/dispatch surfaces, the builder's Static<> threading produces real ergonomic wins (contract fixtures lose entire blocks of defensive narrowing), and the runtime now validates outputs with Value.Check against the precise declared shape. Below are the issues worth a look.

Correctness — input validation silently ignores TypeBox refinements

packages/workflows/src/runs/shared/validate-inputs.ts switches on schemaFieldKind and for primitive kinds (text/number/integer/boolean/select) only runs JS typeof / Number.isFinite / choices.includes checks. It never calls Value.Check(def, value) for these branches. Outputs, by contrast, go through Value.Check(schema, value) in assertWorkflowOutputsExplicit (executor.ts:1726) and do enforce the full schema.

So today, if an author writes:
```ts
.input("slug", Type.String({ pattern: "^[a-z]+$" }))
.input("count", Type.Number({ minimum: 1, maximum: 10 }))
```
runtime input validation accepts any string and any finite number — refinements are silently dropped, but the same constraints on .output(...) would be enforced. The README/docs don't surface this asymmetry, and it's surprising given the PR's stated goal of "full runtime Value validation."

Two reasonable fixes:

  1. After the JS-typeof check in each case, also run Value.Check(def, value) and surface the first Value.Errors(...) message (keeps the friendly leading-error wording, adds refinement enforcement).
  2. Or document explicitly that TypeBox refinements on inputs are advisory only.

Option 1 also fixes a related edge case in schemaFieldKind: a bare Type.Literal(\"foo\") returns kind \"text\" (schema-introspection.ts:60), so an input declared as Type.Literal(\"foo\") accepts any string at validation time.

Code quality — isPlainObjectValue in serializable.ts

packages/workflows/src/shared/serializable.ts:9-13:
```ts
function isPlainObjectValue(value: unknown): boolean {
if (value === null || typeof value !== "object" || Array.isArray(value)) return true;
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
```
Two readability things:

  • The name says "is a plain object", but the function returns true for null, primitives, and arrays. It works because TypeBox tries each union member in order and arrays/null match earlier members, but a reader has to reverse-engineer that to see the intent. Consider renaming (e.g. acceptableForObjectMember) or returning false for those values and letting earlier union members handle them.
  • The function definition sits between two import groups (line 9 is after the Refine, Type, … import but before the Value and WorkflowOutputValues imports). Hoist it below all imports to match the rest of the package.

Scope — unrelated cleanups bundled in

The PR title is "TypeBox-native input/output schemas," but the diff also removes unused locals/imports across unrelated packages:

  • packages/coding-agent/src/modes/interactive/components/diff.ts — drops unused digitsStart
  • packages/coding-agent/src/modes/interactive/components/footer.ts — removes sanitizeStatusText
  • packages/coding-agent/src/modes/interactive/chat-input-actions.ts — drops EditorComponent import
  • packages/mcp/host-html-template.ts — drops cspContent, resourceHtml, uiHtml (CSP application is removed; the locals appear unused in the template body, but please confirm this is genuinely dead code rather than an accidental functional change — applyCspMeta/buildCspMetaContent are still exported)
  • packages/subagents/src/runs/background/async-execution.ts, packages/subagents/src/shared/utils.ts — drop os, APP_NAME, and the getOutputTail/writePrompt helpers
  • packages/subagents/src/runs/shared/nested-render.ts — drops NestedStepSummary type import

These look like real dead-code wins, but they're tangential to the migration. Either split into a follow-up commit/PR or call them out explicitly in the PR description so reviewers don't need to verify they're unrelated.

Minor

  • packages/workflows/src/workflows/define-workflow.ts:138-142freezeSchemaMap only top-level-freezes the map (intentional, per the comment, to preserve TypeBox internal symbols). Good — but worth keeping a unit test for "compiled definition still validates after JSON round-trip through structuredClone" so a future refactor doesn't accidentally deep-freeze.
  • The Type.Unsafe<T>(Type.Object({}, { additionalProperties: true })) pattern appears repeatedly in contract fixtures (contract-hil-nested-parent.ts, contract-hil-nested-child.ts, etc.) where the actual shapes look knowable. The README correctly tells authors to prefer precise schemas — tightening these fixtures (where reasonable) would make them better exemplars of the new system. (Skip if they're intentionally exercising the loose-shape path.)
  • CLAUDE.md still mentions @sinclair/typebox under "Tech Stack" / "Schema tooling". The loader aliases both names so this works, but the canonical package is now typebox 1.x — worth a one-line update.
  • Doc snag: the runtime error string declare it with .output(\"<key>\", Type....) (executor.ts:1711 and the corresponding docs) leaks the Type.... placeholder verbatim to users. Consider .output(\"<key>\", <TypeBox schema>) or Type.<Kind>(…).

What looks good

  • Picking Type.Cyclic for the recursive serializable schema reads cleanly and replaces the Zod recursion neatly.
  • The formatInstancePath translation (/a/0/ba[0].b) preserves the historical error-path wording — nice attention to error-message stability.
  • assertWorkflowOutputsExplicit keeps the select-vs-generic error split after Value.Check so consumers still get the "must be one of [...]" message for unions of literals rather than a generic TypeBox error.
  • The selectWorkflowOutputs comment correctly explains why the second validation pass is unnecessary at the parent boundary — exactly the kind of why-comment worth keeping.
  • Static<> flowing through DeclaredEntry<K, S extends TOptional<TSchema>> is the right shape: a default keeps the key required at the type level, which matches runtime behavior after defaults are resolved.

Nothing in here looks blocking; (1) is the one I'd most want addressed before this lands as the new authoring surface, since the inputs/outputs asymmetry will trip authors up.

@lavaman131
lavaman131 merged commit 3ffe3c0 into main Jun 2, 2026
9 checks passed
@lavaman131
lavaman131 deleted the feat/workflows-typebox-schemas branch June 2, 2026 04:10
lavaman131 added a commit that referenced this pull request Jun 29, 2026
…1181)

* feat(workflows)!: TypeBox-native input/output schemas (replace Zod)

Workflow inputs and outputs are now declared with TypeBox schemas instead
of the legacy { type, required, default, choices } descriptor, and Zod is
removed entirely.

- Authoring: .input("prompt", Type.String()), .input("count", Type.Number({ default: 2 })),
  .input("flavor", Type.Union([Type.Literal("a"), Type.Literal("b")])),
  .output("packet", Type.Object({ topic: Type.String(), score: Type.Number() })).
  Type.Optional(...) marks an optional field. Type is re-exported from
  @bastani/workflows (with the Static and TSchema types) for single-import.
- Typing: ctx.inputs, the .run() return, and ctx.workflow(child).outputs are
  precisely typed via Static<> (missing-required, wrong-type, and nested-shape
  mismatches are caught statically; excess keys are enforced at runtime).
- Runtime: inputs and outputs validate via TypeBox Value (defaults applied,
  precise path errors); serializable.ts reimplemented on TypeBox, dropping zod.
- A schema-introspection adapter derives the normalized field descriptor
  (type/choices/default/required) from a TypeBox schema, so the inputs picker
  UI, validation, and dispatch keep working without being rewritten
  (TUnion<TLiteral> -> select, TBoolean -> toggle, TNumber -> number, etc.).
- Migrated all builtin and contract workflows and the test suite to the
  TypeBox API; updated docs (coding-agent + workflows) and the CHANGELOG.

BREAKING CHANGE: the { type, required, default, choices } input/output
descriptor is removed; workflows must declare inputs/outputs with TypeBox
schemas. This feature was unreleased, so there is no migration path for
shipped consumers.

Assistant-model: Claude Opus 4.8

* chore(workflows): drop unused locals in builtins and contract workflows

The TypeBox migration inlined select choices into Type.Union([Type.Literal(...)]),
leaving the original const choice arrays (FLAVORS, VARIANTS, INVALID_MODES)
unused, plus a stale reviewerErrorResult(iteration) param and an unused
erasedDef binding. These tripped the project's noUnusedLocals/noUnusedParameters
standard (enforced by the IDE) even though the root tsconfig does not set the
flags. Remove them so the migrated built-in and .atomic contract workflows are
clean.

Assistant-model: Claude Opus 4.8

* refactor(workflows): precise schemas, enable noUnused* repo-wide, refresh docs

Post-migration polish for the TypeBox-native workflow schemas:

- Precise output types where the shape is known, replacing loose
  Type.Array(Type.Unknown())/additionalProperties schemas: deep-research
  'partitions' -> Type.Array(Type.String()); goal 'receipts' -> a precise
  Type.Object array; and the contract workflows (contract-valid echo/items,
  contract-hil-basic, contract-hil-nested-root, contract-parent,
  contract-complex-root) now declare precise Type.Object/Type.Array schemas
  or Type.Unsafe<Interface>(...) so ctx.inputs/.run() return/child.outputs
  are precisely typed and runtime validation enforces the real shape.
- Enable noUnusedLocals/noUnusedParameters in the root tsconfig (the repo
  standard per AGENTS.md) so 'bun run typecheck'/lint/CI match the IDE and
  this can't drift again, and remove the dead locals/imports it surfaced
  across workflows, tests, coding-agent, subagents, and mcp. Exhaustiveness
  checks (const _x: never = ...) are preserved via an explicit reference.
- Docs: packages/coding-agent/docs/workflows.md and packages/workflows
  README now steer authors toward precise TypeBox schemas (with a
  schema->Static table and a loose-vs-precise example) and document the
  Type.Unsafe<T> escape hatch for genuinely dynamic data.

Assistant-model: Claude Opus 4.8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant