Живой снимок проекта: карточки, PR, проверки и вердикты читаются из GitHub, руками не правятся.
Репозиторий с правилами, из которого можно вести разработку
make check
limits: ok / forbidden: ok
Issue #1 PR нет
вердиктов нет
M0 Каркас
Клиент GitHub REST только на чтение: карточки, PR с head sha, статус CI, комментарии PASS/FAIL. Токен из GITHUB_TOKEN.
go test ./internal/github/... -run TestClient -v
PASS ok
delovdelo · 33ff3f9 · FAIL · 06.09.2026 16:26 delovdelo · 33ff3f9 · PASS · 06.09.2026 16:27
M1 Доска читает GitHub
Функция Derive(issue, pr, ci, verdicts) → State по машине переходов; никаких ручных статусов
go test ./internal/domain/... -run TestDerive -v
PASS ok
delovdelo · 7b1e508 · FAIL · 06.09.2026 16:25 delovdelo · 7b1e508 · PASS · 06.09.2026 16:28
M1 Доска читает GitHub
GET /board — html/template + owner.css: дорожная карта по вехам (план/факт), четыре сводные карточки, таблица карточек с раскрытием; GET /board.json тот же вид
go test ./internal/httpapi/... -run TestBoardRendersOwnerLayout -v
PASS ok
delovdelo · d184be9 · PASS · 06.09.2026 16:41
M1 Доска читает GitHub
agentstaff serve поднимает /board и /healthz на порту из PORT; agentstaff status печатает доску текстом
go test ./cmd/agentstaff/... ./internal/app/... -run 'TestConfigFromEnv|TestLoader|TestNewCached' -v
PASS ok
delovdelo · 07cc660 · FAIL · 06.09.2026 16:47 delovdelo · 07cc660 · PASS · 06.09.2026 16:49
M1 Доска читает GitHub
Один пул pgx, миграции с версией в БД, DDL только в migrations/
AGENTSTAFF_TEST_DATABASE_URL='postgres://agentstaff@127.0.0.1:55440/agentstaff_test?sslmode=disable' go test ./internal/store/... -run TestStore -v
PASS ok
delovdelo · 4dd6345 · PASS · 06.09.2026 17:03
M2 Очередь и аренда
Маршруты аренды по contracts/http-api.md. httpapi зависит только от app (ADR-0002): app даёт фасад Leaser с собственными типами и ошибками поверх queue
go test ./internal/httpapi/... -run TestLeaseRoutes -v
PASS ok
delovdelo · a3cceeb · FAIL · 06.09.2026 17:50 delovdelo · a3cceeb · PASS · 06.09.2026 17:53
M2 Очередь и аренда
POST /webhooks/github проверяет X-Hub-Signature-256, превращает pull_request/check_suite/issue_comment в события
go test ./internal/github/... -run TestWebhook -v
PASS ok
delovdelo · 7cb5c29 · PASS · 06.09.2026 18:07
M3 События и вебхуки
Правила вердикта живут в одном пакете acceptance (разбор PASS/FAIL/BLOCKED, законный приёмщик, совпадение sha, счёт кругов, порог блокировки) и используются и domain.Derive (доска), и pipeline (переходы); дубли shaMatches/isAcceptor удалены
go test ./internal/acceptance/... -v
PASS ok
delovdelo · f5eae62 · PASS · 06.09.2026 19:01
M4 Приёмка другим вендором
owner.decision_needed → комментарий с упоминанием владельца в Issue; не чаще одного на карточку
go test ./internal/app/... -run TestOwnerSignal -v
PASS ok
delovdelo · a53237f · PASS · 06.09.2026 18:55
M4 Приёмка другим вендором
Стоп-критерий версии 0.1 из docs/ЗОЛОТОЙ-ПУТЬ.md: три реальные карточки с исполнителем Opus 5 и приёмщиком Codex
git tag --list v0.1.0
v0.1.0
Issue #14 PR нет
вердиктов нет
M5 Золотой путь пройден
Продукт ведёт несколько арендаторов одновременно: список арендаторов в конфиге (AGENTSTAFF_TENANTS="agentstaff=infaton/AgentStaff,second=<owner>/<repo>" — пути репозиториев допустимы как адреса); для каждого — свой Loader/Board, свой Poller и Runtime (общая база, tenant_id разделяет строки — таблицы уже так устроены); доска и status принимают --tenant/параметр пути /tenants/{t}/board; второй арендатор в 0.1 — только чтение (без аренды и без записи комментариев). Один арендатор без списка — прежнее поведение
go test ./cmd/agentstaff/... ./internal/app/... ./internal/httpapi/... -run 'TestConfigFromEnv|TestTenants|TestBoardRendersOwnerLayout' -v
PASS ok
delovdelo · 579a2e0 · PASS · 07.09.2026 03:42
M5 Золотой путь пройден
GitHub отдаёт due_on как полночь в зоне репозитория, сдвинутую в UTC; на доске должна быть календарная дата вехи из GitHub (06.09, не 05.09). Показывать дату по правилу GitHub: округлять due_on к ближайшей календарной дате в UTC+12/по дате, а не по времени
go test ./internal/app/... -run TestMilestoneDue -v
PASS ok
delovdelo · f0d0a96 · PASS · 06.09.2026 16:57
M1 Доска читает GitHub
Верхняя панель компактная (логотип, репозиторий, штамп), заголовок «Что запланировано и что доказано» и подзаголовок — в блоке .hero под ней, как в эталоне владельца
go test ./internal/httpapi/... -run TestBoardRendersOwnerLayout -v
PASS ok
delovdelo · 265dc8c · PASS · 06.09.2026 16:58
M1 Доска читает GitHub
Если есть карточки в состоянии blocked — над таблицей блок .focus-panel как в эталоне: номер и название первой заблокированной, факт, следующий шаг «нужно решение владельца»; если нет — блок не рисуется
go test ./internal/httpapi/... -run TestBoardRendersOwnerLayout -v
PASS ok
delovdelo · 93ec7fd · PASS · 06.09.2026 17:07
M1 Доска читает GitHub
В job check поднимается сервис postgres:17 и задаётся AGENTSTAFF_TEST_DATABASE_URL; тест TestStore в CI выполняется, а не skip. Исполнитель готовит изменение workflow как предложение в PR; сливает архитектор (защищённый путь)
gh run view --job $(gh run list --branch main --limit 1 --json databaseId --jq '.[0].databaseId') --log 2>/dev/null | grep -c 'SKIP.*TestStore'
0
вердиктов нет
M2 Очередь и аренда
agentstaff version печатает три строки: версия продукта, версия Go из runtime, короткий commit из debug.ReadBuildInfo (vcs.revision, первые 7 символов, или «unknown»). Тест TestVersionOutput проверяет формат трёх строк
go test ./cmd/agentstaff/... -run TestVersionOutput -v
PASS ok
delovdelo · fbe2c5b · PASS · 06.09.2026 17:57
M2 Очередь и аренда
LinkPull учитывает, кроме ветки feat/N-*, строку Closes/Fixes/Resolves #N в теле PR (поле Body у domain.Pull; app передаёт body из github.Pull). Приоритет: ветка, затем Closes. Нужно для исполнителей с чужим именованием веток (Copilot: copilot/*)
go test ./internal/domain/... -run TestDeriveLinkPull -v
PASS ok
delovdelo · 722c759 · FAIL · 06.09.2026 18:03 delovdelo · 722c759 · PASS · 06.09.2026 18:04
M2 Очередь и аренда
ParseVerdict понимает BLOCKED <sha>: причина; такой вердикт не двигает состояние, попадает в Verdicts с Result=blocked и виден на доске в факте карточки как «приёмка не смогла проверить: причина»; Derive порождает признак OwnerDecisionNeeded=true
go test ./internal/domain/... -run 'TestDerive|TestParseVerdict' -v
PASS ok
delovdelo · 9e05804 · PASS · 06.09.2026 18:18
M4 Приёмка другим вендором
Маршрут принимает вебхук: проверка подписи и приём тела передаются функциями через Options (httpapi не импортирует github); ответы 202/401/400/503; cmd подключает github.VerifySignature с секретом AGENTSTAFF_WEBHOOK_SECRET и пока логирует событие
go test ./internal/httpapi/... -run TestWebhookRoute -v
PASS ok
delovdelo · f932f9b · PASS · 06.09.2026 18:20
M3 События и вебхуки
Инструмент лимитов чинится (ветка fix/limits-root), после чего TestDeriveVerdictsFiltered (89 строк) делится на две функции, а events_test.go (501 строка) делится на два файла; поведение тестов не меняется
go test ./tools/limits/... -run TestLimits -v && go run ./tools/limits
PASS ok limits: ok
delovdelo · ed85292 · PASS · 06.09.2026 18:28
M3 События и вебхуки
WebhookEvent получает поля PRAuthor (pull_request.user.login; для issue_comment — issue.user.login, если это PR) и Body (issue.body для issues, pull_request.body для pull_request); ParseWebhook их заполняет; тесты TestWebhook* проверяют оба поля на фикстурах. Нужно пайплайну (#11) для отсева самоприёмки и разбора блока карточки
go test ./internal/github/... -run TestWebhook -v
PASS ok
delovdelo · bfb4a70 · PASS · 06.09.2026 18:35
M3 События и вебхуки
При заданных AGENTSTAFF_DATABASE_URL и AGENTSTAFF_WEBHOOK_SECRET serve: WebhookSink разбирает событие через github.ParseWebhook и вызывает pipeline.Apply с арендатором из конфига; результат (From→To или Ignored) пишется в slog; фоновая горутина раз в 5 с вызывает events.Publisher.Deliver с sink=slog.Info и останавливается по контексту. Без БД — как раньше, только лог. Разбор конфига и сборка зависимостей вынесены в app.Runtime (New(cfg) → Board, Leaser, WebhookSink, Deliver) с тестом на фейках
go test ./internal/app/... -run TestRuntime -v
PASS ok
delovdelo · 7eceafd · PASS · 06.09.2026 18:42
M3 События и вебхуки
При pull_request opened/synchronize пайплайн сохраняет row.PRAuthor = wh.PRAuthor; при issues opened/edited разбирает блок карточки из wh.Body (не из CommentBody); при issue_comment самоприёмка отсеивается по row.PRAuthor, а если он пуст — по wh.PRAuthor из вебхука
go test ./internal/pipeline/... -run TestPipeline -v
PASS ok
delovdelo · f65b942 · PASS · 06.09.2026 18:45
M3 События и вебхуки
Фоновый опрос GitHub раз в N секунд (AGENTSTAFF_POLL_SECONDS, 0 = выключено) читает PR, проверки и комментарии карточек через github.Client и синтезирует github.WebhookEvent (pull_request opened/synchronize/closed, check_suite completed, issue_comment created) с детерминированным DeliveryID (poll:<вид>:<pr>:<sha>|<comment id>), передавая их в pipeline через Runtime.WebhookSink; повторы отсеивает существующая идемпотентность по delivery. Нужно, чтобы золотой путь проходил на локальном сервере без туннеля
go test ./internal/app/... -run TestPoller -v
PASS ok
delovdelo · 010542a · PASS · 06.09.2026 19:08
M5 Золотой путь пройден
Poller.Once перед обработкой PR читает Issues с меткой card (открытые и закрытые) и синтезирует issues opened с DeliveryID poll:issue:<n>:opened (тело — issue.body, чтобы пайплайн разобрал блок карточки); для закрытого Issue без PR — issues closed (DeliveryID poll:issue:<n>:closed). Порядок: сначала все issues, потом PR. Замер 07.09: на чистой базе все события PR игнорировались с card.unknown
go test ./internal/app/... -run TestPoller -v
PASS ok
delovdelo · a397be7 · PASS · 06.09.2026 19:22
M5 Золотой путь пройден
applyPull определяет карточку сначала по ветке feat/N-*|fix/N-*, а если не вышло — по Closes/Fixes/Resolves #N в теле PR (wh.Body), переиспользуя правило из domain (вынести bodyRefersTo в экспортируемую функцию domain.PullRefersTo или использовать LinkPull с одним PR). Замер 07.09: ветка copilot/* давала branch.no-card
go test ./internal/pipeline/... -run TestPipeline -v
PASS ok
delovdelo · 50c96fe · PASS · 06.09.2026 19:19
M5 Золотой путь пройден
NewRuntime (или serve) вызывает store.UpsertTenant(tenant, repo, acceptors) до первого события — иначе SaveCard падает по внешнему ключу (замер 07.09: SQLSTATE 23503). Poller.Once при ошибке sink на одном событии логирует её и продолжает остальные, возвращая в конце сводную ошибку (errors.Join)
go test ./internal/app/... -run 'TestRuntime|TestPoller' -v
PASS ok
delovdelo · 401748f · PASS · 06.09.2026 19:41
M5 Золотой путь пройден
MarkDelivery и SaveCard/публикация выполняются так, что при ошибке сохранения доставка НЕ считается применённой (транзакция в store: ApplyDelivery(ctx, tenant, delivery, fn) или порядок «сохранить → пометить»); повтор того же delivery после ошибки применяется заново. Замер 07.09: три доставки помечены, ни одной карточки не сохранено
AGENTSTAFF_TEST_DATABASE_URL='postgres://agentstaff@127.0.0.1:55440/agentstaff_test?sslmode=disable' go test ./internal/pipeline/... ./internal/store/... -run 'TestPipeline|TestStoreCards' -v
PASS ok
delovdelo · eef062f · PASS · 06.09.2026 19:48
M5 Золотой путь пройден
Next(StateNew, EventPROpened) → StateInProgress (аренда желательна, не обязательна — контракт обновлён). Derive уже так считает; машина переходов отставала. Замер 07.09: 29 PR отброшены как transition new --pr_opened--> not allowed
go test ./internal/domain/... -run 'TestGoldenPath|TestNoManual|TestDerive' -v
PASS ok
delovdelo · 3e4f2a4 · PASS · 06.09.2026 20:00
M5 Золотой путь пройден
ApplyDelivery помечает доставку применённой только если событие дало переход либо отброшено по структурной причине (branch.no-card, not-a-verdict, action.*); причины «transition … not allowed», check.no_card, comment.no_card, card.unknown оставляют доставку непомеченной, чтобы следующий опрос применил её, когда карточка или состояние догонят. Замер 07.09: после починки машины состояний 29 PR остались бы навсегда «duplicate»
AGENTSTAFF_TEST_DATABASE_URL='postgres://agentstaff@127.0.0.1:55440/agentstaff_test?sslmode=disable' go test ./internal/pipeline/... ./internal/store/... -run 'TestPipeline|TestStoreCards' -v
PASS ok
delovdelo · 4403459 · PASS · 06.09.2026 20:08
M5 Золотой путь пройден
Опрос не тратит лимит GitHub (5000 запросов/час): кэш по updated_at и head sha — check-runs и комментарии запрашиваются только для PR, которые изменились с прошлого круга; закрытые и слитые PR старше N дней не перечитываются; ListPulls с per_page=100 и sort=updated; ETag/If-None-Match для issues/pulls там, где GitHub его отдаёт; в логе одной строкой на круг — число запросов. Замер 07.09: опрос раз в 30 с исчерпал 5000 запросов за ~40 минут (30 PR × 3 запроса × 120 кругов)
go test ./internal/app/... -run TestPoller -v
PASS ok
delovdelo · 833abfa · PASS · 06.09.2026 20:59
M5 Золотой путь пройден
После FAIL приёмка может быть перезапущена на том же sha (карточка исправлена, код нет) — PASS в состоянии rework при зелёном CI переводит в accepted; merged в состоянии rework/ready_for_acceptance после PASS — в done. Замер 07.09 на живом прогоне: 5 карточек застряли в rework («transition rework --verdict_pass--> not allowed», «transition rework --merged--> not allowed»), хотя доска (Derive) считает их готовыми. Дополнительно: слияние — факт; merged из in_progress/ready_for_acceptance/rework тоже переводит в done, но пайплайн публикует событие card.done с data.without_acceptance=true, если PASS не было (замер: карточка #35 застряла в ready_for_acceptance после слияния PR архитектора без вердикта)
go test ./internal/domain/... ./internal/pipeline/... -run 'TestGoldenPath|TestSecondFail|TestNoManual|TestPipeline' -v
PASS ok
delovdelo · 6948510 · PASS · 06.09.2026 20:41
M5 Золотой путь пройден
Закрытие Issue без PR переводит карточку в done (контракт: «Issue закрыт руками без PR → done, допустимо только для карточек без процесса»). Замер 07.09: карточка #1 осталась new, событие issues closed отброшено как action.closed
go test ./internal/pipeline/... -run TestPipeline -v
PASS ok
delovdelo · ba631ba · PASS · 06.09.2026 20:55
M5 Золотой путь пройден
Пакет internal/app достиг лимита 3000 строк. Разделить без изменения поведения: internal/app (Board, Source, Count, Loader, Cached, Leaser — сборка доски и фасады), internal/poll (Poller, состояние круга, синтез вебхуков), internal/runtime (Runtime, Deps, OwnerSignal). Зависимости по ADR-0002: poll → github, domain; runtime → pipeline, events, store, github, app; httpapi по-прежнему только app. Тесты переезжают вместе с кодом, ни один не удаляется
go run ./tools/limits && go test ./internal/app/... ./internal/poll/... ./internal/runtime/... -count=1
limits: ok ok
delovdelo · 48e7a60 · FAIL · 07.09.2026 03:21 delovdelo · 48e7a60 · PASS · 07.09.2026 03:24
M5 Золотой путь пройден
Опрос отдаёт события в порядке: issues opened → все события PR (opened, check_suite, issue_comment, closed) → issues closed последними. Иначе закрытая карточка уходит в done по закрытию Issue раньше, чем пайплайн узнаёт о её PR, и путь PR → CI → вердикт → слияние не фиксируется (замер 07.09: 35 done, ни одного события ready_for_acceptance/verdict.pass, 102 «transition done --pr_opened»)
go test ./internal/app/... -run TestPoller -v
PASS ok
delovdelo · ca0a796 · PASS · 06.09.2026 21:31
M5 Золотой путь пройден
Продукт ходит в GitHub от имени установленного приложения (GitHub App), а не по личному токену владельца: конфиг `AGENTSTAFF_GITHUB_APP_ID`, `AGENTSTAFF_GITHUB_APP_KEY_FILE` (PEM), `AGENTSTAFF_GITHUB_INSTALLATION_ID`; клиент сам получает токен установки по JWT и обновляет его до истечения (запас 5 минут); при заданном `AGENTSTAFF_GITHUB_TOKEN` и незаданном приложении — прежнее поведение. Смысл: у приложения свой лимит запросов, отдельный от лимита учётки владельца (замер 07.09: опрос каждые 30 с сжёг 5000/час за 40 минут), и в проде не лежит личный токен
go test ./internal/github/... ./cmd/agentstaff/... -run 'TestAppToken|TestConfigFromEnv' -v
PASS ok
delovdelo · 4491b69 · PASS · 07.09.2026 04:44
M6 Продукт в бою
При нескольких арендаторах строки журнала опроса (`круг завершён requests=… emitted=… skipped=…`, «webhook ignored», ошибки запросов) не различить: в них нет имени арендатора и репозитория (замер 07.09 на трёх арендаторах). Логгер, который получает Poller, должен нести атрибуты `tenant` и `repo` во всех своих строках; Runtime — атрибут `tenant`. Сборка в `cmd` передаёт `slog.Default().With("tenant", …, "repo", …)`
go test ./internal/poll/... -run 'TestPollerLogsCarryTenant' -v
PASS ok
delovdelo · d6ddb50 · PASS · 07.09.2026 04:59
M6 Продукт в бою
Прогон арендатора infaton 07.09 (карточка #22 → PR #27): после пуша второго коммита опрос каждый круг присылал `pull_request/opened` с новым sha, конвейер отвечал «transition in_progress --pr_opened--> not allowed», head карточки застрял на старом sha, настоящий PASS на новом sha был отвергнут как verdict.stale, а после слияния продукт ложно просигналил «слито без приёмки». Плюс тот же отвергнутый opened повторялся каждый круг для уже готовой карточки (retryable). Нужно: (1) опрос помнит head PR и при его смене эмитит `poll:pr:N:synchronize:<sha>` с action=synchronize (конвейер уже умеет updateHead); при первом наблюдении PR после старта, когда карточка уже с этим PR, — тоже synchronize, если sha отличается от head карточки — то есть (2) конвейер трактует `opened` для карточки, у которой уже есть этот PR, как обновление head при другом sha и как не повторяемый дубль (не retryable) при том же sha или в состоянии done
go test ./internal/poll/... ./internal/pipeline/... -run 'TestPollerEmitsSynchronize|TestPipelineOpenedAgain' -v
PASS ok
delovdelo · f3f5982 · PASS · 07.09.2026 07:54
M6 Продукт в бою
В режиме опроса факты о предыдущем head теряются, если пуш и слияние уложились между двумя кругами (прогон infaton 07.09, PR #27: CI зелёный на f6b2ce26fb, FAIL f6b2ce26fb, пуш a346e447f4, CI зелёный, PASS a346e447f4, слияние — всё за 7 минут; продукт увидел только opened и merged, путь FAIL→доработка→PASS не записан, fail_count=0). Факты остаются доступны в GitHub после события: нужно при смене head внутри круга сначала эмитить CI старого sha (`poll:ci:N:<old>:<conclusion>`) и вердикты (уже эмитятся все), затем synchronize нового sha, затем CI нового sha, затем закрытие. Порядок внутри круга: opened → ci(old) → comments(old) → synchronize(new) → ci(new) → comments(new) → closed. Стоимость — один запрос check-runs на смену head
go test ./internal/poll/... -run 'TestPollerReplaysSupersededHead' -v
PASS ok
delovdelo · 42471a2 · PASS · 07.09.2026 08:13
M6 Продукт в бою
Доска (internal/app Loader) отбирает карточки по метке `card`, а конвейер (internal/pipeline через domain.ParseCard) — по блоку «## Карточка» в теле Issue; контракт tenant.md называет только блок. Опрос (internal/poll) тоже отбирает Issue по метке: четыре карточки вехи M6 без метки (#95, #100, #102, #103) ни доска не показывала, ни опрос не вёл — конвейер узнал о них только после проставления метки (замер 07.09 09:18Z) (замер 07.09 на проде и на ноутбуке: 39 карточек вместо 43). Нужно: Loader считает карточкой Issue, у которого ParseCard тела проходит без ошибки; метка не требуется и не мешает; Issue без блока не карточка, даже с меткой `card`. Поле CardLabel удалить из конфигурации app вместе с переменной, если она есть
go test ./internal/app/... ./internal/poll/... -run 'TestLoaderCardByBlock|TestPollerCardByBlock' -v
PASS ok
delovdelo · 32a9d88 · PASS · 07.09.2026 09:43
M6 Продукт в бою
На проде 07.09 GitHub прислал события `ping` и `installation` (владелец сохранял права приложения), продукт ответил 500 «вебхук не обработан» (`github: unsupported webhook event: installation`), GitHub пометил доставку как сбой. Событие, которое продукт не разбирает, — не ошибка сервера: ответить 202, записать в журнал одной строкой уровня INFO «webhook ignored event=<тип> reason=event.unsupported» и ничего не применять. Подпись при этом проверяется как обычно (неверная подпись — по-прежнему отказ). Ошибка 500 остаётся только для настоящих сбоев (база недоступна и т.п.)
go test ./internal/httpapi/... ./internal/runtime/... -run 'TestWebhookUnsupportedEvent' -v
PASS ok
delovdelo · 8921529 · PASS · 07.09.2026 10:04
M6 Продукт в бою
Прод на pristock (ssh-имя `agentstaff-vps`, служба systemd `agentstaff`, бинарник `/opt/agentstaff/bin/agentstaff`) сегодня обновляется руками: сборка на ноутбуке, scp, mv, restart. Нужен `scripts/deploy.sh <vX.Y.Z> [--dry-run]`: проверяет форму тэга (иначе «тэг должен быть вида vX.Y.Z», код 2), что тэг есть в origin, собирает бинарник linux/amd64 из этого тэга в отдельном worktree (`CGO_ENABLED=0`, `-ldflags "-s -w"`), копирует на хост (`AGENTSTAFF_DEPLOY_HOST`, по умолчанию `agentstaff-vps`) как `agentstaff.new`, на хосте сохраняет прежний как `agentstaff.prev`, ставит новый, `systemctl restart agentstaff`, ждёт до 30 с `curl 127.0.0.1:8080/healthz` с версией X.Y.Z; если версия не совпала или служба не поднялась — откат на `agentstaff.prev` и код 1. `--dry-run` печатает план построчно (`план: …`) и не трогает сеть и хост. Каждая строка вывода начинается с `deploy:`
bash -n scripts/deploy.sh && scripts/deploy.sh --dry-run v0.1.0 && ! scripts/deploy.sh --dry-run notatag
deploy: план: сборка v0.1.0 linux/amd64 deploy: план: проверка версии 0.1.0 через /healthz, при несовпадении откат deploy: тэг должен быть вида vX.Y.Z
delovdelo · 9c0bf39 · FAIL · 07.09.2026 10:10 delovdelo · 9c0bf39 · PASS · 07.09.2026 10:13
M6 Продукт в бою
На проде 07.09 после перезапуска службы первый запрос `/board` и `/board.json` строил доску с нуля (130 запросов к GitHub, ~1–6 минут), nginx обрывал ожидание через 60 с, владелец видел 502, в журнале «context canceled». Нужно: (1) при старте `serve` продукт прогревает доску каждого арендатора в фоне, не блокируя HTTP; (2) `Cached` отдаёт последний успешно собранный снимок мгновенно, а по истечении TTL обновляет его в фоне (stale-while-revalidate), не заставляя запрос ждать; (3) пока снимка ещё нет, `/board` отвечает 200 со страницей «доска собирается, обновите через минуту» и заголовком `Retry-After: 30`, `/board.json` — 503 с тем же Retry-After; (4) ошибка обновления не портит имеющийся снимок — в журнал одна строка, снимок остаётся прежним
go test ./internal/app/... ./internal/httpapi/... -run 'TestCachedServesStaleWhileRefreshing|TestBoardWithoutSnapshot' -v
PASS ok
delovdelo · 40942fd · PASS · 07.09.2026 10:40
M6 Продукт в бою
Приложение владельца («Двойник») должно рождать факт, а не рисовать статус: `POST /tenants/{t}/cards` с Bearer-токеном арендатора и JSON `{title, goal, package, acceptance: {command, expected: [строки]}, mutation: {file, what, sed, expect}, stop?}` создаёт в репозитории арендатора GitHub Issue с блоком «## Карточка» ровно в формате STANDARD.md §3.2 (Цель, Пакет, Команда приёмки с отступом 4 пробела, Ожидаемый вывод построчно, Подлог с файл:/что:/sed:/ждём:, Стоп-условия) и отвечает `201 {"number": N, "url": …}`. Проверка: собранное тело проходит `domain.ParseCard` до отправки, иначе 422 с причиной. Токены арендаторов: `AGENTSTAFF_TENANT_TOKENS="имя=токен,…"` (арендатор по умолчанию продолжает принимать `AGENTSTAFF_TENANT_TOKEN`); без токена — 401; арендатор, для которого токена нет, — 403. Запись в GitHub — через существующий клиент (приложение имеет право Issues: write). Дубли: повторный запрос с тем же заголовком `Idempotency-Key` в течение часа возвращает тот же номер, а не второй Issue
go test ./internal/domain/... ./internal/httpapi/... -run 'TestRenderCardRoundTrip|TestCreateCard' -v
PASS ok
delovdelo · 2f818b6 · PASS · 07.09.2026 10:54
M7 Движок для приложений владельца
Приложение владельца должно узнавать «этап принят / провален / нужно решение» не опросом доски, а событием. Конфиг `AGENTSTAFF_TENANT_SINKS="имя=https://адрес,…"`: для арендатора с адресом каждое событие его outbox (конверт Event Contract v1, как в contracts/event-contract-v1.md) отправляется `POST` JSON на адрес с заголовками `X-AgentStaff-Event: <type>`, `X-AgentStaff-Delivery: <id конверта>`, `X-AgentStaff-Signature-256: sha256=<HMAC тела токеном арендатора>`; ответ 2xx — доставлено (delivered_at), иначе повтор с нарастающей паузой (1, 2, 4… до 5 минут) без блокировки других событий и арендаторов; после 24 часов неудач — одна строка ERROR в журнал и событие остаётся недоставленным (не теряется). Арендаторы без адреса — как сейчас (журнал). Существующий сигнал владельцу (комментарий в GitHub) не отключается
go test ./internal/events/... ./internal/runtime/... -run 'TestSignEnvelope|TestSinkDelivers' -v
PASS ok
delovdelo · 28fef21 · PASS · 07.09.2026 10:48
M7 Движок для приложений владельца
Сегодня движок признаёт факт «CI зелёный» только по check runs (GET …/commits/{sha}/check-runs и события check_suite/check_run). Арендаторы с проверками вне GitHub Actions (прогон в закрытом контуре, внешний CI) могут выставить только commit status (POST /repos/{r}/statuses/{sha}, событие `status`). Пилот infaton-1c 07.09: приёмка в тестовой базе 1С идёт с узла тайлнета, GitHub-runner до базы не достаёт. Нужно: опрос дополнительно читает combined status head PR (GET …/commits/{sha}/status) и эмитит `poll:ci:N:<sha>:<state>` по правилу: success — зелёный; failure/error — красный; pending — ничего; при наличии и check runs, и статусов — зелёный только если оба не красные и хотя бы один зелёный. Вебхук `status` разбирается как check_suite (нужна подписка на событие status в приложении — отметит владелец). Стоимость: +1 запрос на PR со сменой head, кэш как у check runs
go test ./internal/github/... ./internal/poll/... -run 'TestCombinedStatus|TestPollerEmitsCIFromStatus' -v
PASS ok
delovdelo · ba229f2 · PASS · 08.09.2026 06:39
M7 Движок для приложений владельца
Подкоманда `agentstaff doccheck form <файл.md>` проверяет документ этапа «Двойника» по docs/проект/Ф2-приёмка-документов.md §3–4 без обращения к 1С: (1) YAML-паспорт с полями этап (протокол|анализ|лт|тз), заявка, источник, база, предыдущий (кроме протокола), коды (список регулярных выражений схемы кодов требований арендатора, например П\d+, Т\d+\.\d+, FR-\d{3}), объекты; (2) обязательные разделы по этапу задаются таблицей в пакете (по docs/проект/Ф2-приёмка-документов.md §2а/§4: протокол — таблица Тема/Требования/Договорённости; анализ — пять таблиц этапа 1, этап 2, сопоставление с типовым функционалом; лт — 12 разделов Листа Требований; тз — Цель, Объекты, Алгоритм, Кейсы приёмки, Подлог, Ограничения) и легко расширяются; (3) требования имеют коды по схеме из паспорта, без дублей, у каждого есть источник; (4) нить: файл из «предыдущий» существует, и каждый код требования предыдущего документа (по его схеме) упомянут в текущем; (5) нет заглушек (TODO, «…», «уточнить»). Вывод построчно с префиксом `doccheck:`, последняя строка `doccheck: ok` либо `doccheck: FAIL <n> замечаний`, код возврата 0/1. Образцы трёх документов — в `internal/doccheck/testdata/`
go test ./internal/doccheck/... -run 'TestDoccheckForm' -v && go run ./cmd/agentstaff doccheck form internal/doccheck/testdata/анализ.md
PASS ok doccheck: ok
delovdelo · f782a60 · PASS · 08.09.2026 06:03
M7 Движок для приложений владельца
Сегодня движок ходит в GitHub через одну установку приложения (AGENTSTAFF_GITHUB_INSTALLATION_ID), поэтому все арендаторы обязаны жить в одной учётке владельца. Репозиторий delovdelo/EventCode лежит во второй учётке владельца: приложение туда ставится отдельной установкой с другим installation id. Нужно: в AGENTSTAFF_TENANTS необязательный суффикс `@<installation_id>` (пример: `eventcode=delovdelo/EventCode@123456`); для арендатора с суффиксом клиент GitHub собирается со своим AppTokenSource (тот же App ID и ключ, другой installation id, свой кэш токена); без суффикса — установка по умолчанию, как сейчас. Poller, Loader/Board, Runtime (комментарии сигнала владельцу) и создание карточек арендатора используют клиент этого арендатора. Личный токен (AGENTSTAFF_GITHUB_TOKEN) с суффиксом несовместим — ошибка конфигурации с понятным текстом
go test ./cmd/agentstaff/... ./internal/github/... -run 'TestConfigFromEnvTenantInstallation|TestTenantClients|TestAppToken' -v
PASS ok
delovdelo · a0ab235 · PASS · 07.09.2026 19:51
M7 Движок для приложений владельца
Пакет `internal/brain`: единый интерфейс `Brain` (Complete(ctx, req) с системной инструкцией, сообщениями, инструментами и лимитом токенов → ответ с текстом и вызовами инструментов) и два адаптера: Anthropic Messages API (`/v1/messages`, заголовки x-api-key и anthropic-version, модель по умолчанию claude-sonnet-5) и OpenAI-совместимый chat completions (`/v1/chat/completions`, для LM Studio и локальных моделей, поле content, max_tokens). Конфигурация двух мозгов: `AGENTSTAFF_BRAIN_EXECUTOR_URL/KEY/MODEL` и `AGENTSTAFF_BRAIN_ACCEPTOR_URL/KEY/MODEL`; тип адаптера выводится по URL (api.anthropic.com → Anthropic, иначе совместимый). Правило двух мозгов: исполнитель и приёмщик обязаны различаться моделью или адресом (одинаковые URL и MODEL — ошибка конфигурации с понятным русским текстом; одинаковый URL с разными моделями — допустимо). Health: `Ping(ctx)` (Anthropic — короткий запрос с max_tokens 1; совместимые — GET /v1/models содержит модель). Ошибки сети/статуса — типизированные, с телом ответа (первые 200 байт); повтор 429/5xx до трёх раз с паузой. Без новых зависимостей
go test ./internal/brain/... ./cmd/agentstaff/... -run 'TestBrain|TestConfigFromEnvBrains' -v
PASS ok
delovdelo · 1853659 · PASS · 07.09.2026 20:47
M8 Команда внутри
Пакет `internal/acceptor` повторяет scripts/accept.sh внутри продукта: по PR читает карточку (Closes/Fixes/Resolves), команду приёмки, ожидаемые строки и подлог; готовит чистую копию репозитория на sha PR (git worktree в каталоге работ `AGENTSTAFF_WORKDIR`); выполняет команду (bash, таймаут из настроек, по умолчанию 20 минут); машинная сверка ожидаемых строк по порядку; `--- SKIP` ≠ PASS; подлог: sed по карточке, проверка суммы файла, сборка (`Сборка:` из карточки, если задана; иначе `go build ./...` при наличии go.mod, иначе пропуск с пометкой), команда обязана покраснеть (код возврата ≠ 0 или строка FAIL), контрольная правка комментария (// для .go, # для остальных) обязана остаться зелёной; затем закрытый вопрос мозгу приёмщика (internal/brain, ответ одной строкой PASS/FAIL/BLOCKED <sha>); мозг недоступен после трёх попыток — BLOCKED. Итог — структура Verdict {Kind, SHA, Reason, MachineCheck, MutationCheck, Log} и текст комментария первой строкой `PASS <sha>` / `FAIL <sha>: причина` / `BLOCKED <sha>: чего не хватило`. Публикация комментария — через интерфейс Poster (в этой карточке фейковый; реальная учётка приёмщика — следующая карточка). Подкоманда `agentstaff accept <owner/repo> <PR>` для ручного запуска
go test ./internal/acceptor/... -run 'TestAcceptor' -v
PASS ok
delovdelo · 1037eab · PASS · 07.09.2026 20:49
M8 Команда внутри
Связать internal/acceptor с продуктом: (1) `Asker` поверх `brain.Brain` (мозг приёмщика из AGENTSTAFF_BRAIN_ACCEPTOR_*): системная инструкция «ответь одной строкой PASS|FAIL|BLOCKED <sha>», MaxTokens не ниже 6000 (опыт accept.sh), ответ — первая строка с вердиктом; (2) `Poster` поверх `*github.Client`, собранного на токене учётки приёмщика `AGENTSTAFF_ACCEPTOR_GITHUB_TOKEN` (личный токен отдельной учётки, например delovdelo; логин учётки должен входить в AGENTSTAFF_ACCEPTORS — проверка при старте через GET /user, иначе отказ с понятным текстом); (3) `Source` и `Git` для приватных репозиториев: клон по https с токеном приёмщика через переменную окружения git (GIT_ASKPASS/credential helper), токен не в командной строке и не в журнале; (4) автозапуск в `serve`: при заданных мозгах и токене приёмщика продукт слушает события outbox `card.ready_for_acceptance` (как OwnerSignal слушает свои) и запускает приёмку PR арендатора, не больше одной одновременно на арендатора и не повторно для того же sha (память в процессе + проверка, что вердикта приёмщика на этот sha в PR ещё нет); результат публикуется комментарием, дальше конвейер видит его через вебхук/опрос как обычно; (5) `agentstaff accept <repo> <PR> --post` публикует по-настоящему. Живая проверка после слияния — архитектор запускает `agentstaff accept` на следующем PR рядом с scripts/accept.sh и сравнивает вердикты
go test ./internal/acceptor/... ./internal/runtime/... -run 'TestAcceptorBrainAsker|TestAcceptorAutoRun' -v
PASS ok
delovdelo · e37ffa3 · PASS · 07.09.2026 21:19
M8 Команда внутри
Пакет `internal/worker`: исполнитель по ADR-0006 п.4. Вход — карточка арендатора (номер, тело, репозиторий); шаги: аренда карточки (через существующий Leaser/queue, WIP=1 на исполнителя); чистая копия репозитория в AGENTSTAFF_WORKDIR на ветке `feat/<n>-<slug>` от ветки по умолчанию; цикл с мозгом исполнителя (brain.Brain) и инструментами, ограниченными каталогом копии: `list_files(path)`, `read_file(path)`, `write_file(path, content)`, `run(command)` (bash с таймаутом 10 минут, вывод обрезан до 200 строк), `run_acceptance()` (команда приёмки из карточки), `finish(summary)`; системная инструкция: цель, пакет, команда приёмки и ожидаемый вывод, стоп-условия из карточки и STANDARD.md §5, запрет трогать защищённые пути арендатора (список из карточки/стандарта), запрет менять карточку; бюджет: не больше 40 ходов и не больше 400 тысяч входных токенов суммарно, иначе стоп с сообщением в карточку; перед finish команда приёмки обязана быть зелёной локально; затем commit (сообщение по-русски, `Co-Authored-By: AgentStaff <noreply@agentstaff>`), push токеном исполнителя (`AGENTSTAFF_EXECUTOR_GITHUB_TOKEN`, отдельная учётка или установка с правом contents:write; токен не в командной строке), PR с телом «Closes #n» и выводом команды приёмки; исполнитель не сливает и не публикует вердиктов. Стоп-условие → комментарий в Issue одной записью и освобождение аренды. Подкоманда `agentstaff work <owner/repo> <n>` — один круг вручную
go test ./internal/worker/... -run 'TestWorker' -v
PASS ok
delovdelo · 7ddce40 · PASS · 07.09.2026 21:31
M8 Команда внутри
Пакет cmd/agentstaff — 2996 строк из 3000 (STANDARD.md §6.3). Вынести без изменения поведения: сборку арендаторов (tenants.go: разбор AGENTSTAFF_TENANTS, клиенты по установкам, buildTenants, startReadOnlyTenants, warmTenants), сборку мозгов (brains.go), токенов (tokens.go), адресов событий (sinks.go) и приёмщика (accept_serve.go) в новый пакет `internal/boot` (сборка продукта из настроек; зависит от app, poll, runtime, github, brain, acceptor, events, store — cmd после этого только разбирает переменные окружения и вызывает boot). Тесты переезжают вместе с кодом; число тестов до/после совпадает; `go run ./tools/limits` — ok; ADR-0002 дополнит архитектор узлом `cmd → internal/boot → …`
go run ./tools/limits && go test ./cmd/agentstaff/... ./internal/boot/... -count=1
limits: ok ok
delovdelo · 53b59cb · PASS · 07.09.2026 21:52
M8 Команда внутри
Подкоманда `agentstaff accept` при незаданном доступе к GitHub (нет AGENTSTAFF_GITHUB_TOKEN и нет тройки переменных приложения) печатает «не задан доступ к GitHub: …» и завершается кодом 0 (замер архитектора 08.09 при живой сверке). Ошибка конфигурации обязана давать код 2, как остальные отказы подкоманды («приёмщик не собран» уже даёт 2). Исправить и закрепить тестом: при пустом окружении `runAccept` (или как называется точка входа подкоманды) возвращает код 2 и текст с «доступ к GitHub»
go test ./cmd/agentstaff/... -run 'TestAcceptWithoutGitHubAccessExits2' -v
PASS ok
Issue #152 PR нет
вердиктов нет
M8 Команда внутри
`GET /healthz` отвечает `{"ok":true,"version":"…"}`; для наблюдения за продом нужно видеть, сколько арендаторов ведёт продукт. Добавить в ответ поле `tenants` (целое число арендаторов из реестра; без реестра — 1, арендатор по умолчанию). Ответ остаётся совместимым: прежние поля не меняются
go test ./internal/httpapi/... -run 'TestHealthzTenants' -v
PASS ok
delovdelo · 2037cd4 · PASS · 08.09.2026 05:54
M8 Команда внутри
PR #158, сделанный встроенным исполнителем, содержит тело «Closes #157» и вывод команды приёмки, а затем — сырой шаблон .github/PULL_REQUEST_TEMPLATE.md с плейсхолдерами («Closes #<номер>», «<команда из карточки>»). Нужно: `internal/worker/publish.go` заполняет шаблон, если он есть: подставляет номер карточки, пакет из карточки, команду приёмки и её вывод в соответствующие разделы; плейсхолдеров в угловых скобках в теле PR не остаётся; без шаблона — прежнее тело. Тест `TestWorkerPullBodyFillsTemplate`: шаблон с плейсхолдерами → тело без «<» и «>» в строках Closes/команды, с номером карточки и командой
go test ./internal/worker/... -run 'TestWorkerPullBodyFillsTemplate' -v
PASS ok
delovdelo · ca9baab · PASS · 08.09.2026 06:07
M8 Команда внутри
Подкоманда `agentstaff doccheck facts <файл.md>` — слои 2 и 3 из docs/проект/Ф2-приёмка-документов.md: (1) адаптер к источнику фактов `internal/facts` — HTTP JSON-RPC 2.0 по образцу сервиса ERPDoctorMCP (POST на URL с basic auth: методы `tools/list` и `tools/call` с `params.name` и `params.arguments`; ответ `result` с `content[]` текст/JSON), настройки `AGENTSTAFF_FACTS_MCP_URL/LOGIN/PASSWORD`, таймаут 60 с, ошибки типизированные, пароль не в журнале; (2) проверки по документу: каждый объект из паспорта `объекты` и каждое полное имя вида `Справочник.X`/`Документ.X`/`РегистрСведений.X`/`РегистрНакопления.X` в теле существует — `tools/call get_object_metadata {full_name}` (ошибка/пусто → замечание «объект не найден»); реквизиты вида `Документ.X.Реквизит` — через `get_object_attributes`; (3) воспроизводимость: каждая цифра в разделе «Данные» (или таблице с колонкой «Запрос (1С)») имеет рядом запрос; проверка выполняет `execute_query {query, max_rows}`, сравнивает: целые количества точно, суммы с допуском 1 %; цифра без запроса — замечание «не воспроизводима» (не ошибка формы, но замечание); (4) паспортная `база` сверяется с `get_server_info` (имя базы). Вывод как у `doccheck form` (`doccheck: …`, ok/FAIL, коды 0/1/2; сервис недоступен — код 3 «BLOCKED: источник фактов недоступен»). Без новых зависимостей. Тесты — с фейковым MCP-сервером (httptest), отвечающим на tools/call по именам
go test ./internal/facts/... ./internal/doccheck/... -run 'TestFacts|TestDoccheckFacts' -v
PASS ok
delovdelo · ef6fc42 · PASS · 08.09.2026 06:43
M7 Движок для приложений владельца
Карточка #163 остановлена исполнителем на 16-м ходу: 429 тыс. входных токенов при пределе 400 тыс., потому что каждый ход пересылает всю историю с полными выводами инструментов (до 200 строк каждый). Нужно: (1) после каждого хода выводы инструментов старше N=4 ходов заменяются в истории на короткую сводку («read_file internal/x.go: 180 строк, сжато») — модель видит свежие выводы целиком, старые в одну строку; (2) бюджеты настраиваются переменными `AGENTSTAFF_WORKER_MAX_TURNS` (по умолчанию 40) и `AGENTSTAFF_WORKER_MAX_INPUT_TOKENS` (по умолчанию 800000), значения печатаются при старте круга; (3) при остановке по бюджету исполнитель перед комментарием в Issue делает commit и push того, что есть, в ветку feat/<n>-… и пишет в комментарий имя ветки — чтобы работу можно было продолжить, а не потерять; (4) в результате круга — суммарные входные и выходные токены
go test ./internal/worker/... -run 'TestWorkerCompactsHistory|TestWorkerBudgetPushesBranch' -v
PASS ok
delovdelo · 3cdab51 · PASS · 08.09.2026 06:37
M8 Команда внутри
Пакет internal/worker — 2894 строки из 3000 (STANDARD.md §6.3). Вынести без изменения поведения: инструменты и границы каталога (tools.go, instruments.go) в `internal/worker/tools`, историю и сжатие (history.go) в `internal/worker/history`; тесты переезжают вместе с кодом; число тестов до/после совпадает; `go run ./tools/limits` — ok
go run ./tools/limits && go test ./internal/worker/... -count=1
limits: ok ok
delovdelo · 328e0fa · PASS · 08.09.2026 07:06
M8 Команда внутри
Живой прогон 08.09 на тестовой базе владельца показал два ложных замечания. (1) `doccheck form`: код требования в первой ячейке таблицы «Сопоставление с типовым функционалом 1С» (и любой другой таблицы вне объявляющих разделов) считался вторым объявлением → «дубль кода» и «нет источника». Правило: объявлением считается только строка таблицы (или заголовок) в объявляющем разделе этапа — для анализа это «Потребности», «Требования», «Проблемы»; для протокола — «Темы»; для лт — «Функциональные требования», «Пользовательские истории»; для тз — «Кейсы приёмки»; во всех остальных таблицах коды — ссылки (нить), не объявления. (2) `doccheck facts`: паспортная `база` (например `erp_dst`) сверялась только с полями ответа `get_server_info` (там `configuration_name: УправлениеПредприятием`, имени базы нет) → «база не подтверждена». Правило: база подтверждена, если её имя (без учёта регистра) встречается в любом строковом поле ответа get_server_info ИЛИ в пути адреса сервиса (`/erp_dst/hs/…`); иначе замечание с перечислением того, что сервис сообщил
go test ./internal/doccheck/... -run 'TestDoccheckFormReferencesAreNotDeclarations|TestDoccheckFactsBaseFromURL' -v
PASS ok
delovdelo · 53e3625 · PASS · 08.09.2026 06:57
M7 Движок для приложений владельца
Встроенный исполнитель дважды остановился на карточке #167 (разъезд пакета): 34 хода, 800 тыс. входных токенов, изменений нет — он читал файлы целиком и не начал писать. Нужно: (1) первый ход круга — план: мозг получает только карточку и список файлов пакета с размерами (`list_files` с числом строк) и обязан ответить планом из шагов (какие файлы читать, какие создавать), план записывается в журнал и в историю как один короткий ход; (2) `read_file(path, from, to)` — чтение диапазоном строк (по умолчанию первые 120), с подсказкой в ответе «файл N строк, показаны from–to»; (3) суммарный лимит прочитанных строк за круг (по умолчанию 6000; переменная `AGENTSTAFF_WORKER_MAX_READ_LINES`) — при превышении инструмент отвечает отказом «лимит чтения исчерпан, пиши код по тому, что уже прочитано»; (4) если к ходу 20 нет ни одного write_file — напоминание в ответе инструмента «половина бюджета без изменений — начни писать»; (5) в Result — число прочитанных и записанных строк
go test ./internal/worker/... -run 'TestWorkerPlansFirst|TestWorkerReadBudget' -v
PASS ok
delovdelo · cc8dfdc · PASS · 08.09.2026 07:56
M8 Команда внутри
Урок соседнего проекта (архитектор EventCode, 08.09): выкат кода прошёл, а одна новая настройка до прода не доехала, и это никто не заметил. У нас служба systemd читает EnvironmentFile при старте, поэтому риск ниже, но проверки нет. Нужно: (1) `scripts/deploy.sh` после подтверждения версии сверяет список ключей (без значений) из `/opt/agentstaff/etc/agentstaff.env` со списком ключей окружения живого процесса (`systemctl show agentstaff -p MainPID` → `/proc/<pid>/environ`, только имена переменных `AGENTSTAFF_*` и `PORT`); расхождение → строка `deploy: настройки: ключ X есть в файле, но не в процессе` и код возврата 1 (без отката: код уже верный, но владельцу нужно знать); совпадение → `deploy: настройки: N ключей совпали`; (2) в dry-run — строка плана `deploy: план: сверка ключей настроек файла и процесса`. Значения в вывод не попадают
bash -n scripts/deploy.sh && scripts/deploy.sh --dry-run v0.1.0 | tee /dev/stderr | grep -q "сверка ключей настроек файла и процесса"
deploy: план: сборка v0.1.0 linux/amd64 deploy: план: сверка ключей настроек файла и процесса
delovdelo · 73ea209 · FAIL · 08.09.2026 07:12 delovdelo · 73ea209 · PASS · 08.09.2026 07:14
M8 Команда внутри
Приёмщик (internal/acceptor и scripts/accept.sh) после подлога считает команду покрасневшей только по коду возврата ≠ 0 или строке FAIL. Карточка #173 (PR #178): подлог убирал ожидаемую строку плана из вывода, команда оставалась зелёной по коду — вердикт «механизм не защищён тестами», хотя ожидаемые строки — часть контракта карточки. Нужно: после подлога краснотой считается любое из трёх: код ≠ 0, строка FAIL, или машинная сверка ожидаемых строк не прошла (та же функция, что в зелёном прогоне); в строке «подлог: …» вердикта указывать, чем именно покраснел («код 1» / «FAIL» / «пропала строка "…"»). Контрольная правка по-прежнему обязана оставлять все три зелёными. accept.sh — то же правило (эталон и продукт должны совпадать)
go test ./internal/acceptor/... -run 'TestAcceptorMutationMissingExpectedLineIsRed' -v
PASS ok
delovdelo · 5c22e40 · PASS · 08.09.2026 07:38
M8 Команда внутри
Арендатор INFATON в команде приёмки документного этапа пишет две команды через `&&` (`doccheck form … && doccheck facts …`). Нужна подкоманда `agentstaff doccheck all <файл.md>`: сначала form; если form дал замечания — печатает их и завершается с кодом 1, facts не запускается (нет смысла сверять факты у документа без формы); если form ok — печатает `doccheck: форма ok` и запускает facts, итоговые строки как у facts (`doccheck: ok` / `doccheck: FAIL n замечаний` / `doccheck: BLOCKED: …`), коды 0/1/2/3 как у facts. Без переменных источника фактов — код 2 с текстом, какие переменные нужны (как у facts). Usage в main.go дополнить
go test ./internal/doccheck/... ./cmd/agentstaff/... -run 'TestDoccheckAll' -v
PASS ok
delovdelo · 6eddb87 · PASS · 08.09.2026 09:13 не учтено вердиктов: 1
M7 Движок для приложений владельца
Встроенный исполнитель четыре раза останавливался без единой записи (#163, #167, #179, #183: 16–40 ходов, до 800 тыс. токенов, «записано строк 0»), и из вывода круга нельзя понять, что делала модель. Нужно: (1) при `AGENTSTAFF_WORKER_TRACE=1` каждый ход пишется в журнал (slog INFO): номер хода, входные/выходные токены хода, stop_reason, первые 200 символов текста модели, и для каждого вызова инструмента — имя, аргументы (путь, диапазон, первые 80 символов команды/контента) и первые 120 символов ответа инструмента, включая отказы; (2) без переменной — как сейчас, плюс в итоговой строке круга число вызовов по инструментам (`read_file=…, write_file=…, run=…, отказов=…`); (3) отказы инструментов считаются и печатаются отдельно: «отказов инструментов: N (последний: …)»; (4) если модель три хода подряд не вызывает инструменты и не зовёт finish — исполнитель отвечает ей напоминанием «нужен вызов инструмента или finish» (в трассе видно)
go test ./internal/worker/... -run 'TestWorkerTraceLogsTurns|TestWorkerCountsToolCalls' -v
PASS ok
delovdelo · dee57e7 · PASS · 08.09.2026 08:20
M8 Команда внутри
На проде 08.09 событие owner.decision_needed по «карточке» #47 (на самом деле PR) 856 раз за два часа получало 403 от GitHub (комментарий в PR требует права pull_requests:write, у приложения его нет) и всё это время держало 23 следующих события арендатора недоставленными: DeliverLoop повторяет головное событие каждые 5 секунд и не идёт дальше. Нужно: (1) ошибка получателя, которая не пройдёт от повтора — HTTP 4xx кроме 429 и 408 (403, 404, 410, 422) — считается окончательной: событие помечается доставленным с пометкой отказа (поле `delivery_note`/причина в журнале одной строкой ERROR), очередь идёт дальше; (2) временные ошибки (сеть, 5xx, 429) — как сейчас, но через ErrDeferred (не блокируя следующие события), с нарастающей паузой; (3) отдельная строка журнала раз в час: «недоставленных событий арендатора N, старейшее с …»; (4) сигнал владельцу, который нельзя доставить комментарием, дополнительно пишется в журнал WARN с текстом сигнала — чтобы факт не пропал
go test ./internal/events/... ./internal/runtime/... -run 'TestDeliverPermanentRefusalDoesNotBlock' -v
PASS ok
delovdelo · 8d7379e · FAIL · 08.09.2026 08:31 delovdelo · 8d7379e · PASS · 08.09.2026 08:37
M8 Команда внутри
В БД прода есть «карточка» #47 арендатора agentstaff, хотя #47 — PR (закрыт 07.09): PR #48 в теле ссылался Closes #47, конвейер при pull_request/opened на неизвестную карточку завёл строку и повёл её до done, а затем сигнал владельцу упёрся в 403. Нужно: (1) конвейер при первом pull_request для номера, которого нет в таблице карточек, НЕ заводит строку сам: карточка появляется только из события issues (opened/edited) с блоком «## Карточка»; PR на неизвестный номер — «webhook ignored reason=card.unknown» (retryable, как сейчас для незнакомых), и это не меняется от того, что PR слит; (2) если в БД уже есть карточка, номер которой в GitHub — PR (проверка через Source при старте или при первом событии: у issue есть поле pull_request), конвейер помечает её `parked` с причиной «номер — PR» одной строкой WARN и больше не ведёт; (3) миграция не нужна — правка только логики и одна проверка при старте `serve` (список карточек арендатора × ListIssues: номера, которых нет среди issues, — паркуются с WARN)
go test ./internal/pipeline/... -run 'TestPipelineDoesNotInventCardFromPull' -v
PASS ok
delovdelo · 353fe81 · FAIL · 08.09.2026 08:30 delovdelo · 353fe81 · PASS · 08.09.2026 08:36
M8 Команда внутри
Трасса круга №183 (`AGENTSTAFF_WORKER_TRACE=1`, 14 ходов, 26 read_file, 0 write_file) показала две причины кругов без записи. (1) Модель claude-sonnet-5 по умолчанию отвечает блоками `thinking` (проверено живым запросом: `[thinking, text, tool_use]`, в `usage.output_tokens_details.thinking_tokens` их число); адаптер Anthropic их выбрасывает — ход 6 стоил 6446 выходных токенов, а в трассе `text=""` и три read_file. Разговор переигрывается модели текстом («[вызов …]» и «[read_file] …» в user), а не родными блоками, поэтому рассуждения и вызовы каждого хода теряются, и модель думает заново. (2) Сжатие истории по числу ходов (четыре последних) стирает содержимое прочитанных файлов уже при 20 тыс. входных токенов, и модель перечитывает те же файлы по кругу (ход 8 повторяет ход 3; ходы 12–14 — ходы 3–4), не доходя до записи. Нужно: (а) `brain.Message` несёт блоки, а не одну строку: у assistant — `thinking` (текст и `signature` как вернул API, возвращаются в запрос без изменений), `text`, `tool_use` (id, name, input); у user — `text` или `tool_result` (`tool_use_id`, content). Адаптер Anthropic шлёт и читает их в родном виде (`{"type":"thinking","thinking":…,"signature":…}`, `{"type":"tool_use",…}`, `{"type":"tool_result","tool_use_id":…,"content":…}`); совместимый адаптер — через `tool_calls` у assistant и сообщения `role: "tool"` с `tool_call_id`, `thinking` он не шлёт. `brain.Response` отдаёт блоки thinking (для переигрывания) и `ThinkingTokens` из `usage.output_tokens_details.thinking_tokens` (0, если поля нет). (б) История исполнителя переигрывает родные блоки: ход модели — как вернулся, ответы инструментов — tool_result по id вызова. Сжатие — по объёму, а не по ходам: ответы инструментов остаются целиком, пока история меньше предела в символах (по умолчанию 200 000; переменная `AGENTSTAFF_WORKER_HISTORY_CHARS`), при превышении сжимаются самые старые ответы (одной строкой «[сжато] …», как сейчас); вызовы и последний ход модели с его thinking не сжимаются никогда. (в) Трасса хода пишет `thinking_tokens` и перечень типов блоков ответа. (г) Строка системного промпта про «старше четырёх последних ходов» заменяется на правду про предел по объёму. Всё без сети: адаптеры проверяются на httptest с записанными телами.
go test ./internal/brain/ ./internal/worker/... -run 'TestAnthropicReplaysBlocks|TestCompatReplaysToolMessages|TestHistoryCompactsBySize|TestWorkerTraceLogsTurns' -v
PASS ok
delovdelo · ec6e56d · PASS · 08.09.2026 08:49
вне вех
Подлог, который не лёг на код, — дефект карточки, а не работы, и не должен стоить исполнителю вердикта FAIL (08.09: три PR — #189, #190, #196 — получили FAIL от встроенного приёмщика только потому, что строка `sed:` карточки не совпала с кодом; второй такой FAIL блокирует карточку). Нужно: когда команда sed из карточки завершилась ошибкой или сумма файла после неё не изменилась (`mutationDidNotLand`), приёмщик выносит BLOCKED, а не FAIL: первая строка вердикта `BLOCKED <sha>: подлог не лёг: <причина>` (причина — «sed не отработал: …» либо «сумма файла не изменилась»), дальше как сейчас — сверка, подлог, модель. Мозг приёмщика в этом случае не спрашивается (закрытый вопрос задаётся только после состоявшегося подлога). Подлог, который лёг, но не покраснел, и подлог, который сломал сборку, остаются FAIL, как в STANDARD.md §4.4
go test ./internal/acceptor/ -run 'TestAcceptorMutationDidNotLandIsBlocked|TestAcceptorMutationStayedGreenIsFail' -v
PASS ok
delovdelo · 2d55ba9 · FAIL · 08.09.2026 09:30 delovdelo · 2d55ba9 · PASS · 08.09.2026 09:32
вне вех
Встроенный исполнитель закончил карточку #183 с зелёной командой приёмки, а CI покраснел на проверке `go run ./tools/limits` (функция теста длиннее 80 строк): исполнитель проверяет только команду приёмки, а арендатор проверяет ещё и стиль, пределы, запрещённые слова. Нужно: (1) необязательное поле карточки «Проверка:» — команда bash, которую исполнитель обязан прогнать зелёной после последней записи, прежде чем finish будет принят; разбирается из блока карточки так же, как «Сборка:» (одна строка или блок с отступом); (2) инструмент `finish` отказывает, если после последней записи команда проверки не прогонялась или была красной: ответ «сначала проверка: …» с последними 20 строками вывода; отказ считается в счётчиках отказов (#185); (3) новый инструмент `run_check` (как `run_acceptance`, но команда из поля «Проверка:»); карточка без поля — как сейчас, finish требует только зелёную приёмку; (4) в системном промпте исполнителя и в описании инструментов сказано про проверку; (5) первое сообщение круга показывает команду проверки рядом с командой приёмки
go test ./internal/worker/... -run 'TestWorkerFinishNeedsGreenCheck|TestJobParsesCheckCommand' -v
PASS ok
delovdelo · f6fe93f · PASS · 08.09.2026 09:34
вне вех
После #191 история круга не сжимается по ходам, и каждый ход заново отправляет всё прочитанное: круги #197 и #198 остановились на бюджете входных токенов (840 302 и 820 600 при пределе 800 000 за 24 и 30 ходов) — один с пятью записями и зелёными тестами, но без приёмки, другой без единой записи. Бюджет считает повторно отправленные токены как новые, хотя Messages API умеет кэш промпта: повтор кэшированной части стоит десятую долю. Нужно: (1) адаптер Anthropic помечает системный промпт и последний блок последнего сообщения меткой `cache_control: {"type":"ephemeral"}` (описания инструментов кэшируются вместе с системой; метка в системе — блок `{"type":"text","text":…,"cache_control":…}`; в сообщениях — на последнем блоке содержимого последнего сообщения user); (2) `brain.Response` отдаёт `CachedInputTokens` из `usage.cache_read_input_tokens` и `CacheWriteTokens` из `usage.cache_creation_input_tokens` (0, если полей нет; совместимый адаптер — 0); (3) бюджет круга считает некэшированные входные токены: `input_tokens + cache_creation_input_tokens` (кэшированное чтение в бюджет не входит); предел по умолчанию остаётся 800 000; в итоговой строке круга и в трассе хода печатаются входные, из них кэшированные, и выходные; (4) при отсутствии кэша (совместимый адаптер) поведение прежнее. Проверить без сети на httptest: второй запрос несёт метки, а usage с cache_read_input_tokens не тратит бюджет
go test ./internal/brain/ ./internal/worker/... -run 'TestAnthropicMarksCache|TestWorkerBudgetSkipsCachedTokens' -v
PASS ok
delovdelo · 28f7be1 · PASS · 08.09.2026 09:59
вне вех
Круг #197 остановился по бюджету с пятью записанными файлами и зелёными тестами; исполнитель закоммитил «незавершённая работа исполнителя: карточка #197» только в локальную копию, а следующий круг стирает копию (`Prepare` делает `RemoveAll`) и начинает с нуля — работа за 24 хода пропала. Уточнение по факту 08.09: незавершённую работу исполнитель уже отправляет в ветку карточки на origin (657428f в feat/197-blocked-fail); беда во втором круге: он начал с main, сделал ту же ветку заново и не смог запушить (non-fast-forward) — готовая работа с зелёной приёмкой осталась в копии. Нужно: (1) push ветки карточки — `--force-with-lease` от того sha, с которого круг начал (свой же прошлый круг перезаписывается, чужой коммит — нет); (2) следующий круг той же карточки начинает с этой ветки, если она есть на origin: копия берётся с неё, а не с main, ветка сохраняется; (3) первое сообщение круга говорит модели, что это продолжение: какой была причина остановки прошлого круга (из сообщения коммита) и `git diff --stat main...HEAD` — какие файлы уже изменены; план первого хода строится от этого; (4) если ветки нет — как сейчас; (5) готовая работа по-прежнему уходит в тот же PR-поток: коммит поверх незавершённого, push, PR с `Closes #N`
go test ./internal/worker/... -run 'TestWorkerResumesFromUnfinishedBranch|TestWorkerPushesUnfinishedWork' -v
PASS ok
delovdelo · b699738 · PASS · 08.09.2026 10:05
вне вех
Исполнитель не видит поля «Подлог» карточки: системный промпт несёт цель, пакет, команду приёмки, ожидаемый вывод и стоп-условия, а строку `sed:` — нет. Поэтому код пишется без оглядки на подлог, sed архитектора не ложится, и приёмщик отвечает «подлог не лёг» (08.09: PR #201 по карточке #197 — sed ждал `verdict.go`, исполнитель сделал развилку в `run.go`). Нужно: (1) блок «Подлог» карточки (файл, что, sed, ждём) попадает в системный промпт исполнителя с правилом: «код обязан быть построен так, чтобы sed подлога лёг ровно на одно место названного файла и после него команда приёмки краснела»; (2) новый инструмент `check_mutation`: применяет sed из карточки к копии названного файла во временном каталоге, отвечает «лёг» (сумма изменилась) или «не лёг» и число совпадений (`grep -c` шаблона до замены, если шаблон — простая подстановка `s/…/…/`); файлы копии не трогает; (3) `finish` отказан, если в карточке есть подлог, а `check_mutation` после последней записи не отвечал «лёг»: текст «сначала подлог: …»; отказ считается счётчиком отказов; (4) у карточки без подлога поведение прежнее
go test ./internal/worker/... -run 'TestWorkerFinishNeedsLandedMutation|TestToolsCheckMutation|TestJobParsesMutation' -v
PASS ok
Issue #202 PR нет
вердиктов нет
вне вех
В круге #199 исполнитель правил файлы не инструментом write_file, а командами `run` с python-скриптами и heredoc (7 из 20 вызовов run), потому что write_file пишет файл целиком: переписать 346 строк ради двух — дорого в выходных токенах. Правки через run не считаются записью: счётчик «записано строк» врёт, состояние «после последней записи» для run_check и finish (#198) не сбрасывается, трасса не видит, что изменилось. Нужно: (1) инструмент `edit_file(path, old, new)`: заменяет ровно одно вхождение `old` на `new`; если вхождений нет или больше одного — отказ с числом вхождений, файл не тронут; пути — те же границы, что у write_file; правка считается записью (счётчик строк по разнице, сброс состояния проверки, трасса пишет путь и первые 80 символов old); (2) системный промпт: «для точечной правки — edit_file, для нового файла или полной замены — write_file; файлы командой run не править»; (3) `run` отказывает командам, которые явно пишут в файлы копии в обход инструментов: heredoc в файл (`<<` вместе с `>`/`>>` или `cat >`), `sed -i`, `python3 -`/`python -c` с `open(...,'w')`; текст отказа называет edit_file; обычные `go build`, `go test`, `grep`, `gofmt -l` не задеваются (gofmt -w — задевается: форматирование делается edit_file/write_file или перечитыванием, скажи об этом в промпте); (4) тесты на каждую ветку
go test ./internal/worker/... -run 'TestToolsEditFile|TestToolsRunRefusesFileWrites' -v
PASS ok
Issue #205 PR нет
вердиктов нет
вне вех
Приложению GitHub на проде не выдано право Commit statuses: Read-only, и чтение свёрнутого статуса (`GET …/commits/{sha}/status`) отвечает 403 «Resource not accessible by integration». Поллер считает это ошибкой всего опроса PR: журнал за 90 минут 08.09 — 2921 строка ERROR, факт CI по check runs для этих PR из опроса пропадает (карточки двигались только вебхуками). Нужно: (1) 403/404 от источника commit status — не ошибка опроса: CI читается по check runs, как до карточки #129; (2) об отсутствии права продукт говорит одной строкой WARN на арендатора в час («статусы коммитов недоступны приложению (403): факт CI только по check runs; выдайте право Commit statuses: Read-only»), а не ERROR на каждый PR; (3) другие ошибки источника статусов (5xx, сеть) — как сейчас; (4) тест: читатель, чей CombinedStatus отвечает 403, а check runs зелёные, — карточка идёт в «на приёмке»
go test ./internal/poll/... -run 'TestPollerStatusForbiddenFallsBackToChecks' -v
PASS ok
delovdelo · f5c4d3a · FAIL · 08.09.2026 10:12 delovdelo · f5c4d3a · PASS · 08.09.2026 10:13
вне вех