Prompt file imported from nv-lang/nova (
.claude/commands/commit-push.md). Fill in{{arguments}}before use. Copyright stays with the author.
КОМУ ЭТА КОМАНДА: любому окну, которое коммитит. Пуш в main — только интегратору; окно со своей веткой берёт отсюда порядок коммита и слияния с main.
Реши, что из трёх — коммит, пуш, синхронизация с main — сейчас ВОЗМОЖНО и УМЕСТНО, и
сделай ровно это: {{arguments}}
Правила — из AGENTS.md (Git) и docs/dev/rules-for-agents.md; здесь — порядок решения
и команды целиком. Ветка — своя (main принадлежит интегратору). Команда ничего не
делает «на всякий случай»: каждое из трёх действий сначала получает вердикт, и в докладе
стоит вердикт по каждому, даже если действие не понадобилось.
Шаг 0. Вердикты — по фактам дерева, не по памяти
Запусти bash scripts/tools/git-readiness.sh и прочитай три блока: COMMIT, PUSH,
SYNC — «ВОЗМОЖЕН / НЕВОЗМОЖЕН: причина / НЕЧЕГО / НУЖЕН / НЕ НУЖЕН» со строками фактов
(незавершённое слияние, маркеры конфликта, изменённые и новые файлы, чужой индекс, автор
непушенных, возраст последнего вердикта гейта относительно HEAD, отставание от
origin/main, сдвиг стражей/спеки/оракула в main). «Невозможен» — не спорить с
причиной, а устранить её или остановиться и сказать. Дальше — уместность, которую скрипт
не решает:
-
Коммит уместен, когда единица работы ЗАКРЫТА и это доказано (тест, страж, фикстура, строка вердикта — назови её), и правка статуса плана едет тем же коммитом. Не уместен: посреди правки («половина», красные тесты), «чтобы сохранить» — для этого есть worktree, коммитов-заготовок не делаем; чужие правки и черновики «по слову» — не твои. Незакрытая работа при передаче окна не коммитится, а описывается в передаче.
-
«ВПЕРЕДИ НА N» БЕЗ НАЗВАННОЙ ТОЧКИ ОТСЧЁТА — НЕ ФАКТ, и у окна со своей веткой точек ДВЕ, отвечающих на РАЗНЫЕ вопросы (замер окна Карины, 2026-09-19).
origin/main..HEADотвечает «сколько мне сливать» — это готовность.@{u}..HEADотвечает «сколько я потеряю, если умрёт диск» — это СОХРАННОСТЬ. Той ночью первое давало 98, второе 312, и оба были истинны; окно всю смену докладывало 98, а тревожиться надо было о 312, и увидел это владелец по панели редактора, а не по докладу. Следствие, не пожелание: решая, пора ли пушить СВОЮ ветку, смотри@{u};mainк сохранности отношения не имеет. Копится пуш Вmain(ниже) — сохранность своей ветки не копится никогда. -
ПУШ КОПИТСЯ, КОММИТЫ — НЕТ (владелец, 2026-09-03: «реже пушить, каждый пуш стоит полного гейта»). Коммит по-прежнему на каждую закрытую единицу работы — дробность истории не трогаем; копится ПУШ, потому что гейт висит именно на нём и занимает машину на 20–40 минут. Норма: несколько закрытых волн → ОДИН гейт → зелёный → пуш немедля. Гнать раньше — только если (а) ветку ждёт другое окно и сказало об этом, (б) правка язык-меняющая и нужна интегратору сегодня, (в) накопилось столько, что разбор возможного отказа станет дороже лишнего прогона. Замер, из которого правило выросло: за смену 2026-09-03 гейт прогнан ВОСЕМЬ раз, и пять падений были не по коду (протухший оракул, долг охоты, бюджет времени, неполный локальный набор стражей, чужая замершая ветка) — платой за привычку «волна → гейт → пуш».
-
Пуш уместен, когда гейт нужного яруса зелёный на ЭТОМ HEAD (вердикт моложе HEAD — скрипт это говорит) и волна или веха закрыта в плане. Не уместен: гейт устарел или красен (чинить причину, не базу); в реестре строка без номера (гейт формы красен — номер даёт интегратор до гейта); слияние меняет язык, а D-амендмента в нём нет; в
mainдвинули стражей или спеку и синк ещё не сделан — локальный гейт судит не тем, чем CI. -
Синк уместен: перед началом новой волны; когда
mainдвинул стражей, гейт или спеку (иначе зелёный локально — красный на CI); по слову владельца («обновись» — влитьorigin/mainк себе; «синканись» — двусторонне, второй половиной занимается интегратор). Не уместен: посреди волны на грязном дереве (сначала коммит или решение, что не едет); когда отставания нет. После синка, еслиmainдвинул источники оракула, — пересборкаnova-cliи оболочкиnovacДО гейта.
Аргумент «только синк» или «только пуш» сужает действие, но вердикты печатаются все три.
Шаг 1. Коммит
- Что едет.
git status --shortиgit diff --stat. Раздели изменения на коммиты по природе работы, а не по времени написания: отдельно — тип работ (правка компилятора, документ, инструмент, страж, тест), отдельно — каждый пункт или шаг плана, отдельно — каждый дефект (строка реестра и её фикс — один коммит), отдельно — каждая задача владельца. Разноплановое в один большой коммит не соединять: такой коммит нельзя ни прочитать по заголовку, ни откатить по частям. Исключение — план: документ плана едет целиком одним коммитом (он читается как одно решение), а подплан (NNN.N-…) можно отдельным. Правка плана, сопровождающая шаг кода (статус, живая строка, критерии), едет вместе с этим шагом, а не отдельно. Файлы, о которых владелец не говорил (чужие правки, черновики «по слову»), в коммит не входят — назови их в докладе как оставленные. - Новые файлы —
git add <имя>по одному (никогда-A,.,-u). Файл, который игнорирует.gitignore,git addотклонит — это не повод для-f: реши, где ему место, или добавь исключение в.gitignoreс причиной. - Проверь индекс перед коммитом:
git diff --cached --stat— в нём только твои файлы этой задачи; чужое в индексе общего дерева — не твоё,--onlyего не возьмёт, но увидеть надо. - Сообщение — по-английски, файлом по длинному пути (не через
B7E3~1):target/msg-<тема>.txt. Заголовок ≤ 72 знака в форметип(область): что изменилось; тело — почему и чем доказано (строка вердикта, тест, замер). Для правокnovac— строкаDonor:(законный донор или «none — потому что…»; оракул донором не бывает, стражcheck-novac-commit-donorоткажет). - Коммит с областью и грепом маркеров конфликта В ОДНОЙ КОМАНДЕ:
grep -rn -E '^(<<<<<<<|=======|>>>>>>>)' -- <файлы> && echo "conflict markers" && exit 1; git commit -s -F target/msg-<тема>.txt --only -- <файл1> <файл2>Флаги — до--, пути — после (хукguard-git.pyоткажет иначе). Проверь, что коммит создался:git log -1 --format='%h %s'— «Everything up-to-date» при пуше означает, что коммита не было.
Шаг 2. Синхронизация с main (когда вердикт — НУЖЕН и уместен)
Только на чистом дереве. git fetch origin && git merge --no-ff origin/main. Конфликты —
руками, по смыслу файла: реестр 221.1 — объединение строк по номеру, в общей строке
побеждает main; базы стражей — большее из двух с причиной; документы — обе правки, если
не противоречат. Затем в ОДНОЙ команде: греп маркеров по конфликтным файлам → git commit -s -F target/msg-merge.txt с комментарием # index-verified: merge origin/main, конфликты разрешены руками (хук требует явного слова, потому что коммит слияния берёт весь индекс).
Если main двинул источники оракула — пересобрать nova-cli и оболочку novac
(scripts/tools/novac-regen-shell.sh) до гейта. Слияние — отдельный коммит, не смешивать
с работой.
СИНК НУЖЕН И ПОСЛЕ КАЖДОГО КОММИТА ИНТЕГРАТОРА В main — НЕ ПОСЛЕ ПУША, а сразу
после слияния, — И НЕ ТОЛЬКО КОГДА СВОЯ ВЕТКА ЭТОГО ПОПРОСИЛА — из-за №TBD (владелец,
2026-09-22, замер той же ночи; уточнение владельца тем же вечером — ждать пуша не нужно
вовсе). Номер строки реестра даёт ТОЛЬКО интегратор, и делает это ПРИ слиянии твоей
ветки — до этого момента у тебя в дереве стоит №TBD. Если после слияния ты продолжаешь
коммитить на СТАРОЙ базе, не подтянув main к себе, следующая твоя пачка снова несёт
СТАРУЮ находку с новым №TBD — та же строка, только уже пронумерованная у интегратора и
заново безымянная у тебя. За ночь 2026-09-21/22 это случилось трижды: интегратор вручную
сверял байт-в-байт, дубликат это или нет, прежде чем отбросить чужую копию.
ПОЧЕМУ НЕ ЖДАТЬ ПУША: воркtree делят ОДИН .git. Коммит интегратора в main виден
всем соседним ворктри СРАЗУ через git merge main (ветку своего же общего репозитория,
не origin/main) — ни fetch, ни тем более пуск в удалённые зеркала для этого не нужен.
Ждать пуша значило бы держать окно неведущим номер дольше, чем физически необходимо.
Следствие: синк с main — не только когда main двинул стражей/гейт/спеку (случай
выше), но и КАЖДЫЙ РАЗ, когда интегратор слил твою предыдущую пачку (видно по git log main в общем .git, даже без пуша), — прежде чем писать НОВЫЕ строки с №TBD. Тогда
номер, уже присвоенный интегратором, придёт к тебе слиянием сам, и дубликат физически
не возникнет.
Шаг 3. Пуш
КТО ПУШИТ main И КАК ЧАСТО — стоячее слово владельца 2026-09-16: «ты пушить сам
можешь, только не делай это часто, т.к. частый гейт сильно тормозит весь процесс».
Адресовано ИНТЕГРАТОРУ: ему не нужно спрашивать разрешения на каждый пуш main.
Окно на своей ветке пушит свою ветку, как и раньше.
Ограничение важнее разрешения, и причина у него измеримая: цена пуша — не git push, а ГЕЙТ перед ним, занимающий машину на десятки минут; машина одна на все окна.
Частота пушей = частота гейтов, и режется она группировкой, а не аккуратностью. Норма:
несколько закрытых волн → ОДИН гейт → пуш немедля по зелёному, и в этот пуш идёт
ВСЁ готовое, а не один коммит. Раньше нормы — когда ждать дороже: работа блокирует
чужое окно, пакет нужен потребителю по тегу, дерево держит слишком много несохранённого.
ПОЛНЫЙ ГЕЙТ — НЕ ЧАЩЕ РАЗА В СУТКИ. Число владельца, 2026-09-17 вечером, и
оно появилось после ЗАМЕРА, а не из общих соображений: за те сутки интегратор
прогнал ярус push ЧЕТЫРЕЖДЫ (01:00, 09:44, 10:07, 22:58), каждый раз занимая
единственную машину на 23–30 минут, и окно Карины всё это время стояло. Причина
у всех четырёх была одна и неуважительная: накопились правки компилятора, и
каждый раз казалось, что «этот код гейт ещё не видел». Ровно для этого и
существует накопление, описанное абзацем ниже.
Практическое следствие, чтобы правило не осталось пожеланием: день без полного гейта — норма, а не задержка. Если работа готова, а суточный прогон уже был — она ждёт следующего, и это дешевле, чем отнимать полчаса у соседнего окна. Раньше срока полный ярус законен только там, где ждать дороже: работа блокирует чужое окно, пакет нужен потребителю по тегу, дерево держит слишком много несохранённого.
РЕДКО ЗАПУСКАЕТСЯ НЕ ПУШ, А САМ ГЕЙТ — требование владельца 2026-09-17, и оно ШИРЕ правила выше. Абзац про группировку говорит о пушах; это — обо ВСЯКОМ тяжёлом прогоне, включая те, что пушем не кончаются: проверка своей правки, дифференциал, «прогоню на всякий случай». Гейт идёт десятки минут на ОДНОЙ машине, которую делят все окна, поэтому лишний прогон отнимает время не у себя, а у соседа.
Что делать вместо гейта, по возрастанию цены — правило выбора, а не
пожелание: (1) свой страж точечно (python scripts/guards/<имя>.py .) —
секунды; (2) ОДНА фикстура (nova test spec_tests --filter <имя>) — минуты;
(3) ярус loop — когда правка в стражах или планах; (4) полный ярус push —
только перед пушем и только когда накопилось несколько закрытых волн.
Замер этой ночи: два отказа CI были ДОКУМЕНТНЫМИ (леджер времени и живая
строка плана), и оба нашлись двумя точечными запусками стражей за секунды —
полный гейт ради них был бы потерей получаса.
Между окнами гейт согласуется, а не запускается молча: кто стартовал первым — тот держит машину; заявка называет ВРЕМЯ СТАРТА и ожидаемую длительность (иначе две заявки, разминувшиеся в пути, нельзя сравнить); занятость проверяется ЗАМЕРОМ, а не ответом одного соседа — «свободно» от одного окна значит «свободно ОТ МЕНЯ». Дом этого правила — план 291 Ф.4, здесь строка, потому что ритм задаёт эта команда.
ОДНОГО ps НЕДОСТАТОЧНО, и это замер, а не осторожность (окно Карины,
2026-09-19). Она посмотрела список процессов, не увидела ничего тяжёлого и
заключила, что прогон снят таймаутом, — а он шёл и через пять минут сел
зелёным. Причина в устройстве наших прогонов: у корпусного прохода CPU 15 с у
novac и 101 с у оракула при СТЕНЕ 1595 с, то есть большую часть времени ни
одного долгоживущего процесса не видно. Снимок ps попадает в промежуток
чаще, чем в работу.
Живость фона читается РОСТОМ ФАЙЛА ВЫВОДА и статусом задачи, а не одним
снимком списка процессов. ps годится, чтобы УВИДЕТЬ чужой прогон, и не
годится, чтобы заключить, что его НЕТ: у него ложное «свободно», а не ложное
«занято».
Гейт и пуш — одной командой: bash scripts/tools/push-after-gate.sh. Она сама откажет
на грязном дереве и на main, проверит автора непушенных коммитов, выберет ярус по путям
(novac/ → NOVAC_TIER=push и основной loop; иначе только основной loop), запушит с
повтором и напечатает unpushed: N. Красный гейт — чинить причину, не базу; пуш без
гейта — только GATE=none GATE_SKIP_REASON="…", и причина идёт в доклад.
ПЕРВЫЙ пуш ветки скрипт ДЕЛАЕТ НЕ САМ (замер 2026-09-07, окно 283). Если
origin/<ветка> ещё нет, он отказывает и печатает команду: git push -u origin <ветка>. Это не сбой — у первого пуша есть побочный эффект (создать ветку на
удалённом и завести upstream), и скрипт его не берёт на себя. Записано здесь,
потому что расхождение бьёт ровно по тому, кто пушит ВПЕРВЫЕ: совет «просто
запусти скрипт» давался даже интегратором, который этого не знал. На втором и
дальнейших пушах той же ветки скрипт работает целиком.
ЯРУС СЧИТАЕТСЯ ПО ПУТЯМ НЕПУШЕННЫХ КОММИТОВ, А СЛИЯНИЕ ТЯНЕТ ЧУЖИЕ ПУТИ
(замер 2026-09-08, окно 283). Первый пуш ПОСЛЕ синка с main стоит полного яруса,
сколько бы СВОЕГО в нём ни было: коммит слияния приносит чужие .rs/.c, а
правило выбора выше говорит «исходники вне novac → push». Замеренный случай: ветка
держала РОВНО ОДИН коммит сверх main — само слияние, а git diff --stat main...HEAD
был ПУСТ: уникального содержимого ноль, а цена пуша — 20-40 минут машины.
Что делать: ПЕРЕД пушом спросить git diff --stat main...HEAD и
git rev-list --count main..HEAD. Если дифф пуст, пушать НЕЧЕГО — работа уже в main,
а страж check-novac-local-only-work судит НАЛИЧИЕ копии на origin, а не её свежесть
(это названо в его собственной шапке как слепая зона). Пуш ради формы — трата машины,
а не аккуратность.
Шаг 4. Доклад
Три вердикта шага 0 и что по каждому сделано; хеши и заголовки коммитов; вердикт каждого
яруса гейта с временем; unpushed: 0; что осталось незакоммиченным и почему.
Чего команда не делает: не сливает в main, не пушит --force, не трогает
git config user.*, не добавляет Co-Authored-By, не делает коммитов-заготовок, не пушит
слияние, меняющее язык, без спек-амендмента в том же слиянии.
