Агенту кажуть, код перевіряє
Контракт — це правило, про яке агенту розповіли і яке код усе одно перевіряє. Справа не в тому, що агент недбалий. Справа в тому, що ти не дивився, тож потім довіряти можна лише тому, що перевірила програма.
| Контракт | На яке питання відповідає | Коли падає |
|---|---|---|
| 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 агент витратив би юніт на те, щоб з'ясувати, що база даних не запущена.