Stage: all of them. Layer: delivery, made configurable. Workflows shown: every file in .factory/workflows/.
The ticket. Runs 29 and 31, recorded in the plan brief tests in factory/engine/workflows.test.ts.
Without the layer. Run 29's plan held a note, and run 31 listed its own golden test. Both plans were refused.
What the layer did. The shared plan brief in .factory/config.yaml now carries one sentence per failure, each pinned by a test that names the run. A workflow file is where a rule like that lives once, instead of in every prompt.
Notice: the tree asks what evidence the task can give, never how big the ticket is.
Write a workflow file from scratch, make the repo's own tests accept it, run it on an issue, and pick the right one for each kind of ticket.
factory/engine/workflow.ts is the whole schema. Read it before writing a line of YAML.
- A workflow has
name,method,source,for, and a list ofsteps.manifestsdefaults to false and only a dependency update needs it. - A step has
stageandagent. Optional fields:brief,writes(one ofAGENT_ARTIFACTSinfactory/engine/artifacts.ts:intent.md,spec.md,plan.md,constitution.md,clarify.md,tasks.md,analyze.md),startsWith,checks,loop,review,gate(humanorauto) andphase(buildorfix). review: { lenses: [...] }turns the step into the voting review of F5: the lenses hunt in parallel, three verifiers vote on each finding, two keep it, and a builder fixes what survives.- A
planstep that sets nobrieftakesplanBrieffrom.factory/config.yaml, so the plan rules are written once per repo. - The
stagename also picks the model.stepStagesin.factory/config.jsonmaps a stage name to a config stage (F12). A name that appears in neitherstepStagesnor the stage list falls back to the middle model. checksmay only name entries ofCHECK_NAMES:red,green,lock,holdout,acceptance,properties,behaviour-unchanged,mutation,contract,visual,eval,plan-drift,gate,prepush,ci,prose,manifest-deps.loop: { until: [...], max: n }repeats the agent until those checks pass.maxis 1 to 5, and everyuntilcheck must also be in the step'schecks, orloadWorkflowthrows.
- Read
.factory/workflows/docs.yaml. It is the smallest workflow that plans, changes files and checks the result. - Create
.factory/workflows/typo.yamlfor a one-line docs fix. The first line must start with# Method:, becausefactory/engine/workflows.test.tsreads it.
# Method: a one-line docs fix, held to the prose rules and nothing else.
name: typo
method: prose check only
source: engineering standard
for: one-line documentation fixes
steps:
- { stage: plan, writes: plan.md, agent: planner }
- {
stage: fix,
agent: builder,
brief: "Fix the typo the ticket names and change nothing else. No em dashes, no filler.",
checks: [prose],
loop: { until: [prose], max: 2 },
}
- {
stage: review,
agent: reviewer,
review: { lenses: [reviewer, security-reviewer] },
checks: [plan-drift],
}The plan step has no brief, so it takes planBrief from .factory/config.yaml. The fix stage name maps to the build model in stepStages.
- Run
npx vitest run factory/engine/workflows.test.ts. Every file in the folder gets the same tests, sotypo.yamlis tested the moment it exists. - Break it on purpose, one edit at a time, and read each failure:
- change
agent: buildertoagent: coder: the "uses only agents that exist" test fails; - add
loop: { until: [red], max: 2 }to the fix step:loadWorkflowrejects a loop on a check the step does not run; - delete the
planstep: the "plan-drift needs a plan" test fails.
- change
- File an issue, then run it:
npx tsx factory/engine/main.ts <issue> --workflow=typo. It needsghlogged in. The agents need one credential:ANTHROPIC_API_KEY, which the script loads inside its own process, orCLAUDE_CODE_OAUTH_TOKENfromclaude setup-tokenwith--auth=subscription. Never put either on a command line. Auth modes has both paths. - Open
specs/<issue>/run.log,specs/<issue>/cost.jsonand.factory/runs/<issue>/metrics.json. The log has each step's rounds, the model it ran on, its cost and every check asokorFAIL.cost.jsonhas the cap, the total and one entry per agent call.
The shared plan brief in .factory/config.yaml holds three sentences, and every plan step gets them. Each is pinned by a test in factory/engine/workflows.test.ts that names the run that went wrong:
- "Do not list files under
specs/<issue>/": run 31 listed its golden test and the plan was refused. - "List
levels.jsonwhenever you add a file": a new file needs an owner there, andplan-driftfailed without it. - "Commentary of any kind makes the plan unusable": run 29's plan held a note, and
badPlanLinesinfactory/engine/artifacts.tsrefuses lines that are not repo paths.
One workflow per kind of task. The test "covers every task type the plan names" fails if one is deleted.
| task | workflow | what it refuses to skip |
|---|---|---|
| a bug | bug-tdd |
red on the base commit, then lock, then mutation |
| a small feature with examples | feature-atdd |
one test per Given/when/then example, then holdout |
| user-facing behaviour | feature-bdd |
Gherkin scenarios, a human gate after the amigos review |
| a large feature or epic | feature-sdd |
constitution, clarify, tasks and analyze, with human gates |
| a refactor | refactor |
characterization tests first, then behaviour-unchanged |
| a UI change | ui-visual |
a design contract, visual, the design lens |
a prompt, model or .claude change |
llm-eval |
eval red on main, a loop to the pass rate |
| the api and web boundary | contract |
the contract test first, both sides move |
| a dependency bump | deps |
manifest-deps and the whole gate, never the bump alone |
| a security fix | security |
a failing exploit test and a PR naming the class of bug |
| an open question | spike |
the output is a decision record behind a human gate |
| documentation | docs |
prose and gate |
| an incident or band breach | incident |
the incident becomes an eval before the fix |
| a red open PR | ci-fix |
the failing job log, a fix loop, an infra fault is rerun |
Ask what evidence the task can honestly give.
- Can you write a test that fails today? Use
bug-tdd. If the change is meant to leave behaviour alone, userefactor, where the pinned tests pass on both sides. - Is "done" something a person must judge? Use a workflow with a
gate: human, and put the gate before the build. - Is there no code to write yet? Use
spike. Its two steps explore and decide, and have no build. - Does the thing under test answer differently each time? Use
llm-eval, which grades a pass rate instead of one run.
factory/engine/main.ts does not pick for you: you name the workflow with --workflow=. The name also sets the ticket type used for model routes, with one exception: feature-sdd runs as type epic (F12). The older runner in factory/route-ticket.ts classifies a ticket for its own stages; read it to see how a type is chosen there.
npx vitest run factory/engine/workflows.test.tsparses and dry-runs every workflow, including yours.npx vitest run factory/engine/engine.test.tscovers the schema rejections and the loop cap ("stops at the cap and writes run.log").make prove LEVEL=workflowsruns the engine tests as one gate (scripts/prove/workflows.ts).
Which of these would your team delete first, and what evidence would you lose? Which task in your backlog fits none of them?
- Principal: Workflows are data, so any team can add one. What review does a new workflow need before it can touch a repo, and how do you retire one that nobody owns?