From 1a37928bb9721f4b8b4ffb4ae6666dfd8ce936d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 5 Jun 2026 23:13:54 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20workflow-authoring=20docs=20?= =?UTF-8?q?=E2=80=94=20type:object=20+=20const=20vs=20enum=20clarity=20(#1?= =?UTF-8?q?23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add type:object to all frontmatter examples (flat and oneOf) - Restructure $status section: Multi-exit (oneOf/const) vs Single-exit (flat/enum) - Add Important rules box clarifying validation requirements - Restore Custom Fields subsection Fixes #123 --- .changeset/workflow-authoring-docs.md | 10 ++++++++ .../util/src/workflow-authoring-reference.ts | 23 +++++++++++-------- 2 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 .changeset/workflow-authoring-docs.md diff --git a/.changeset/workflow-authoring-docs.md b/.changeset/workflow-authoring-docs.md new file mode 100644 index 0000000..08d0812 --- /dev/null +++ b/.changeset/workflow-authoring-docs.md @@ -0,0 +1,10 @@ +--- +"@united-workforce/util": patch +--- + +fix: workflow-authoring docs — add type:object to oneOf examples, clarify const vs enum rules (#123) + +- All frontmatter examples include `type: object` (both flat and oneOf) +- Restructure $status section: "Multi-exit (oneOf)" vs "Single-exit (flat schema)" +- Add "Important rules" box: type:object required, const only in oneOf, enum in flat +- Restore "Custom Fields" subsection diff --git a/packages/util/src/workflow-authoring-reference.ts b/packages/util/src/workflow-authoring-reference.ts index 7d32e31..28c1f68 100644 --- a/packages/util/src/workflow-authoring-reference.ts +++ b/packages/util/src/workflow-authoring-reference.ts @@ -28,6 +28,7 @@ roles: # named actors 2. Do that output: "..." # what the agent should produce frontmatter: # JSON Schema for structured output + type: object oneOf: - properties: $status: { const: "ready" } @@ -71,10 +72,13 @@ The \`frontmatter\` field is a standard JSON Schema. It defines the structured f ### \`$status\` Field -\`$status\` is the only standard field. Its value determines which graph edge the moderator follows. Use \`const\` to constrain each variant: +\`$status\` is the only standard field. Its value determines which graph edge the moderator follows. + +**Multi-exit (oneOf)** — use \`const\` to constrain each variant: \`\`\`yaml frontmatter: + type: object oneOf: - properties: $status: { const: "done" } @@ -86,13 +90,7 @@ frontmatter: required: [$status, error] \`\`\` -### Custom Fields - -Add any fields you need for data passing between roles. These are available in edge prompts via Mustache templates. - -### Flat Schema (Single Status) - -When a role has only one outcome, use \`enum\` with a single value: +**Single-exit (flat schema)** — use \`enum\` (not \`const\`): \`\`\`yaml frontmatter: @@ -105,7 +103,14 @@ frontmatter: required: [$status, summary] \`\`\` -Note: \`$status: { const: "done" }\` is **not** valid in flat schemas — the validator requires \`enum\` or \`oneOf\` with \`const\`. Use \`const\` only inside \`oneOf\` variants. +**Important rules:** +- \`type: object\` is **required** at the top level of frontmatter (both flat and oneOf) +- In flat schemas, \`$status\` must use \`enum: [value]\`, NOT \`const: "value"\` — the validator rejects \`const\` outside of \`oneOf\` variants +- In \`oneOf\` schemas, each variant's \`$status\` must use \`const: "value"\` + +### Custom Fields + +Add any fields you need for data passing between roles. These are available in edge prompts via Mustache templates. ## Graph Routing -- 2.43.0 From 68079cc00390ea02a427945c75c43a03c8c7e840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 5 Jun 2026 23:31:56 +0000 Subject: [PATCH 2/2] fix: unify $status to const-only, drop enum support (#123) - Validator: hasStatusConst/getConstStatuses replace enum checks - enum in $status is now rejected with clear error message - All docs/examples/tests migrated from enum to const/oneOf - bootstrap hello.yaml updated Fixes #123 --- .changeset/workflow-authoring-docs.md | 15 ++- .../cli/src/__tests__/current-role.test.ts | 28 ++-- .../cli/src/__tests__/thread-location.test.ts | 6 +- .../src/__tests__/thread-show-status.test.ts | 2 +- .../__tests__/thread-start-cwd-cli.test.ts | 2 +- .../src/__tests__/validate-semantic.test.ts | 123 +++++++++--------- .../src/__tests__/workflow-resolution.test.ts | 2 +- packages/cli/src/commands/prompt.ts | 2 +- packages/cli/src/validate-semantic.ts | 26 ++-- .../build-output-format-instruction.test.ts | 4 +- .../util/src/workflow-authoring-reference.ts | 10 +- 11 files changed, 116 insertions(+), 104 deletions(-) diff --git a/.changeset/workflow-authoring-docs.md b/.changeset/workflow-authoring-docs.md index 08d0812..a3c2e42 100644 --- a/.changeset/workflow-authoring-docs.md +++ b/.changeset/workflow-authoring-docs.md @@ -1,10 +1,15 @@ --- +"@united-workforce/cli": patch "@united-workforce/util": patch --- -fix: workflow-authoring docs — add type:object to oneOf examples, clarify const vs enum rules (#123) +fix: unify $status to const-only, drop enum support (#123) -- All frontmatter examples include `type: object` (both flat and oneOf) -- Restructure $status section: "Multi-exit (oneOf)" vs "Single-exit (flat schema)" -- Add "Important rules" box: type:object required, const only in oneOf, enum in flat -- Restore "Custom Fields" subsection +Breaking: `$status` in frontmatter now requires `const` everywhere. +`enum` is no longer accepted and will be rejected by the validator. + +- Validator: `hasStatusConst()` / `getConstStatuses()` replace enum-based checks +- Error message: "must define $status as const (or oneOf with const)" +- workflow-authoring docs: all examples use `const`, enum explicitly noted as unsupported +- bootstrap hello.yaml: `$status: { const: done }` +- All test fixtures migrated from enum to const/oneOf diff --git a/packages/cli/src/__tests__/current-role.test.ts b/packages/cli/src/__tests__/current-role.test.ts index 7eb8ce7..4ce4a12 100644 --- a/packages/cli/src/__tests__/current-role.test.ts +++ b/packages/cli/src/__tests__/current-role.test.ts @@ -28,9 +28,13 @@ roles: $status: "ready" frontmatter: type: object - required: ["$status"] - properties: - $status: { type: string, enum: ["ready", "not-ready"] } + oneOf: + - properties: + $status: { const: "ready" } + required: ["$status"] + - properties: + $status: { const: "not-ready" } + required: ["$status"] roleB: description: Second role goal: Do B @@ -42,7 +46,7 @@ roles: type: object required: ["$status"] properties: - $status: { type: string, enum: ["done"] } + $status: { const: "done" } graph: $START: new: @@ -82,9 +86,13 @@ roles: $status: "pass" frontmatter: type: object - required: ["$status"] - properties: - $status: { type: string, enum: ["pass", "fail"] } + oneOf: + - properties: + $status: { const: "pass" } + required: ["$status"] + - properties: + $status: { const: "fail" } + required: ["$status"] roleB: description: Pass role goal: Do B @@ -96,7 +104,7 @@ roles: type: object required: ["$status"] properties: - $status: { type: string, enum: ["done"] } + $status: { const: "done" } roleC: description: Fail role goal: Do C @@ -108,7 +116,7 @@ roles: type: object required: ["$status"] properties: - $status: { type: string, enum: ["done"] } + $status: { const: "done" } graph: $START: new: @@ -155,7 +163,7 @@ roles: type: object required: ["$status"] properties: - $status: { type: string, enum: ["done"] } + $status: { const: "done" } graph: $START: new: diff --git a/packages/cli/src/__tests__/thread-location.test.ts b/packages/cli/src/__tests__/thread-location.test.ts index 97f8712..226f9e6 100644 --- a/packages/cli/src/__tests__/thread-location.test.ts +++ b/packages/cli/src/__tests__/thread-location.test.ts @@ -54,7 +54,7 @@ roles: type: object required: ["$status"] properties: - $status: { type: string, enum: ["ready"] } + $status: { const: "ready" } graph: $START: new: @@ -114,7 +114,7 @@ roles: type: object required: ["$status"] properties: - $status: { type: string, enum: ["ready"] } + $status: { const: "ready" } graph: $START: new: @@ -161,7 +161,7 @@ roles: type: object required: ["$status"] properties: - $status: { type: string, enum: ["ready"] } + $status: { const: "ready" } graph: $START: new: diff --git a/packages/cli/src/__tests__/thread-show-status.test.ts b/packages/cli/src/__tests__/thread-show-status.test.ts index 367a64f..268ff2e 100644 --- a/packages/cli/src/__tests__/thread-show-status.test.ts +++ b/packages/cli/src/__tests__/thread-show-status.test.ts @@ -31,7 +31,7 @@ roles: type: object required: ["$status"] properties: - $status: { type: string, enum: ["ready"] } + $status: { const: "ready" } graph: $START: new: diff --git a/packages/cli/src/__tests__/thread-start-cwd-cli.test.ts b/packages/cli/src/__tests__/thread-start-cwd-cli.test.ts index 4831d1d..29801d3 100644 --- a/packages/cli/src/__tests__/thread-start-cwd-cli.test.ts +++ b/packages/cli/src/__tests__/thread-start-cwd-cli.test.ts @@ -54,7 +54,7 @@ roles: type: object required: ["$status"] properties: - $status: { type: string, enum: ["ready"] } + $status: { const: "ready" } graph: $START: new: diff --git a/packages/cli/src/__tests__/validate-semantic.test.ts b/packages/cli/src/__tests__/validate-semantic.test.ts index ec715a9..ab39fdc 100644 --- a/packages/cli/src/__tests__/validate-semantic.test.ts +++ b/packages/cli/src/__tests__/validate-semantic.test.ts @@ -17,7 +17,7 @@ function makeWorkflow(overrides?: Partial): WorkflowPayload { frontmatter: { type: "object", properties: { - $status: { enum: ["done"] }, + $status: { const: "done" }, plan: { type: "string" }, }, required: ["$status", "plan"], @@ -85,7 +85,7 @@ describe("Suite 1: Role Reference Integrity", () => { output: "None", frontmatter: { type: "object", - properties: { $status: { enum: ["done"] } }, + properties: { $status: { const: "done" } }, required: ["$status"], } as unknown as string, }; @@ -187,7 +187,7 @@ describe("Suite 2: Graph Structure", () => { output: "Isolated", frontmatter: { type: "object", - properties: { $status: { enum: ["done"] } }, + properties: { $status: { const: "done" } }, required: ["$status"], } as unknown as string, }; @@ -272,8 +272,8 @@ describe("Suite 3: Status-Edge Consistency", () => { }); }); -describe("Suite 3b: Enum-Based Multi-Exit", () => { - test("3b.1 enum multi-exit passes with matching graph keys", () => { +describe("Suite 3b: Enum-Based $status is Rejected", () => { + test("3b.1 enum multi-exit is rejected (must use oneOf + const)", () => { const wf = makeWorkflow(); wf.roles.reviewer = { ...wf.roles.reviewer, @@ -291,52 +291,10 @@ describe("Suite 3b: Enum-Based Multi-Exit", () => { rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null }, }; const errors = validateWorkflow(wf); - expect(errors).toEqual([]); + expect(errors.some((e) => e.includes("must define") && e.includes("const"))).toBe(true); }); - test("3b.2 enum multi-exit with extra graph key", () => { - const wf = makeWorkflow(); - wf.roles.reviewer = { - ...wf.roles.reviewer, - frontmatter: { - type: "object", - properties: { - $status: { enum: ["approved", "rejected"] }, - comments: { type: "string" }, - }, - required: ["$status", "comments"], - } as unknown as string, - }; - wf.graph.reviewer = { - approved: { role: "$END", prompt: "Done", location: null }, - rejected: { role: "writer", prompt: "Fix", location: null }, - timeout: { role: "$END", prompt: "Timed out", location: null }, - }; - const errors = validateWorkflow(wf); - expect(errors.some((e) => e.includes("extra status keys: timeout"))).toBe(true); - }); - - test("3b.3 enum multi-exit with missing graph key", () => { - const wf = makeWorkflow(); - wf.roles.reviewer = { - ...wf.roles.reviewer, - frontmatter: { - type: "object", - properties: { - $status: { enum: ["approved", "rejected"] }, - comments: { type: "string" }, - }, - required: ["$status", "comments"], - } as unknown as string, - }; - wf.graph.reviewer = { - approved: { role: "$END", prompt: "Done", location: null }, - }; - const errors = validateWorkflow(wf); - expect(errors.some((e) => e.includes("missing status keys: rejected"))).toBe(true); - }); - - test("3b.4 enum with single explicit value passes", () => { + test("3b.2 enum single-exit is rejected (must use const)", () => { const wf = makeWorkflow(); wf.roles.writer = { ...wf.roles.writer, @@ -351,28 +309,71 @@ describe("Suite 3b: Enum-Based Multi-Exit", () => { }; wf.graph.writer = { ready: { role: "reviewer", prompt: "Review: {{{plan}}}", location: null } }; const errors = validateWorkflow(wf); - expect(errors).toEqual([]); + expect(errors.some((e) => e.includes("must define") && e.includes("const"))).toBe(true); }); +}); - test("3b.5 enum multi-exit mustache var not in frontmatter", () => { +describe("Suite 3c: Const-Based Flat Schema", () => { + test("3c.1 flat schema with const $status passes validation", () => { const wf = makeWorkflow(); - wf.roles.reviewer = { - ...wf.roles.reviewer, + wf.roles.writer = { + ...wf.roles.writer, frontmatter: { type: "object", properties: { - $status: { enum: ["approved", "rejected"] }, - comments: { type: "string" }, + $status: { const: "done" }, + plan: { type: "string" }, }, - required: ["$status", "comments"], + required: ["$status", "plan"], } as unknown as string, }; - wf.graph.reviewer = { - approved: { role: "$END", prompt: "Done: {{{nonexistent}}}", location: null }, - rejected: { role: "writer", prompt: "Fix: {{{comments}}}", location: null }, + const errors = validateWorkflow(wf); + expect(errors).toEqual([]); + }); + + test("3c.2 flat schema with const $status detects extra graph key", () => { + const wf = makeWorkflow(); + wf.roles.writer = { + ...wf.roles.writer, + frontmatter: { + type: "object", + properties: { + $status: { const: "done" }, + plan: { type: "string" }, + }, + required: ["$status", "plan"], + } as unknown as string, + }; + wf.graph.writer = { + done: { role: "reviewer", prompt: "Review.", location: null }, + extra: { role: "$END", prompt: "Nope.", location: null }, }; const errors = validateWorkflow(wf); - expect(errors.some((e) => e.includes("nonexistent") && e.includes("not found"))).toBe(true); + expect(errors.some((e) => e.includes("extra status keys") && e.includes("extra"))).toBe(true); + }); + + test("3c.3 flat schema with const $status validates mustache vars", () => { + const wf = makeWorkflow(); + wf.roles.writer = { + ...wf.roles.writer, + frontmatter: { + type: "object", + properties: { + $status: { const: "done" }, + plan: { type: "string" }, + }, + required: ["$status", "plan"], + } as unknown as string, + }; + wf.graph.writer = { + done: { role: "reviewer", prompt: "Review: {{{nonexistent}}}", location: null }, + }; + const errors = validateWorkflow(wf); + expect( + errors.some( + (e) => e.includes('prompt variable "nonexistent"') && e.includes('role "writer"'), + ), + ).toBe(true); }); }); @@ -480,7 +481,7 @@ describe("Suite 6: Multiple Errors Collection", () => { output: "None", frontmatter: { type: "object", - properties: { $status: { enum: ["done"] } }, + properties: { $status: { const: "done" } }, required: ["$status"], } as unknown as string, }; diff --git a/packages/cli/src/__tests__/workflow-resolution.test.ts b/packages/cli/src/__tests__/workflow-resolution.test.ts index 6f85efa..847c546 100644 --- a/packages/cli/src/__tests__/workflow-resolution.test.ts +++ b/packages/cli/src/__tests__/workflow-resolution.test.ts @@ -31,7 +31,7 @@ function makeMinimalPayload(name: string, description: string): WorkflowPayload frontmatter: { type: "object", properties: { - $status: { type: "string", enum: ["done"] }, + $status: { const: "done" }, }, required: ["$status"], } as unknown as CasRef, diff --git a/packages/cli/src/commands/prompt.ts b/packages/cli/src/commands/prompt.ts index 35cc81c..33bf1a3 100644 --- a/packages/cli/src/commands/prompt.ts +++ b/packages/cli/src/commands/prompt.ts @@ -219,7 +219,7 @@ roles: frontmatter: type: object properties: - $status: { enum: [done] } + $status: { const: done } message: { type: string } required: [$status, message] graph: diff --git a/packages/cli/src/validate-semantic.ts b/packages/cli/src/validate-semantic.ts index 35b8cac..b8abf15 100644 --- a/packages/cli/src/validate-semantic.ts +++ b/packages/cli/src/validate-semantic.ts @@ -24,22 +24,22 @@ function isOneOfSchema(fm: unknown): fm is SchemaObj & { oneOf: SchemaObj[] } { return Array.isArray(obj.oneOf); } -/** Check if a frontmatter schema declares "$status" as an enum (the required form for user roles). */ -function hasStatusEnum(fm: unknown): boolean { +/** Check if a frontmatter schema declares "$status" as const (flat schema form). */ +function hasStatusConst(fm: unknown): boolean { if (typeof fm !== "object" || fm === null) return false; const obj = fm as SchemaObj; const props = obj.properties as Record | undefined; if (!props?.$status) return false; - return Array.isArray(props.$status.enum); + return typeof props.$status.const === "string"; } -/** Extract status values from an enum-based $status field. */ -function getEnumStatuses(fm: SchemaObj): string[] { +/** Extract status values from a const-based $status field. */ +function getConstStatuses(fm: SchemaObj): string[] { const props = fm.properties as Record | undefined; if (!props?.$status) return []; const statusDef = props.$status; - if (!Array.isArray(statusDef.enum)) return []; - return statusDef.enum as string[]; + if (typeof statusDef.const === "string") return [statusDef.const]; + return []; } /** Get property names from a schema object. */ @@ -248,21 +248,21 @@ function checkRoleConsistency(payload: WorkflowPayload, errors: string[]): void checkOneOfDiscriminant(roleName, variants, statuses, errors); checkStatusEdges(roleName, graphKeys, new Set(statuses), errors); checkMultiExitMustache(roleName, graphEntry, variants, errors); - } else if (hasStatusEnum(fm)) { - const statuses = getEnumStatuses(fm as SchemaObj); + } else if (hasStatusConst(fm)) { + const statuses = getConstStatuses(fm as SchemaObj); checkStatusEdges(roleName, graphKeys, new Set(statuses), errors); - // For enum-based schemas, mustache vars come from the flat properties - checkEnumMustache(roleName, graphEntry, fm as SchemaObj, errors); + // For const-based flat schemas, mustache vars come from the flat properties + checkFlatMustache(roleName, graphEntry, fm as SchemaObj, errors); } else { errors.push( - `role "${roleName}" must define "$status" as an enum (or oneOf const) in frontmatter`, + `role "${roleName}" must define "$status" as const (or oneOf with const) in frontmatter`, ); } } } /** Check mustache vars in all edge prompts against flat schema properties. */ -function checkEnumMustache( +function checkFlatMustache( roleName: string, graphEntry: Record, fm: SchemaObj, diff --git a/packages/util-agent/__tests__/build-output-format-instruction.test.ts b/packages/util-agent/__tests__/build-output-format-instruction.test.ts index 57fbadf..a975743 100644 --- a/packages/util-agent/__tests__/build-output-format-instruction.test.ts +++ b/packages/util-agent/__tests__/build-output-format-instruction.test.ts @@ -143,7 +143,7 @@ describe("buildOutputFormatInstruction", () => { { type: "object", properties: { - $status: { type: "string", enum: ["approved"] }, + $status: { const: "approved" }, branch: { type: "string" }, }, required: ["$status"], @@ -151,7 +151,7 @@ describe("buildOutputFormatInstruction", () => { { type: "object", properties: { - $status: { type: "string", enum: ["rejected"] }, + $status: { const: "rejected" }, comments: { type: "string" }, }, required: ["$status"], diff --git a/packages/util/src/workflow-authoring-reference.ts b/packages/util/src/workflow-authoring-reference.ts index 28c1f68..4a28e67 100644 --- a/packages/util/src/workflow-authoring-reference.ts +++ b/packages/util/src/workflow-authoring-reference.ts @@ -90,23 +90,21 @@ frontmatter: required: [$status, error] \`\`\` -**Single-exit (flat schema)** — use \`enum\` (not \`const\`): +**Single-exit (flat schema)** — same syntax, just no \`oneOf\` wrapper: \`\`\`yaml frontmatter: type: object properties: - $status: - type: string - enum: [done] + $status: { const: "done" } summary: { type: string } required: [$status, summary] \`\`\` **Important rules:** - \`type: object\` is **required** at the top level of frontmatter (both flat and oneOf) -- In flat schemas, \`$status\` must use \`enum: [value]\`, NOT \`const: "value"\` — the validator rejects \`const\` outside of \`oneOf\` variants -- In \`oneOf\` schemas, each variant's \`$status\` must use \`const: "value"\` +- \`$status\` always uses \`const: "value"\` — simple and consistent +- \`enum\` is **not supported** for \`$status\` — the validator will reject it ### Custom Fields -- 2.43.0