Skip to content

RFC: per-phase config + a workflow layer above phases (DAG, branching, conditionals) #74

Description

@JuroOravec

Background

Saifdocs v0.2 emitted a per-phase `tests/gate.sh` (a plain bash script asserting "the doc page got written, is non-empty, has body content"). Conceptually a perfect fit for a per-phase post-condition check. Mechanically, it didn't work — saifctl's per-phase test contract is a test-profile-shaped spec file (vitest/pytest/gotest), and the bash script was silently dropped by the test runner. We worked around it in saifdocs v0.3.0 by emitting `tests/public/output.spec.ts` instead, but the underlying observation is interesting:

A per-phase gate script could have totally worked — but saifctl has no per-phase gate concept. There's only a single, feature-level `gateScript`.

This RFC explores two layered ideas that fall out of that observation.


Part 1 — Per-phase static configuration

Problem. Today, most "things you'd want different per phase" are global: `gateScript`, agent profile, designer profile, indexer profile, test profile, network policy (Cedar), env, secrets. The phase-level knobs we do have are scattered (`feature.yml` / `phase.yml` schema fields like `tests.mutable`, `tests.fail2pass`, `tests.enforce`, `critics`).

Why it matters now. Saifdocs is one of many "external feature emitters" we'll see. They're going to want phase-specific behavior. Examples beyond saifdocs:

  • a phase that does pure I/O (no compile/test) → cheaper agent + skip test runner
  • a research phase that should run with a network-allowlisted Cedar policy, while subsequent build phases run network-denied
  • a phase whose `gateScript` is "did the file change in this expected way" (e.g. linter clean), separate from the feature's overall test gate
  • a critic phase that should use a different, smaller model than the implementer

Proposal. Promote `phase.yml` (already optional today) to a richer schema. Things to consider supporting per-phase:

  • `gateScript` (path or inline) — runs after the agent attempt, before tests
  • `agent` / `agentInstall` / `agentEnv` — override the run-level agent for this phase
  • `designer` / `indexer` overrides
  • `testProfile` override (e.g. "this phase has no behavior to verify; skip the runner")
  • `cedar` (Leash policy override)
  • `secrets` / `env` (allowlist)
  • `timeout`, `maxRounds`
  • `mutability` (tests, sources, configs — already there in `tests:`, but extend)
  • `skip-test-runner: true` (the saifdocs use case — pure-output phase, no behavior to verify)

The discoverer (`src/specs/phases/discover.ts`) already walks `phase.yml` files and validates against a schema; the compiler (`src/specs/phases/compile.ts`) merges feature-level + phase-level when emitting subtasks. This RFC is mostly schema work + threading.

Acceptance (rough):

  • `phase.yml` schema covers the list above (or a deliberate subset)
  • `feat phases compile` resolves merge order: phase.yml > feature.yml > run-level CLI flags > config defaults
  • Existing features without `phase.yml` keep working unchanged

Part 2 — A workflow layer above phases (the bigger picture)

Once Part 1 lands, saifctl's `feature` looks a lot like an agentic pipeline:

  • a `phase` is a single atomic unit (one agent attempt + critics + gate)
  • `phases.order` is currently a flat list

What we don't have, but increasingly want:

  • Branching — "if phase A produced output X, run phase B; else run phase C"
  • Conditionals — "skip phase D when env Y is set"
  • Loops — "run phase E up to N times until condition Z"
  • Parallel — "run phase F and G concurrently after A"
  • Sub-flows — "phase H is itself a workflow of phases H1..H5"

That last one is the key insight: a workflow folds into a single "phase" externally. So the layering is recursive — a phase can be atomic or a workflow of sub-phases.

This puts us close to the territory of Mastra, Airflow, LangGraph, Temporal, Hatchet (we already use Hatchet for run orchestration internally — but at the run level, not the phase level).

Proposed direction:

  1. Decouple the workflow definition from `feature.yml`. Right now phase ordering lives in `feature.yml.phases.order` — a flat list. Promote workflow logic into its own file (`workflow.yml` or `flow.yml`) that can express branching, conditionals, loops. `feature.yml` keeps owning the high-level metadata (critics defaults, mutability defaults).

  2. File-system-first DSL. Match how phases work today: directories + YAML, not code. So a workflow looks like:
    ```
    workflow.yml # top-level: `steps: [phase-a, branch:if-X]`
    branches/
    if-X.yml # `if: outputs.phase-a.X == "yes"` then phase-b else phase-c
    ```
    This stays auditable, inspectable, and diff-friendly. CI can lint it.

  3. Eventually: SDK. Once the file-form is stable, expose an SDK (TS / Python) that produces the same workflow definitions programmatically. Users get the choice — quick prototyping in code, hardened workflows in YAML. (Mastra and Airflow both work this way.)

  4. Composable. A whole workflow folds into one externally-visible "phase". So `workflow-foo` can be referenced from `workflow-bar` as a single step. This matters for sharing — community/internal libraries of reusable agentic flows.

  5. Dynamic step generation (stretch). One of the steps could itself be an agent that emits more workflow steps for the next stage, based on what it learned. This is what makes the system handle truly open-ended tasks (the agent extends its own plan), while still being constrained by the higher-level structure (the parent workflow says "after the planner produces a plan, run each step under this Cedar policy").


Part 3 — SaaS productization (long-term)

If parts 1 + 2 land, the surface area to productize is small and clean:

Inputs:

  1. Workspace — S3 / zip / GitHub URL
  2. Install script + workspace config — Dockerfile, `saifctl init` config, env, secrets allowlist
  3. Agentic DAG workflow — file-system tree (phases + workflow.yml) or equivalent JSON / SDK-built artifact

Output: the workflow runs in our infra (Hatchet + leash + sandbox containers), gated step-by-step, with per-phase Cedar policies, observability, and the option to drop into an interactive session at any breakpoint.

The pitch:

Like running Claude Code / Cursor in your terminal, but with gated progress and branching logic defined ahead of time. Stops drifting, costs are predictable, results are reproducible — and an agent can extend its own plan within the constraints you defined.

That's a real differentiation against "wrap an LLM with retries and call it a workflow" approaches.


What I'd want out of this RFC

Sequencing — what to do first:

  • Land Part 1 (per-phase static config, especially `gateScript` and `skip-test-runner`). Direct fix for saifdocs's case + small scope.
  • Spike a `workflow.yml` prototype with one branching example. See what falls out before committing to the schema.
  • Survey Mastra / LangGraph / Hatchet's workflow primitives — look for the smallest set that covers branching, conditionals, loops, parallel, sub-flows. Don't reinvent.
  • Decide: does `feature.yml` evolve to absorb workflow concepts, or do we introduce `workflow.yml` as a new top-level artifact? (My read: new artifact. `feature` is a noun, `workflow` is a verb — they're different things.)

Cross-references:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions