Agent Dev 101

Модуль 16 · Урок 16.14

Урок 16.14. Workflows: автоматизация многошаговых процессов

На этой странице
  1. Зачем это нужно
  2. Основная идея
  3. Запуск, возобновление, статус
  4. Установка и жизненный цикл
  5. Оверлеи workflow
  6. Каталоги
  7. Определение workflow
  8. Типы шагов
  9. Кастомные step-пакеты
  10. Конфигурация интеграции на шаг
  11. Выражения и shell-безопасность
  12. Переменные окружения shell-шагов
  13. Типы инпутов и коэрция
  14. Состояние и возобновление
  15. Частые вопросы
  16. Практика
  17. Краткое резюме
  18. Вопросы для самопроверки
  19. Что дальше
  20. Источники

Зачем это нужно

SDD-цикл - это последовательность команд с ревью между ними. Workflows автоматизируют такие последовательности: сцепляют команды, промпты, shell-шаги и человеческие чекпойнты в повторяемые цепочки с условной логикой, циклами, fan-out/fan-in и паузой/возобновлением с точного места остановки. First-party процессы используют эту механику: бандлы assess и bugfix - «расширение + resumable workflow + ворота ревью» (урок 16.4).

Урок - справочник: команды запуска и жизненного цикла, оверлеи, определение workflow на YAML, типы шагов, кастомные step-пакеты, выражения и - критически важный раздел - безопасность shell-шагов.

Основная идея

Workflow описывается YAML-файлом: метаданные (workflow.id, name, version), требования (requires), типизированные inputs и список steps. Движок выполняет шаги, материализует состояние прогона в .specify/workflows/runs/<run_id>/ и умеет продолжать с точки паузы (gate) или сбоя. Источник запуска - ID каталога, URL или локальный файл.

Запуск, возобновление, статус

bash
specify workflow run <source>       # -i/--input key=value (повторяемый), --json
specify workflow resume <run_id>    # -i/--input, --json
specify workflow status [<run_id>]  # --json; без ID - все прогоны
specify workflow list               # установленные в проекте

Пример запуска встроенного SDD-цикла:

bash
specify workflow run speckit -i spec="Build a kanban board with drag-and-drop task management"

Инпуты, объявленные workflow, даются через --input или запрашиваются интерактивно. --json печатает один machine-readable-объект; прогресс шагов при этом уходит в stderr, stdout несет только JSON:

json
{
  "run_id": "662bf791",
  "workflow_id": "build-and-review",
  "status": "paused",
  "current_step_id": "review",
  "current_step_index": 0
}

workflow_id - это workflow.id из YAML, не имя файла. Для failed/aborted объект содержит error с сообщением терминального шага (то же сообщение потом даст status <run_id> --json); completed и paused поле error опускают. Состояния прогона: created, running, completed, paused, failed, aborted.

resume продолжает с точного шага остановки. Переданные --input сливаются поверх сохраненных, ревалидируются по типам, и заблокированный шаг перезапускается с обновленными значениями - так прогон продолжается с информацией, появившейся после паузы:

bash
specify workflow resume <run_id> --input cmd="exit 0"

Большинство команд требуют инициализированный проект; исключение - workflow run <local-file.{yml,yaml}> вне проекта (состояние пишется в .specify/workflows/runs/ текущего каталога).

Установка и жизненный цикл

bash
specify workflow add <source>       # --dev (локальный YAML/пакет/архив), --from <url>, --version <v>
specify workflow update [<id>]      # один или все каталоговые; подтверждение; сбой - копия остается
specify workflow enable|disable <id>   # выключенный: [disabled], запуск запрещен
specify workflow remove <id>
specify workflow search [query]     # --tag, --author
specify workflow info <id> [--versions]  # шаги, инпуты, требования; --versions - история каталога

add принимает каталог, HTTPS-URL, локальный YAML, каталог с workflow.yml или архив .zip/.tar.gz/.tgz (workflow.yml в корне или в одном top-level каталоге). Каталожные записи держат текущий релиз в верхних полях и исторические в releases (у каждого - свой URL и SHA-256, опционально requires); --version требует наличия версии в каталоге высшего приоритета - fallback на другие источники отсутствует, discovery-only не устанавливаются. ID, версия и digest скачанного проверяются до установки; --version не действует для локальных путей и прямых URL.

Оверлеи workflow

Оверлеи позволяют проекту расширять или переопределять установленный workflow, не редактируя его workflow.yml - локальные кастомизации переживают bundle update и workflow add. При run <workflow-id> движок композирует базовый workflow со всеми включенными оверлеями этого ID и валидирует результат.

Оверлей - YAML с списком операций редактирования против списка шагов базового workflow. Приоритет lower-wins: сначала применяются большие числа, меньшие - последними; равные - по алфавиту ID, последний выигрывает конфликты. Файлы живут в .specify/workflows/overlays/<extends>/<id>.yml.

yaml
id: "my-overlay"
extends: "speckit"
priority: 10
enabled: true
edits:
  - insert_after: implement
    step:
      id: run-lint
      type: shell
      run: "ruff check src/"

  - replace: review-spec
    step:
      id: review-spec
      type: gate
      message: "Review the generated spec (overlay override)."
      options: [approve, reject]
      on_reject: abort

Поля: id (строчные буквы/цифры/дефисы, без overlays), extends (ID целевого workflow), priority (дефолт 10), enabled (дефолт true), edits (непустой список). Операции:

Операция step нужен Эффект
insert_after да вставить шаг сразу после якоря
insert_before да вставить сразу до
replace да заменить якорный шаг
remove нет удалить якорный шаг

Якорь - id шага базового workflow; якоря резолвятся рекурсивно внутри then, else, steps, cases.*, default - вложенные шаги достижимы. Шаблоны fan-out (step внутри fan-out) якорями быть не могут. ID шагов не содержат : - символ зарезервирован для движка.

CLI оверлеев:

bash
specify workflow overlay add <файл.yml> --priority <n>   # валидация + копия в overlays/<extends>/
specify workflow overlay list <workflow-id>              # порядок резолвера
specify workflow overlay set-priority <wf> <overlay> <n>
specify workflow overlay enable|disable <wf> <overlay>
specify workflow overlay remove <wf> <overlay>
specify workflow resolve <workflow-id>                   # составленный workflow: слои и атрибуция шагов

Примеры. Автолинт после implement (вставка после шага встроенного цикла):

yaml
id: "add-lint"
extends: "speckit"
priority: 10
edits:
  - insert_after: implement
    step:
      id: run-lint
      type: shell
      run: "ruff check src/"

Замена gate на неинтерактивную команду (priority: 5 - выиграет конфликт у приоритета 10):

yaml
id: "skip-plan-review"
extends: "speckit"
priority: 5
edits:
  - replace: review-plan
    step:
      id: review-plan
      type: command
      command: speckit.plan
      input:
        args: "{{ inputs.spec }}"

При refresh/reinstall установленного workflow проектные оверлеи сохраняются - они живут вне каталога установленного workflow.

Слоты: точки расширения вверх по потоку

Автор workflow может объявить именованный no-op слот type: slot:

yaml
- id: post-implement
  type: slot
  name: "Post-implementation checks"

id слота - уникальный якорь оверлея; name - обязательная человекочитаемая метка. Незаполненный слот завершается как skipped с output: {slot: <name>} - последующие шаги идут дальше. Заполнение - schema-валидный replace по id (не по name); сохраняйте каждый ключ output, который читают последующие шаги (незаполненный слот дает только steps.<id>.output.slot). Слоты не поддерживаются внутри шаблонов fan-out.step.

Ограничения оверлеев

Оверлеи действуют только на список шагов - не на метаданные (name, description, inputs, requires) и не на логику выражений. Нельзя: якориться на fan-out шаблоны и на шаги, добавленные другими оверлеями; добавлять инпуты или менять input-схему; отсутствующий в базе ID шага - ошибка валидации при резолве.

Каталоги

Каталоги определяют, где search и add ищут workflow (и step-каталоги для шагов):

bash
specify workflow catalog list
specify workflow catalog add <url>   # --name; повтор с тем же именем - no-op, с другим - отказ
specify workflow catalog remove <index>

Предупреждение документации: .specify/workflow-catalogs.yml проекта может направить команды на каталог, который вы не выбирали. В незнакомом проекте запускайте specify workflow catalog list (и specify workflow step catalog list); мейнтейнеры поля run не аудируют - читайте shell-шаги workflow сами перед запуском.

Порядок резолюции: SPECKIT_WORKFLOW_CATALOG_URL -> .specify/workflow-catalogs.yml -> ~/.specify/workflow-catalogs.yml -> встроенные (официальный + community).

Определение workflow

Встроенный Full SDD Cycle (speckit) - канонический пример:

yaml
schema_version: "1.0"
workflow:
  id: "speckit"
  name: "Full SDD Cycle"
  version: "1.0.1"
  author: "GitHub"
  description: "Runs specify → plan → tasks → implement with review gates"

requires:
  speckit_version: ">=0.8.5"
  integrations:
    any:
      - "alquimia"
      - "claude"
      - "copilot"
      - "gemini"
      - "opencode"

inputs:
  spec:
    type: string
    required: true
    prompt: "Describe what you want to build"
  integration:
    type: string
    default: "auto"
    prompt: "Integration to use (e.g. claude, copilot, gemini; 'auto' uses the project's initialized integration)"

steps:
  - id: specify
    command: speckit.specify
    integration: "{{ inputs.integration }}"
    input:
      args: "{{ inputs.spec }}"

  - id: review-spec
    type: gate
    message: "Review the generated spec before planning."
    options: [approve, reject]
    on_reject: abort

  - id: plan
    command: speckit.plan
    integration: "{{ inputs.integration }}"
    input:
      args: "{{ inputs.spec }}"

  - id: review-plan
    type: gate
    message: "Review the plan before generating tasks."
    options: [approve, reject]
    on_reject: abort

  - id: tasks
    command: speckit.tasks
    integration: "{{ inputs.integration }}"
    input:
      args: "{{ inputs.spec }}"

  - id: implement
    command: speckit.implement
    integration: "{{ inputs.integration }}"
    input:
      args: "{{ inputs.spec }}"

Поток исполнения:

flowchart TB A["specify (command)"] --> B{"review-spec (gate)"} B -- approve --> C["plan (command)"] B -- reject --> X1["Abort"] C --> D{"review-plan (gate)"} D -- approve --> E["tasks (command)"] D -- reject --> X2["Abort"] E --> F["implement (command)"]

Типы шагов

Тип Назначение
command вызвать команду Spec Kit (например, speckit.plan)
prompt отправить произвольный промпт AI-агенту
shell выполнить shell-команду и захватить вывод
init bootstrap проекта (как specify init)
slot именованный слот; пропускается, если не заполнен
gate пауза на человеческое одобрение
if условное ветвление (then/else)
switch мультиветочный dispatch по выражению
while цикл, пока условие истинно
do-while выполнить минимум раз, затем цикл по условию
fan-out dispatch шага на каждый элемент списка
fan-in агрегация результатов fan-out

Замечание безопасности: shell-шаг запускает локальную команду с вашими привилегиями. Песочницы нет - requires это консультативный предблок (версия spec-kit, интеграции), а не рантайм-гейт; requires.permissions отвергается валидацией именно потому, что подразумевал бы несуществующую песочницу. Ревьюте любые каталоговые и скачанные workflow до запуска; для чувствительных команд требуйте явное одобрение через gate.

Кастомные step-пакеты

Кастомные типы шагов ставятся через specify workflow step. Пакет - каталог с метаданными и исполняемым Python:

text
my-step/
├── step.yml        # обязательный, в корне
├── __init__.py     # обязательный, в корне
└── helpers.py      # опциональные вложенные модули и файлы данных

step.yml объявляет идентичность; step.type_key обязан точно совпадать с <step_id> из командной строки - ID из содержимого пакета не выводится:

yaml
step:
  type_key: my-step
  name: My Step
  version: 0.1.0
  author: you
  description: What this step does

__init__.py определяет подкласс StepBase с совпадающим type_key:

python
from specify_cli.workflows.base import StepBase, StepResult


class MyStep(StepBase):
    type_key = "my-step"

    def execute(self, config, context):
        return StepResult(output={"ok": True})

Установка:

bash
# локальный авторинг-цикл (без каталога и сети)
specify workflow step add my-step --dev ./my-step
specify workflow step list
specify workflow step info my-step
specify workflow step add my-step --dev ./my-step --force   # замена после правок
specify workflow step remove my-step

# из архива по URL: .zip/.tar.gz/.tgz (не голый step.yml)
specify workflow step add my-step --from https://example.com/my-step.zip

# из каталога
specify workflow step add my-step

--from показывает default-deny подтверждение доверия до любого сетевого запроса (пакет содержит исполняемый Python); HTTPS обязателен (HTTP - только loopback), редиректы - безопасные, загрузки ограничены по размеру. Discovery-only каталоги не устанавливают. --force стейджит и валидирует замену до касания существующей установки; сбой публикации после удаления старого каталога может оставить неполную установку - повторите с исходным источником и --force, автоската нет.

Валидация пакетов одинакова для всех источников: step.yml и __init__.py - обычные несимлинк-файлы в корне; дерево копируется рекурсивно (относительные импорты, вложенные модули, файлы данных), симлинки и нерегулярные объекты отвергаются; .git, __pycache__, .DS_Store пропускаются; лимиты - 512 записей, 32 уровня вложенности, 50 MiB; архивы - до 512 записей, 50 MiB суммарно, 10 MiB на член. Установка копирует, но не импортирует пакет - модули грузятся при старте workflow add/run/resume.

Реестр происхождения: записывается только вид источника (catalog/local/url); локальные пути и URL не персистятся (step info показывает источник). Ограничение бандлов: provides.steps резолвится только через активные step-каталоги; bundle-local steps/<id>/ и относительные source-оверрайды в этом релизе не резолвятся - офлайн такие шаги не ставятся (урок 16.15).

Конфигурация интеграции на шаг

Command-шаги могут передавать структурированную рантайм-конфигурацию:

yaml
- id: implement-with-docker-agent
  type: command
  command: speckit.implement
  integration: docker-agent
  integration_args:
    - "{{ inputs.agent_config }}"
  integration_options:
    agent: root
    safety: balanced
  model: "openai/gpt-5"
  input:
    args: "{{ inputs.spec }}"

integration_args - упорядоченный список строк; integration_options - мэппинг со строковыми ключами. Значения резолвятся механизмом выражений и валидируются выбранной интеграцией. Docker Agent принимает единственный позиционный аргумент как ссылку на конфигурацию агента и опции agent/safety; модель - через top-level model шага. Per-step ссылка агента перекрывает SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS для этого шага. Резолвированная конфигурация пишется в состояние прогона; resume с новыми --input перерезолвит значения, без - воспроизведет те же.

Выражения и shell-безопасность

Шаги ссылаются на инпуты и выходы предыдущих шагов через {{ expression }}:

Namespace Описание
inputs.spec значения инпутов workflow
steps.specify.output.file выход предыдущего шага
item текущий элемент итерации fan-out
context.run_id ID текущего прогона
context.workflow_dir абсолютный путь к каталогу источника workflow

Фильтры: default, join, contains, map, from_json.

yaml
condition: "{{ steps.test.output.exit_code == 0 }}"
args: "{{ inputs.spec }}"
message: "{{ status | default('pending') }}"

Интерполяция и безопасность shell

Выражения резолвятся плоской подстановкой строки - значение врезается в текст как есть, без кавычек и экранирования. Для shell-шагов это значит: интерполированное значение интерпретируется шеллом (/bin/sh -c на POSIX) как синтаксис, а не данные.

Если значение может содержать ;, |, &, $( ), бэктики или кавычки - оно способно изменить или расширить выполняемую команду. Главные источники риска: inputs.* (кто запускает workflow) и выход предыдущего шага - для prompt-шага это текст, сгенерированный AI-агентом, на который влияли файлы, тикеты и web-контент, прочитанные агентом. Выход агента - недоверенный, когда он попадает в shell-шаг.

Фильтра экранирования в языке выражений нет, песочницы вокруг shell нет - надежный контроль один: ограничить, чем значение может быть, и не допускать неконтролируемые значения в run вообще:

  • Ограничивайте источник через enum/allowlist - сильнейший контроль движка. Когда inputs.* питает run, зафиксируйте набор известных значений:

    yaml
    inputs:
      target:
        type: string
        enum: [staging, production]   # вызывающий не подставит произвольный shell-текст
    
  • Держите неконтролируемые значения вне run - ветвьтесь по ним через if/switch с фиксированными условиями или обрабатывайте в command/prompt-шаге, а не в shell-команде, собранной из них.

  • Кавычки - не граница безопасности: обрамление {{ inputs.x }} кавычками помогает шеллу считать доверенное значение одним аргументом, но значение, содержащее саму кавычку, все равно вырвется. Кавычьте ограниченные значения для корректности - никогда как защиту.

  • Gate не инспектирует следующий шаг, message печатается дословно: gate рендерит только собственный message/show_file и не показывает/не санирует команду после него; одобрение не нейтрализует инъекцию. Не интерполируйте сырые недоверенные данные в message (без стриппинга control-символов агентский вывод может инъектировать ANSI-эскейпы, меняющие промпт); недоверенный материал показывайте через show_file - его путь и контент зачищаются от control/ANSI.

Shell-шаг по дизайну - примитив произвольной команды; практики снижают экспозицию и держат под контролем автора, какая команда выполняется, но не устраняют риск интерполяции значений, которыми вы не владеете полностью.

Переменные окружения shell-шагов

Переменная Описание
SPECKIT_WORKFLOW_DIR абсолютный путь к каталогу источника workflow (равен {{ context.workflow_dir }}); не ставится, если источник без пути

Типы инпутов и коэрция

Тип Коэрция
string passthrough
number "42" -> 42, "3.14" -> 3.14
boolean "true" / "1" / "yes" -> True

Состояние и возобновление

Каждый прогон персистит состояние в .specify/workflows/runs/<run_id>/:

  • state.json - текущее состояние и прогресс шагов;
  • inputs.json - резолвированные инпуты;
  • log.jsonl - пошаговый журнал исполнения.

Это и дает resume продолжение с точного шага паузы (gate) или сбоя. Каждый запуск - уникальный ID и собственный каталог состояния; status показывает все прогоны.

Вердикты gate через инпуты

verdict_input привязывает вердикт gate к именованному инпуту (объявленному в inputs; необъявленная ссылка - ошибка workflow validate). Внутри fan-out не поддерживается: элементы делят инпуты, а состояние представляет одну паузу - ставьте gate до fan-out (одобрение партии) или после fan-in (ревью агрегата).

Семантика значения: непустая строка, совпавшая с опцией (case-insensitive) - gate решает автоматически; непустая без совпадения - немедленный сбой; не-строка - сбой; отсутствующее/пустое - промпт на TTY, иначе пауза.

yaml
inputs:
  spec_verdict:
    type: string
    default: ""
steps:
  - id: review-spec
    type: gate
    message: "Approve the specification?"
    options: [approve, reject]
    on_reject: retry
    verdict_input: spec_verdict

Возобновление с вердиктом:

bash
specify workflow resume <run_id> --input spec_verdict=approve

Нюанс on_reject: retry: привязанный reject потребляется до паузы - именованный инпут сбрасывается в "", и следующий resume снова промптит/паузит. Поэтому verdict-input с retry обязан допускать пустую строку; с enum включите "", иначе сброс нарушит собственный enum и прогон нельзя будет продолжить (workflow add репортит это ошибкой валидации).

Частые вопросы

  • Что происходит на gate? Прогон встает и ждет человека; после ревью - specify workflow resume <run_id>.
  • Можно гонять один workflow многократно? Да: у каждого запуска свой ID и каталог состояния.
  • Кто поддерживает workflows? Независимые авторы; мейнтейнеры не ревьюят, не аудируют и не поддерживают код workflow. Ревьюте исходник до установки.

Практика

  1. Запустите встроенный speckit-цикл с инпутом spec на маленькой фиче; на первом gate прервите, изучите state.json/log.jsonl в каталоге прогона, затем продолжите через resume.
  2. Напишите оверлей add-lint из примера урока и проверьте specify workflow resolve speckit - какой слой дал каждый шаг.
  3. Соберите минимальный step-пакет (step.yml + __init__.py с StepBase), установите через --dev и используйте в собственном workflow.

Краткое резюме

Workflows автоматизируют многошаговые процессы: YAML с типизированными инпутами, требованиями и шагами двенадцати типов (команды, промпты, shell, gate, условия, циклы, fan-out/fan-in, слоты). Состояние персистится по прогонам и позволяет resume с точного шага. Оверлеи правят список шагов поверх установленного workflow и переживают обновления; слоты - заявляемые автором точки расширения. Кастомные шаги - Python-пакеты с жесткими лимитами и записью вида источника.

Главное из урока:

  • интерполяция - плоская подстановка без экранирования: недоверенные значения (включая выход агента) не должны попадать в run и message;
  • ограничение enum на инпуте - сильнейший контроль против shell-инъекции; кавычки и gate от нее не защищают;
  • requires - консультативный предблок, не песочница; песочницы нет вовсе;
  • оверлеи не меняют метаданные и не якорятся на шаги других оверлеев и fan-out шаблоны;
  • verdict-input с on_reject: retry требует пустую строку в допустимых значениях.

Вопросы для самопроверки

  1. Опишите структуру YAML-определения workflow. Что проверяет блок requires и чего он не гарантирует?
  2. Что хранится в каталоге прогона и как это обеспечивает resume с точного шага?
  3. Перечислите операции оверлеев и правила приоритетов. Какие объекты не могут быть якорями?
  4. Что такое слот type: slot и как его заполняет оверлей? Какой output дает незаполненный слот?
  5. Почему интерполяция {{ steps.plan.output.stdout }} в run опасна? Какие три практики снижают риск?
  6. Как работает verdict_input? Почему связка с on_reject: retry требует пустую строку в enum?

Что дальше

Бандлы пакуют расширения, пресеты, workflows и шаги в один версионируемый юнит. Урок 16.15 - манифест bundle.yml, version pinning, проверка конфликтов, provenance, установка и удаление одним шагом.

Источники

Обновлено 03.10.2026