diff --git a/AGENTS.md b/AGENTS.md index 8548ab01..baf20b83 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/PROCESS.md b/PROCESS.md index 6209d542..9c51dd48 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -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/-`, без аналитики, ТЗ, ревью ТЗ и статусов `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 минут. Поставив такую метку, автор не заканчивает работу, а ждёт смены метки опросом и продолжает по тому, чем она стала. diff --git a/scripts/process-gate.mjs b/scripts/process-gate.mjs index bb48edb4..623a21ea 100644 --- a/scripts/process-gate.mjs +++ b/scripts/process-gate.mjs @@ -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 })); diff --git a/scripts/task-packet.mjs b/scripts/task-packet.mjs index 4570cb22..5c138494 100644 --- a/scripts/task-packet.mjs +++ b/scripts/task-packet.mjs @@ -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); архивный файл — источник только у задач до diff --git a/test/process-gate.test.mjs b/test/process-gate.test.mjs index 6e62bc26..05e0357f 100644 --- a/test/process-gate.test.mjs +++ b/test/process-gate.test.mjs @@ -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: ' diff --git a/test/task-packet.test.mjs b/test/task-packet.test.mjs index ec25bdc4..14812341 100644 --- a/test/task-packet.test.mjs +++ b/test/task-packet.test.mjs @@ -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' },