process: отвязать роли от конкретных агентов (#562)

Issue: #562
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-13 06:56:20 +00:00
committed by claude[bot]
parent f6e5651ccf
commit 3934f8d8e5
6 changed files with 131 additions and 75 deletions
+35 -25
View File
@@ -38,8 +38,11 @@ task records: problem, scope, acceptance criteria and discussion.
**Status lives in labels:** `S1-new`, `S2-analysis`, `S3-spec`, `S4-spec-review`,
`S5-ready`, `S6-in-progress`, `S7-code-review`, `S8-merged`, plus `blocked` on top
of a status and `rejected` on a closed issue. Exactly one `S*` label per open
issue. Labels are the whole of it: GitHub Projects is no longer used.
of a status and `rejected` on a closed issue. A product issue in flight carries
exactly one `S*` label. An infrastructure-only issue is the deliberate exception:
it may carry no `S*` label while being implemented, enters the common flow at
`S7-code-review`, and from then on carries exactly one. Labels are the whole of
it: GitHub Projects is no longer used.
**The light track is the default, not a shortcut** (owner's decision 2026-08-27,
issue #338). `small` — the spec lives in the issue body and its review is a
@@ -57,10 +60,11 @@ review is never skipped on either track; it is what stands in for testing.
`PROCESS.md` §5 and §5.1 hold the criteria.
An issue filed by an outsider is worked exactly like one of the owner's own, once
the owner has decided to take it. The check sits **at the entrance**, not on every
step: while an issue carries no status label it is outside the process and the
invariants do not apply to it; once a label is on, the task is in flight and **who
filed it stops mattering**.
the owner has decided to take it. For product work the check sits **at the
entrance**, not on every step: the first status label admits it to the flow. For
infrastructure work an explicit assignment by the owner is the entrance, and the
issue may remain without an `S*` label until its first `S7-code-review`. In either
case, **who filed it stops mattering** once the owner has admitted it.
Applying that first label *is* the owner's explicit decision, and the platform
already guarantees it — only someone with write access can label. The earlier rule
@@ -185,32 +189,36 @@ way. The layout is therefore fixed:
or the owner — never reset or clean them away.
- **`houseplan-card-src/hp-dev`** — the owner's worktree, permanently on `dev`. For owner-side operations that must not disturb the
author's tree: pushing `dev`, restoring a hook's executable bit, emergencies.
- **The reviewer and the infrastructure agent own no local tree.** The reviewer
runs in CI on a fresh checkout. The infrastructure agent reads via `git show`
and publishes through the GitHub API; it makes no local commits at all, so it
needs no `HEAD` of its own. Its scratch worktrees live outside the repo and are
pruned after use.
- **The reviewer owns no author tree.** It runs in CI on a fresh checkout. The
agent implementing an infrastructure task is an ordinary task author and uses
the same author-tree rules as product work; task branches must not share a
mutable checkout concurrently.
A worktree is only usable on the machine that created it: the `.git` file records
an absolute path in that machine's format. One created from a Linux sandbox is
dead on Windows and vice versa — create worktrees on the machine that will use
them, which for `hp-dev` means the owner's.
## Two-agent workflow
## Agent-neutral workflow
**Codex** writes analysis, specs and all product code. **Claude** reviews specs and
code and owns infrastructure and distribution. The owner rules on disputes, closes
issues and commands releases.
No task type is reserved for Codex, Claude or any other named model. **Any agent
may take any task**: analysis, spec, product implementation, infrastructure or a
release explicitly commanded by the owner. Roles describe the current artifact,
not the agent brand. The owner rules on product disputes, closes issues and
commands releases.
Author and reviewer are different models, which is what "a fresh session without
implementation context" means in practice. The reviewer never edits product code;
the author never grades their own work.
Author and reviewer are independent agents/sessions. They need not use different
model families, but the reviewer must start without implementation context and
must not be the author grading their own work. The reviewer does not edit the
material under review.
**Infrastructure-only work runs outside this flow.** CI, scripts, labels, demo
stands, the landing page and distribution are Claude's alone, and running them
through spec-writing and review buys nothing: the spec would restate what is
already unambiguous, and author and reviewer would be the same role. So no spec
file, no spec review, no code review, no walk through `S1`…`S8`.
**Infrastructure-only work uses an accelerated entry into the common flow.** It
is implemented immediately by any agent, without analysis, spec, spec review or
the statuses `S1`…`S6`. Once the branch is ready and pushed, apply
`S7-code-review`. From there the ordinary controller applies: green review rebases
and merges the checked material into `dev` and then sets `S8-merged`; findings or
a failed merge return the issue to `S6-in-progress`, and after correction it is
submitted to `S7-code-review` again.
The test for "infrastructure only" is mechanical: **not a single class A file** —
nothing under `src/**`, no `custom_components/**/*.py`, no manifests, no i18n. A
@@ -220,8 +228,10 @@ a loose reading would turn this into the route by which product changes skip
review.
What stays mandatory either way: an issue exists, both trailers are on every
commit, `typecheck`, `test` and `build` are green, and any non-obvious decision is
written down in the code or the issue rather than kept in someone's head.
commit, proportionate local gates are green (normally `typecheck`, `test` and
`build`), and any non-obvious decision is written down in the code or the issue
rather than kept in someone's head. Infrastructure work skips specification, not
code review.
**Review starts by itself.** Applying `S4-spec-review` or `S7-code-review` fires the
pipeline, which reviews without anyone asking and takes ten to forty-five minutes.
+53 -33
View File
@@ -1,9 +1,11 @@
# Процесс работы над House Plan
> **Статус документа: канон** (редакция 2026-08-13). Решения владельца, на
> **Статус документа: канон** (редакция 2026-08-13, ролевое уточнение
> 2026-09-13). Решения владельца, на
> которых он стоит: прямые коммиты в `dev` **без PR** · канон статуса — **метки**,
> имена английские · лёгкий трек **включён** · автор и ревьюер — разные модели ·
> инфраструктурные задачи идут **вне** флоу.
> имена английские · лёгкий трек **включён** · автор и ревьюер — независимые
> агенты/сессии · любой агент может взять любую роль · инфраструктурные задачи
> входят в общий флоу сразу на `S7-code-review`.
>
> **Область действия:** обязателен для владельца и для любого агента. Читается
> сразу после `docs/SCOPE.md` и `AGENTS.md`, до `docs/STATUS.md`. Живёт в
@@ -44,13 +46,20 @@
лежит внутри `custom_components/houseplan/frontend/`, и без этого правила он
считался бы продуктовым исходником.
**Инфраструктурная задача идёт вне флоу** (решение владельца 2026-08-13,
issue #118). Признак механический: **ни одного файла класса A**. Такая задача
делается без ТЗ, ревью ТЗ, код-ревью и без прохода по статусам — флоу построен
для изменений, у которых есть персона и видимое поведение, а в инфраструктуре ТЗ
пересказывало бы очевидное, и автор с ревьюером оказались бы одной ролью.
Проверкой служат гейты и CI. Обязательным остаётся issue, трейлеры и зелёные
`typecheck`, `test`, `build`.
**Инфраструктурная задача использует ускоренный вход в общий флоу** (решение
владельца 2026-09-13, issue #562). Признак механический: **ни одного файла класса
A**. Любой агент может сразу реализовать её по issue и в ветке
`issue/<NN>-<slug>`, без аналитики, ТЗ, ревью ТЗ и статусов `S1`…`S6`. Когда
материал готов, локальные гейты зелёные и ветка запушена, исполнитель ставит
`S7-code-review`. Дальше действует тот же контроллер, что для продуктового кода:
зелёное ревью сливает проверенный материал в `dev` и ставит `S8-merged`, а
замечания или неудавшееся слияние возвращают задачу в `S6-in-progress`; после
исправлений она снова идёт в `S7-code-review`.
Отсутствие ТЗ не означает отсутствие проверки. Для инфраструктуры обязательны
issue, терминальные трейлеры, соразмерные изменению зелёные гейты, хендофф с
доказательствами и независимое код-ревью. Модель или имя агента процессом не
предписываются.
Задача, задевающая класс A хотя бы одним файлом, инфраструктурной **не
является** и идёт полным флоу. «В основном инфраструктурная» не бывает: иначе
@@ -61,7 +70,8 @@ issue #118). Признак механический: **ни одного фай
## 2. Жизненный цикл
Восемь рабочих статусов и два служебных. Фазы тестирования в цикле сознательно
Восемь рабочих статусов и два служебных. Полный маршрут ниже относится к
продуктовым задачам. Фазы тестирования в цикле сознательно
**нет**: найденные позже дефекты заводятся отдельными issue и проходят цикл
заново. Issue закрывается после выпуска беты.
@@ -72,6 +82,7 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
служебные: blocked (поверх статуса) rejected (закрыт)
⟲ — возврат на правки, не более 4 циклов (§4), на лёгком и коротком треке 2
короткий трек (`trivial`, §5.1) идёт S2-analysis → S5-ready, минуя S3 и S4
инфраструктурный трек (§1): без S → S7-code-review ⟲ S6-in-progress → S8-merged
```
Переходы `S4-spec-review` и `S7-code-review` выполняются **автоматически**: метка
@@ -501,19 +512,18 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
**Правило разделения:** ревьюер работает состязательно. Ему передаётся тег или
диапазон коммитов и ТЗ — не рассказ автора о том, как всё хорошо.
**Роли закреплены за исполнителями** (решение владельца 2026-08-12):
**Роли не закреплены за моделями или именами агентов** (решение владельца
2026-09-13, issue #562). Codex, Claude или любой другой доступный агент может быть
аналитиком, автором ТЗ, разработчиком, автором инфраструктурной задачи или
релиз-инженером по прямой команде владельца.
| Исполнитель | Роли |
|---|---|
| **Codex** | аналитик, автор ТЗ, разработчик, релиз-инженер по команде владельца |
| **Claude** | ревьюер ТЗ, ревьюер кода, вся инфраструктура и дистрибуция |
| **Владелец** | приоритет, скоуп, арбитраж, закрытие issue, команда на выпуск |
Разделение относится к артефакту: **автор и ревьюер — разные агенты/сессии**.
Модель может совпадать, но ревьюер начинает без контекста реализации и не ставит
вердикт собственной работе. Ревью ТЗ и код-ревью также идут в независимых
сессиях: ревьюер кода не должен приходить с контекстом обсуждения ТЗ.
Автор и ревьюер — **разные модели**, и это сильнее требования «другая сессия»:
одна модель, читая свой же артефакт заново, повторяет свои же слепые пятна.
Ревью ТЗ и код-ревью держатся в **разных сессиях** Claude: ревьюер кода не должен
приходить с контекстом того, как обсуждали ТЗ.
Владелец сохраняет исключительные решения о приоритете, ценности, продуктовом
скоупе, отклонении, арбитраже, закрытии issue и команде на выпуск.
---
@@ -741,18 +751,22 @@ Performance зелёные на точном SHA, плюс зелёный E2E н
Тематические метки (`polish`, `infra`, `tests`, `docs`, `security`, `vacuum`)
ортогональны процессу.
Инварианты: **ровно одна `S*`-метка** на открытом issue; закрытый issue статусных
меток не несёт; `blocked` не заменяет статус, а дополняет его.
Инварианты: продуктовая задача в процессе несёт **ровно одну `S*`-метку**.
Инфраструктурная задача может не иметь `S*` во время первоначальной реализации;
с первого `S7-code-review` на неё действует тот же инвариант ровно одной метки.
Закрытый issue статусных меток не несёт; `blocked` не заменяет статус, а дополняет
его.
**Чужой issue берётся в работу так же, как свой — после явного решения
владельца** (решение владельца 2026-08-13, уточнено в тот же день). Репозиторий
публичный, отчёты заводят и посторонние; проверка стоит **на входе**, а не на
каждом шаге.
Входом служит присвоение первой статусной метки: пока меток нет, issue вне
процесса и инварианты на него не распространяются. Как только метка стоит, задача
в работе, и **кто её завёл, дальше не имеет значения** — статусы, ревью и лимиты
работают одинаково.
Для продуктовой задачи входом служит присвоение первой статусной метки. Для
инфраструктурной — явное назначение владельцем; до готовности к первому
код-ревью она может оставаться без `S*`. Как только продуктовая задача вошла в
полный маршрут либо инфраструктурная получила `S7-code-review`, **кто её завёл,
дальше не имеет значения** — статусы, ревью и лимиты работают одинаково.
Присвоение метки и есть то самое явное решение, причём проверенное платформой:
метки может ставить только тот, у кого есть право записи в репозиторий. Прежняя
@@ -894,11 +908,13 @@ S4-spec-review → ревью ТЗ → S5-ready либо возврат в S3-
S7-code-review → код-ревью → слияние в dev → S8-merged либо возврат в S6-in-progress
```
Ревьюер — `anthropics/claude-code-action`. Он читает `docs/SCOPE.md`, `AGENTS.md`,
этот документ и тело issue, публикует разбор комментарием, заводит issue на Medium-находки
вне скоупа задачи (#202), кладёт документ в `docs/reviews/` ветки задачи и возвращает вердикт
структурированным JSON. **Метку переставляет отдельный детерминированный шаг по
вердикту, а не модель.**
Текущая техническая реализация независимого ревьюера —
`anthropics/claude-code-action`; это деталь автоматизации, а не закрепление роли
или вида задач за Claude. Ревьюер читает `docs/SCOPE.md`, `AGENTS.md`, этот
документ и тело issue, публикует разбор комментарием, заводит issue на
Medium-находки вне скоупа задачи (#202), кладёт документ в `docs/reviews/` ветки
задачи и возвращает вердикт структурированным JSON. **Метку переставляет отдельный
детерминированный шаг по вердикту, а не модель.**
Четыре вещи, без которых конвейер молча не работает:
@@ -1163,6 +1179,10 @@ Golden, браузерные смоки, performance и полный HA-харн
→ закрытие пачкой при выпуске беты. Оба ревью возвращают на правки не более 4
циклов; пятый заход — разбор у владельца (разделить / отклонить / арбитраж).
Инфраструктурная задача (ни одного файла класса A) делается сразу любым агентом:
без `S*` → `S7-code-review` ↔ `S6-in-progress` → `S8-merged`. ТЗ и ревью ТЗ нет,
код-ревью обязательно.
Ревью запускается **само** от меток `S4-spec-review` и `S7-code-review` и идёт до
45 минут. Поставив такую метку, автор не заканчивает работу, а ждёт смены метки
опросом и продолжает по тому, чем она стала.
+6 -6
View File
@@ -422,11 +422,11 @@ export function commitsUnderRuleOne(commits) {
);
}
// Инфраструктурная работа по решению владельца #118: признак механический —
// в диапазоне НЕТ ни одного файла класса A. Такая задача идёт вне продуктового
// флоу и по построению не имеет статусной метки, поэтому проверка 8 требовала у
// неё невозможного и краснела на каждом инфра-коммите (#207): Validate на dev
// был красным систематически, и сигнал догоняющей проверки обесценился.
// Инфраструктурная работа по решению владельца #562: признак механический —
// в диапазоне НЕТ ни одного файла класса A. Такая задача реализуется до первого
// S-статуса и входит в общий флоу готовой веткой через S7-code-review. Поэтому
// первоначальный push без статуса допустим; требовать S-метку на нём означало бы
// сделать предписанный маршрут технически невозможным (#207).
//
// Исключение опирается на diff, а не на метку-разрешение: метку `infra` можно
// поставить продуктовой задаче и увести продуктовый коммит от проверки статуса,
@@ -809,7 +809,7 @@ function main(argv) {
// нарушение и никто не узнает, что проверка не выполнялась.
findings.push({
level: 'warn', rule: 8, sha: '-',
msg: `инфраструктурный диапазон (#118): файлов класса A нет, статусная метка issue не требуется`,
msg: `инфраструктурный диапазон (#562): файлов класса A нет, статусная метка не требуется до S7-code-review`,
});
}
findings.push(...checkIssueStatuses(numbers, cached, { allowed, statusOptional }));
+14 -4
View File
@@ -26,11 +26,16 @@ export const STATUS_LABELS = ['S1-new', 'S2-analysis', 'S3-spec', 'S4-spec-revie
export function rightsFor(status, labels = []) {
const blocked = labels.includes('blocked');
const exhausted = labels.includes('review-4');
const code = ['S5-ready', 'S6-in-progress', 'S7-code-review'].includes(status);
const infrastructure = labels.includes('infra');
const code = !infrastructure && ['S5-ready', 'S6-in-progress', 'S7-code-review'].includes(status);
const lines = [];
if (exhausted) lines.push('review-4: лимит циклов исчерпан — решение владельца (разделить, отклонить, арбитраж); дальше не двигать');
if (blocked) lines.push('blocked: работа стоит, ждём внешнего решения — коммиты по задаче гейт не пропустит');
lines.push(code ? 'продуктовый код трогать МОЖНО (правило №1)' : 'продуктовый код трогать НЕЛЬЗЯ: статус не S5/S6/S7 (правило №1)');
lines.push(code
? 'продуктовый код трогать МОЖНО (правило №1)'
: infrastructure
? 'файлы класса A трогать НЕЛЬЗЯ; инфраструктурную реализацию МОЖНО вести сразу по issue (#562)'
: 'продуктовый код трогать НЕЛЬЗЯ: статус не S5/S6/S7 (правило №1)');
switch (status) {
case 'S1-new': lines.push('следующий шаг: аналитика (S2) — оценки метками, критерий лёгкого трека, затем ТЗ'); break;
case 'S2-analysis': lines.push('следующий шаг: ТЗ (S3); трек по умолчанию small — отказ от него обосновать названным критерием §5'); break;
@@ -40,7 +45,10 @@ export function rightsFor(status, labels = []) {
case 'S6-in-progress': lines.push('следующий шаг: gate:small + смоки по AC → push ветки → метка S7-code-review (метку после push)'); break;
case 'S7-code-review': lines.push('идёт код-ревью: ждать вердикт, ничего не пушить в ветку — вердикт привязан к SHA (#312)'); break;
case 'S8-merged': lines.push('код в dev, ждёт беты; ничего не делать; issue закроет владелец при выпуске'); break;
default: lines.push('статусной метки нет — задача вне процесса; вход в процесс — присвоение первой S*-метки владельцем');
default:
lines.push(infrastructure
? 'инфраструктурный вход: реализовать и проверить → push ветки → S7-code-review; ТЗ и S1–S6 не нужны (#562)'
: 'статусной метки нет — продуктовая задача вне процесса; вход — первая S*-метка владельца');
}
return lines;
}
@@ -104,7 +112,9 @@ export function buildPacket(inputs) {
issue, labels = [], comments = [], owner = 'Matysh', branch = null, specs = [], reviewDocs = [], validate = null,
} = inputs;
const status = STATUS_LABELS.find((l) => labels.includes(l)) || null;
const track = labels.includes('trivial') ? 'trivial' : labels.includes('small') ? 'small' : 'полный';
const track = labels.includes('infra')
? 'инфраструктурный'
: labels.includes('trivial') ? 'trivial' : labels.includes('small') ? 'small' : 'полный';
const stage = status === 'S4-spec-review' || status === 'S3-spec' || status === 'S5-ready' ? 'spec' : 'code';
const verdict = lastVerdict(comments, reviewDocs, stage);
// ТЗ живёт в теле issue (#517); архивный файл — источник только у задач до
+6 -6
View File
@@ -701,7 +701,7 @@ test('the CLI falls back to origin/dev when BEFORE_SHA is orphaned by a force-pu
}
});
test('an infrastructure range is recognised by the absence of class A files (#207)', () => {
test('an infrastructure range is recognised by the absence of class A files (#562)', () => {
const infra = makeCommit({
sha: 'a'.repeat(40), subject: 'Tune CI', body: 'Issue: #206\nUser-Visible: no',
files: ['.github/workflows/validate.yml'],
@@ -734,8 +734,8 @@ test('a support-relay-only change stays on the reviewed class-B track (#43)', ()
assert.equal(isInfrastructureRange([relay]), true);
});
test('statusOptional waives the status label but keeps every other rule-8 refusal (#207)', () => {
// Метки инфраструктурного issue по #118: тип, приоритет, тема — без S*.
test('statusOptional permits the pre-S7 infra push but keeps every other rule-8 refusal (#562)', () => {
// Первый push инфраструктурного issue по #562: тип, приоритет, тема — без S*.
const infraIssue = () => ({
ok: true, json: JSON.stringify({ state: 'OPEN', labels: [
{ name: 'bug' }, { name: 'P2' }, { name: 'infra' },
@@ -764,10 +764,10 @@ test('statusOptional waives the status label but keeps every other rule-8 refusa
assert.deepEqual(rules(checkIssueStatuses(['206'], garbage, { statusOptional: true })), [8]);
});
// AC #207: сквозной прогон CLI по настоящему репозиторию с подставным gh.
// AC #562: сквозной прогон CLI по настоящему репозиторию с подставным gh.
// Диапазон без класса A и с issue без статусной метки обязан быть зелёным;
// тот же диапазон плюс один файл `src/**` — красным по проверке 8.
test('the CLI waives the issue status for a class-B-only range but not with class A (#207)', (t) => {
test('the CLI permits a pre-S7 class-B-only range but not one with class A (#562)', (t) => {
if (process.platform === 'win32') {
t.skip('нужен исполняемый stub gh — прогон в Linux CI');
return;
@@ -794,7 +794,7 @@ test('the CLI waives the issue status for a class-B-only range but not with clas
git('-c', 'user.name=t', '-c', 'user.email=t@t', '-c', 'core.hooksPath=/dev/null',
'commit', '-q', '-m', message);
};
// Подставной gh: инфраструктурный issue по #118 — тип, приоритет, тема, без S*.
// Подставной gh: первый push инфраструктурного issue по #562 — без S*.
const ghStub = join(dir, 'gh-stub.mjs');
writeFileSync(ghStub, '#!/usr/bin/env node\n'
+ 'process.stdout.write(JSON.stringify({ number: 206, state: "OPEN", labels: '
+17 -1
View File
@@ -16,7 +16,11 @@ test('права выводятся из статусной метки по пр
assert.ok(rightsFor('S7-code-review').some((l) => l.includes('#312')));
assert.ok(rightsFor('S6-in-progress', ['blocked'])[0].startsWith('blocked'));
assert.ok(rightsFor('S7-code-review', ['review-4'])[0].startsWith('review-4'));
assert.ok(rightsFor(null).some((l) => l.includes('вне процесса')));
assert.ok(rightsFor(null).some((l) => l.includes('продуктовая задача вне процесса')));
const infrastructure = rightsFor(null, ['infra']);
assert.ok(infrastructure.some((l) => l.includes('инфраструктурную реализацию МОЖНО')));
assert.ok(infrastructure.some((l) => l.includes('S7-code-review')));
assert.ok(infrastructure.every((l) => !l.includes('продуктовый код трогать МОЖНО')));
});
test('AC распознаются из таблицы ТЗ и из строк тела issue (#496)', () => {
@@ -90,6 +94,18 @@ test('пакет собирается и рендерится: статус, м
assert.match(renderPacket(buildPacket({ issue: { number: 1, title: 't', state: 'OPEN', body: '' }, labels: [] })), /материал не запушен/);
});
test('#562: statusless infra issue is the accelerated track ending at S7 review', () => {
const packet = buildPacket({
issue: { number: 562, title: 'process', state: 'OPEN', url: 'u', body: '' },
labels: ['P1', 'infra', 'process', 'tech-debt'],
});
assert.equal(packet.status, null);
assert.equal(packet.track, 'инфраструктурный');
const md = renderPacket(packet);
assert.match(md, /инфраструктурный вход/);
assert.match(md, /S7-code-review/);
});
test('#517 AC5: AC берутся из тела issue, файл ТЗ — только когда в теле их нет', () => {
const base = {
issue: { number: 700, title: 'x', state: 'OPEN', url: 'u', body: '## ТЗ\n\n- AC1. Из тела\n' },