Перейти до змісту

Контракти

ЦільДізнатися, які перевірки дають довіряти запуску, за яким ти не стежив.

Агенту кажуть, код перевіряє

Контракт — це правило, про яке агенту розповіли і яке код усе одно перевіряє. Справа не в тому, що агент недбалий. Справа в тому, що ти не дивився, тож потім довіряти можна лише тому, що перевірила програма.

КонтрактНа яке питання відповідаєКоли падає
scopeЧи чіпав юніт лише те, що йому дозволено?жорстка зупинка, задачу запарковано
gateЧи проходить проєкт свої перевірки — в межах цієї зміни?жорстка зупинка, задачу запарковано
task fileЧи кожна роль писала лише у свої секції?жорстка зупинка, задачу запарковано
traceЧи надрукував юніт вердикт, якого вимагає його етап?жорстка зупинка, задачу запарковано
closingЧи кожен критерій закриття справді виконаний — на думку коду?задача не закривається
ліміт поверненьЧи не надто часто implementer і рев'юери не погоджувались?задачу запарковано для людини
один цикл на checkoutЧи не працює тут уже інший цикл?цикл відмовляється стартувати
постійні правилаЧи кожен файл агента містить актуальні спільні правила?runner виходить з кодом 2
розділення моделейЧи суддя на іншій моделі, ніж implementer і tester?runner виходить з кодом 2
гілкаПеред пушем: чи є гілка задачі, чи вона попереду базової, чи кожен коміт T-NNN:?задачу запарковано, нічого не запушено
планЧи кожна команда [closes] — справжня команда в цьому проєкті?план запарковано, коду ще немає
probeЧи піднято оточення?код 4, задачу не зачеплено

Жорстка зупинка завершує юніт з кодом 1 і зберігає докази. Вона спрацьовує, коли не пройшла перевірка scope, task file, trace чи gate, коли агент виходить з помилкою або коли юніт працює довше за limits.unitTimeoutMinutes (60 за замовчуванням). Runner переносить зміни юніта в іменований git stash, harness quarantine: T-NNN unit N, відновлює робоче дерево і паркує задачу з причиною. Наступний юніт стартує на чистому дереві, а ти можеш подивитися, що саме зробив агент. Ліміт використання — не жорстка зупинка: юніт завершується з кодом 4, а дерево повертається як було.

Scope

Planner пише scope у задачу: список шляхів, які implementer може міняти. Для T-002 у прикладі-проєкті, «Do not add the same URL twice», planner міг би написати таке (ілюстрація, а не вивід справжнього запуску):

scope:
  - src/lib/bookmarks.ts
  - src/lib/bookmarks.test.ts

Після implement-юніта runner перевіряє, що:

  • гілка досі гілка задачі, а її upstream не зрушив;
  • юніт зробив не більше комітів, ніж дозволено;
  • кожен закомічений файл і кожен щойно забруднений файл лежить у межах scope.

Юніти qa, judge і prototype лише читають. Писати їм можна тільки в .harness/artifacts/T-NNN/. Файл будь-де інде валить юніт.

За цим парканом стоїть другий: в allowlist інструментів агента (agent.allowedTools) немає git commit і push. Комітить лише runner.

Файл задачі, який ти змінив під час юніта, схваленням чи відповіддю, не вважається роботою юніта: harness веде журнал людських записів, а контракт scope пропускає файл, вміст якого з ним збігається. Правка агентом файлу іншої задачі, як і раніше, паркує юніт.

Gate, і чому важливе звуження

Гейт — команда, яка має пройти: gate: задачі плюс дефолт проєкту, зазвичай типи, лінт і тести.

gate: pnpm typecheck && pnpm test

У простого гейту є пастка. У проєкті вже є кілька попереджень лінта й один нестабільний тест. Юніт міняє хедер, а гейт падає на файлі, якого той ніколи не чіпав. І ось цикл паркує нормальну роботу знову і знову — за дефект, який не він спричинив.

Тому гейт звужено до зміни:

  • лінт відповідає лише за рядки, які змінив дифф;
  • тест, що впав, перезапускається на стартовому коміті юніта, в одноразовому worktree. Якщо він падає й там, дефект був і до того, і цей юніт не винен.

Task file

Файл задачі поділено на секції, і кожна роль володіє частиною з них. Runner розбирає файл до і після юніта й порівнює. Роль, яка писала поза своїми секціями або лишила файл, що не розбирається, спричиняє жорстку зупинку. Блоки ### Review у ## Plan і ## Prototype твої, тож юніт, який їх змінює, теж паркується. Хто якою секцією володіє — на сторінці ролей.

Trace

Runner пише stream-json вивід кожного юніта в trace/unit-NNNN.jsonl. Контракт трейсу читає вердикт із фінальної події result — останнього слова самого координатора. Вердикт спеціаліста, процитований усередині результату інструмента, не рахується. config.json перелічує вердикти, обов'язкові для кожного етапу: qa-юніту потрібен qa: PASS, qa: FAIL або qa: CANNOT_VERIFY, юніту судді — judge: CONFIRMED або judge: REJECTED.

Немає обов'язкового вердикту — немає прогресу. А задача закривається лише з CONFIRMED від судді в трейсі. Саме це ловить координатора, який завершив хід зарано.

Виконуване закриття

Те, що агент сказав «готово», нічого не доводить. Контракт закриття перетворює найважливіші критерії приймання на команди, які запускає код.

Кожен критерій має тег:

  • [closes] — shell-команда, записана після двокрапки. Runner запускає її; код виходу 0 означає, що критерій виконано. Лише ці критерії вирішують, чи закривається задача.
  • [found] — те, що перевіряють на запущеному застосунку, наприклад поведінку на сторінці. З браузером (проєкт задає app і застосунок стартував) qa проходить кожен критерій [found]: робить кроки, читає консоль і зберігає по скриншоту на критерій у теці артефактів. Критерій, що не виконується, — це FAIL зі скриншотом. Без браузера, а також для кроку, який браузер зробити не може (минає час, інший пристрій, лист, другий акаунт), qa перевіряє те, що можуть показати команда, тест чи код, а решту дослівно копіює у список For the reviewer у .harness/artifacts/T-NNN/qa-report.md. Цей список ніколи не привід валити чи паркувати задачу. Pull request позначає пройдений qa критерій ✓ і скриншотом, а решту показує невідміченою, для людини. Суддя оцінює критерії [found] за кодом.
  • [process] — те, що робить людина, наприклад дизайн-рев'ю.

qa-report.md починається з рядка на кожен критерій у сталій формі, бо з цих рядків збирається pull request: - <n> PASS|FAIL|REVIEWER · browser|command|code · <файл скриншота або ->, наприклад - 7 PASS · browser · qa-7-toast.png.

Продовжуючи ілюстрацію з T-002:

## Acceptance criteria

1. **[closes]** Adding the same URL twice is rejected: `node --test src/lib/bookmarks.test.ts`
2. **[found]** adding a URL that is already in the list keeps the old bookmark and tells the user
3. **[process]** a human reads the message wording

Цикл читає лише критерії з номером N. [tag]. Рядок із тегом під номером 1a. або у вигляді пункту списку не читається: контракт плану називає його, «not read as a criterion, number it N.: …», паркує план, а harness status показує це повідомлення.

Команда — це перший фрагмент у зворотних лапках після двокрапки, тож назви в лапках перед нею лишаються просто текстом. Критерій без двокрапки бере свій перший фрагмент, тож старі файли задач розбираються як раніше. Ще до коду контракт плану перевіряє кожну команду: її перше слово, після префіксів ! і VAR=, має проходити command -v у проєкті. Назва чи шлях на цьому місці паркують план з «criterion N: word is not a command here», тож одрук коштує одне коло плану, а не скасований правильний вердикт судді.

Тег працює і з жирним, і без нього; канонічна форма — **[closes]**. Цілься в один [closes] на задачу. Перший урок пояснює, чому більше — гірше.

Ліміт повернень

qa FAIL і judge REJECTED ділять лічильник bounces у frontmatter задачі. Відмова, яка доводить його до limits.bounces, 2 за замовчуванням, паркує задачу з причиною, а не повертає її implementer. За замовчуванням це максимум два implement-кола і два коміти. qa: CANNOT_VERIFY теж паркує задачу, але поверненням не рахується: він означає, що команда [closes] або гейт не змогли запуститися, наприклад бракує інструмента. Reopen скидає bounces до 0.

Implement-коло, яке нічого не змінило, іде в qa без коміту, якщо гілка задачі вже містить її коміти; у лозі з'являється «no change this round». Implementer має нічого не міняти, коли причина відмови не в коді, тож так виправляється хибна відмова або зламана перевірка. Коло без жодної роботи на гілці, як і раніше, паркується з «implement: no change inside scope».

Один цикл на checkout

Контракт scope диффає все робоче дерево, тож два цикли в одному checkout звинувачували б один одного в чужих змінах. Supervisor тримає .harness/trace/supervisor.lock, і другий цикл відмовляється стартувати.

Постійні правила

Деякі правила потрібні кожному агенту, хай яка в нього роль. Вони живуть в одному файлі, harness/prompts/STANDING-RULES.md, і harness agents --sync копіює їх між маркерами в кожного базового агента. Агенту проєкту без цього блоку його додають. Перед юнітом runner порівнює кожну копію з джерелом. Копія, що розійшлася, змушує його вийти з кодом 2 — зламана конфігурація, — тож жоден агент не працює за застарілими правилами.

Правило 11 каже: одна проста команда на один виклик Bash. З --permission-mode dontAsk Claude Code відмовляє в $?, $(…), змінних shell і mkdir, хай що каже allowlist, тож агент записує докази інструментом Write. Теку артефактів задачі runner створює ще до старту юніта. Allowlist зіставляє команду за її початком, тож правило ще каже: запускай git з теки, де стартуєш (git diff main...HEAD), ніколи не git -C <path>, а файли змінюй інструментами Write і Edit, ніколи не tee, echo > чи cat >. Це знайшов справжній прогін: qa і суддя обидва просили git -C <path> diff і отримали відмову.

Правило 7 каже тримати хід, доки не відзвітує кожен спеціаліст. Щоб чекати, агент запускає sleep 20 окремим викликом Bash і потім читає звіт, ніколи не цикл shell (until, while); sleep для цього є в стандартному allowlist.

Розділення моделей

Суддя мусить працювати на іншій моделі, ніж implementer і tester, — з причини, яку пояснює сторінка ролей. Runner перевіряє налаштовані моделі перед юнітом і виходить з кодом 2, якщо вони збігаються.

Гілка

Лише з увімкненим deliver.pr. Перед будь-яким пушем runner перевіряє, що гілка задачі існує, що вона попереду базової гілки і що тема кожного коміту в ній починається з T-NNN: — ознака, що коміт зробив harness. Якщо перевірка падає, задачу паркують із parked: deliver: branch contract: …, і нічого не пушиться. Увесь крок доставки описано на сторінці Життя задачі.

Probe оточення

Проєкт може задати команду probe: у config.json. Якщо оточення лежить, юніт завершується з кодом 4 ще до старту агента, і задача лишається як була. Без probe агент витратив би юніт на те, щоб з'ясувати, що база даних не запущена.

ДаліДашборд→
Гайд10 розділів · ~55 хвилин
  1. 1Поняття
  2. 2Ролі
  3. 3Координація
  4. 4Життя задачі
  5. 5Керування циклом
  6. 6Контрактити тут
  7. 7Дашборд
  8. 8Швидкий старт
  9. 9Під свій проєкт
  10. 10Уроки
ОглядДемоПрикладGitHub