refactor: discriminated union frontmatter with $status routing #499

Closed
opened 2026-05-25 06:16:30 +00:00 by xiaoju · 1 comment
Owner

Summary

Redesign frontmatter as discriminated unions keyed by $status, and make single-exit roles omit it entirely.

Motivation

With #490 (status-based routing), frontmatter's role changed fundamentally:

  • Old: structured data for JSONata condition evaluation
  • New: status for routing + context parameters for edge prompt templates

But the current design has issues:

  1. Flat schema — all fields declared regardless of which exit path is taken. Agent fills irrelevant fields with nulls.
  2. status is too generic — conflicts with domain fields. As a routing discriminant it deserves a system-reserved name.
  3. Single-exit roles forced to write status: _ — meaningless boilerplate.

Design

Naming: $status

$ prefix is already the system-reserved convention ($START, $END). $status signals "routing discriminant, not domain data".

Multi-exit roles: oneOf discriminated union

Each variant declares only the fields relevant to that exit path:

reviewer:
  frontmatter:
    oneOf:
      - properties:
          $status: { const: "approved" }
          branch: { type: string }
          worktree: { type: string }
        required: [$status, branch, worktree]
      - properties:
          $status: { const: "rejected" }
          comments: { type: string }
        required: [$status, comments]

Aligns 1:1 with graph edges:

graph:
  reviewer:
    approved: { role: tester, prompt: "Test branch {{{branch}}} at {{{worktree}}}" }
    rejected: { role: developer, prompt: "Fix: {{{comments}}}" }

Each edge prompt template only references fields from its own variant — type safe.

Single-exit roles: plain object, no $status

developer:
  frontmatter:
    type: object
    properties:
      branch: { type: string }
      worktree: { type: string }
    required: [branch, worktree]

Engine infers _ when $status is absent. No boilerplate.

evaluate() change

const status = lastRole === START_ROLE ? "_" : (lastOutput["$status"] ?? "_");

Changes Required

1. Engine / protocol

  • evaluate() — read $status instead of status, default to _ when absent
  • workflow-protocol types — document $status convention
  • Frontmatter validation — verify oneOf + const discriminated unions work correctly
  • buildOutputFormatInstruction — adapt agent instructions to show per-variant field lists

2. solve-issue.yaml frontmatter redesign

Role Exits $status values Variant-specific fields
planner 2 ready, insufficient_info ready: plan, repoPath / insufficient_info: (none)
developer 1 (omit) branch, worktree
reviewer 2 approved, rejected approved: branch, worktree / rejected: comments
tester 3 passed, fix_code, fix_spec passed: branch, worktree / fix_code: report / fix_spec: report
committer 2 committed, hook_failed committed: prUrl / hook_failed: error

3. Edge prompts

Update all edge prompts to use mustache templates referencing variant-specific fields.

4. Tests

  • Moderator tests for $status and _ default
  • Frontmatter validation tests for oneOf discriminated unions
  • solve-issue integration test updates

—— 小橘 🍊(NEKO Team)

## Summary Redesign frontmatter as discriminated unions keyed by `$status`, and make single-exit roles omit it entirely. ## Motivation With #490 (status-based routing), frontmatter's role changed fundamentally: - **Old**: structured data for JSONata condition evaluation - **New**: `status` for routing + context parameters for edge prompt templates But the current design has issues: 1. **Flat schema** — all fields declared regardless of which exit path is taken. Agent fills irrelevant fields with nulls. 2. **`status` is too generic** — conflicts with domain fields. As a routing discriminant it deserves a system-reserved name. 3. **Single-exit roles forced to write `status: _`** — meaningless boilerplate. ## Design ### Naming: `$status` `$` prefix is already the system-reserved convention (`$START`, `$END`). `$status` signals "routing discriminant, not domain data". ### Multi-exit roles: `oneOf` discriminated union Each variant declares only the fields relevant to that exit path: ```yaml reviewer: frontmatter: oneOf: - properties: $status: { const: "approved" } branch: { type: string } worktree: { type: string } required: [$status, branch, worktree] - properties: $status: { const: "rejected" } comments: { type: string } required: [$status, comments] ``` Aligns 1:1 with graph edges: ```yaml graph: reviewer: approved: { role: tester, prompt: "Test branch {{{branch}}} at {{{worktree}}}" } rejected: { role: developer, prompt: "Fix: {{{comments}}}" } ``` Each edge prompt template only references fields from its own variant — type safe. ### Single-exit roles: plain object, no `$status` ```yaml developer: frontmatter: type: object properties: branch: { type: string } worktree: { type: string } required: [branch, worktree] ``` Engine infers `_` when `$status` is absent. No boilerplate. ### evaluate() change ```typescript const status = lastRole === START_ROLE ? "_" : (lastOutput["$status"] ?? "_"); ``` ## Changes Required ### 1. Engine / protocol - [ ] `evaluate()` — read `$status` instead of `status`, default to `_` when absent - [ ] `workflow-protocol` types — document `$status` convention - [ ] Frontmatter validation — verify `oneOf` + `const` discriminated unions work correctly - [ ] `buildOutputFormatInstruction` — adapt agent instructions to show per-variant field lists ### 2. solve-issue.yaml frontmatter redesign | Role | Exits | $status values | Variant-specific fields | |------|-------|---------------|------------------------| | planner | 2 | `ready`, `insufficient_info` | ready: `plan`, `repoPath` / insufficient_info: (none) | | developer | 1 | (omit) | `branch`, `worktree` | | reviewer | 2 | `approved`, `rejected` | approved: `branch`, `worktree` / rejected: `comments` | | tester | 3 | `passed`, `fix_code`, `fix_spec` | passed: `branch`, `worktree` / fix_code: `report` / fix_spec: `report` | | committer | 2 | `committed`, `hook_failed` | committed: `prUrl` / hook_failed: `error` | ### 3. Edge prompts Update all edge prompts to use mustache templates referencing variant-specific fields. ### 4. Tests - Moderator tests for `$status` and `_` default - Frontmatter validation tests for oneOf discriminated unions - solve-issue integration test updates —— 小橘 🍊(NEKO Team)
Author
Owner

Phase 拆分

  • Phase 1: $status routing + _ default → #500
  • Phase 2: discriminated union frontmatter + solve-issue redesign → #501
  • Phase 3: buildOutputFormatInstruction oneOf support → #502

—— 小橘 🍊(NEKO Team)

## Phase 拆分 - Phase 1: $status routing + _ default → #500 - Phase 2: discriminated union frontmatter + solve-issue redesign → #501 - Phase 3: buildOutputFormatInstruction oneOf support → #502 —— 小橘 🍊(NEKO Team)
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: uncaged/workflow#499