local-fleet-install · git:20260924.2784cb2 · 2026-09-24 · sha256 5d373d3b736e68f2

local-fleet-install git:20260924.2784cb2A

Immutable. This exact content never changes and is served at /api/v1/blob/5d373d3b736e68f2.

---
name: local-fleet-install
description: >-
  Раскатать деталь (скрипт/движок/фикс) на КАЖДЫЙ узел флота одной командой и доказать применение
  ФАКТОМ, а не рассылкой. Делает на каждом узле: доставка по scp → сверка md5 → прогон теста →
  apply → машинный verify (exit 0) → запись в реестр `deploy_apply`. Недостижимые узлы не
  замалчивает: кладёт посылку в _transit, шлёт TASK по шине и называет отставших ПОИМЁННО.
  Триггеры: "/fleet-install", "/fi", "раскатай на флот", "разложи по машинам", "поставь всем",
  "доставь на все узлы", "раскатка", "fleet install", "roll out to fleet", а также САМ после
  сборки любой детали, которая нужна не только этой машине. Родился из приказа Антона 29.07:
  «разослали всем "почините у себя" и решили, что дело сделано. Никто не починил».
  Канон: CLAUDE.md §7.3-бис · память [[fleet-parity-board]], [[one-system-propagate]],
  [[deploy-three-layers-apply-gap]], [[delivery-claim-check-rail-first]].
---

# /fleet-install — раскатка, которая доказывает себя

> **Закон, ради которого скилл существует (§7.3-бис):** починка не существует, пока не стоит на
> КАЖДОМ узле-потребителе. Готово = применено везде И доказано чтением факта на каждом.
> **НЕ готово:** «отправил по шине» (доставка ≠ применение) · «предложил раскатку» (вопрос, не
> действие) · «узел молчит, значит применил» — ⛔ **тишина ≠ ок**: выключенный, отставший и
> исправный узлы молчат одинаково.

## Когда звать
Собрал деталь, которая нужна не только этой машине: скрипт, движок, робот, фикс конфига, гейт.
Зови СРАЗУ после `/tt` — пока деталь горячая и ты помнишь, чем её проверять.

Не зови для: правок канона (CLAUDE.md, Библия, скиллы — у них своя рельса через хаб, §7.9),
секретов (они не ездят), разовых экспериментов на одной машине.

## Шаг 0 — назови круг потребителей ДО раскатки
Кому деталь НЕ нужна — запиши явно с причиной (`NOTFORME: <узел> — <почему>`), молчание запрещено.
Пример: «Маяк headless → браузерная деталь ему не нужна».

⛔ **Гейт «запрет протух?»** — если маршрут доставки выбран из-за какого-то «нельзя» («шара receive-only», «нет доступа», «узел не примет») старше ~недели → сперва ОДИН живой запрос к системе-владельцу запрета (Syncthing REST `/rest/config/folders`, `gh api`, проба записи), потом стройка обходного маршрута. Замер 14.08: сессия на протухшем контексте обновляла давно потреблённую посылку хабу и затёрла живой канон-скилл — шара 2 недели как two-way. Канон: [[ban-is-a-claim-recheck-before-workaround]] + `reglament-prichina-takoy-zhe-claim-kak-vyvod` §Поправка.

## Шаг 1 — одна команда
```bash
python3 ~/.claude/skills/local-fleet-install/fleet_install.py \
  --id <deploy-id> \
  --file ~/.claude/scripts/<деталь>.py \
  --file ~/.claude/scripts/_test_<деталь>.py \
  --test  "{py} _test_<деталь>.py" \
  --apply "{py} {f} --install" \
  --verify "{py} {f} --verify"
```
`{f}` = имя первого файла · `{py}` = python3/python по ОС узла · `{dir}` = папка назначения.
Флаги: `--only NODE1,NODE2` (точечно) · `--dry-run` (показать цели) · `--dest-posix/--dest-win`.

Перед этим деталь должна быть зарегистрирована:
```bash
python3 ~/.claude/scripts/deploy_register.py all <deploy-id> "<описание>" "<apply>" "<verify>" --tier 1
```

## Шаг 2 — что скилл делает на каждом узле (и почему именно так)
| Шаг | Зачем |
|---|---|
| scp + сверка **md5** | доставка доказана хэшем, а не «scp вернул 0» |
| прогон **теста** | красный тест = узел НЕ считается применённым, деталь туда не встаёт |
| **apply** | обычно `--install`: деталь сама ставит расписание своей ОС |
| **verify** машинный | exit 0. Проза («вроде работает») не принимается — [[verify-the-replacement-not-the-symptom]] |
| **deploy_apply** | пишет DONE-маркер, иначе табло паритета не увидит факт (замер 29.07: узел применён, а табло показывает «нет») |

## Шаг 3 — отставшие называются поимённо
Недостижимые узлы получают: посылку в `_transit/DEPLOY-*`, `TASK:` по шине (с префиксом в НАЧАЛЕ,
иначе робот пира её не увидит — [[bus-send-robot-visible-prefix]]) и строку в отчёте.
Отчёт Антону обязан содержать список отставших. Формулировки «раскатано», «разослано» без таблицы
применения — запрещены.

## Шаг 4 — доказать табло
```bash
python3 ~/.claude/scripts/fleet_fix_audit.py --by-fix
```
⛔ **Считать отставание РАЗНИЦЕЙ, а не строками манифеста**
[[pending-manifest-line-count-is-not-backlog]]. `wc -l PENDING-<узел>.jsonl` = сколько узлу
КОГДА-ЛИБО адресовали (файл append-only, по DONE не чистится). Правда = уникальные `id`
минус `DONE-<узел>/*.ok` минус `NOTFORME-<узел>.jsonl`:
```bash
python3 ~/.claude/scripts/deploy_open_count.py <узел>     # печатает: lines / DONE / NOTFORME / открыто
```
Замер 10.08: по строкам у соседа выходило «171 непринятая», по разнице — **36**, и патруль
там стоял с 04.08. Цифру «строк в манифесте» в отчёт Антону не выносить вообще.
Пока в строке детали есть «не применён на: …» — вердикт `/tt` максимум ⚠️, не ✅.

## Грабли, за которые заплачено
- **`~/.claude/scripts` — receive-only шара `claude-home`.** Правка там наружу НЕ уезжает и
  сносится кнопкой «Revert Local Changes»; ошибки при записи НЕТ. Поэтому этот скилл возит файлы
  по scp, а не «положил и жду синка». Для канонных деталей — канон-предложение хабу.
- **Автоканала доставки `~/.claude/scripts` у пиров нет** (известная P0-дыра) — «доедет само» здесь
  ложь по умолчанию.
- **`ssh` есть не до всех**: узлы [коллега] и [коллега] часто офлайн → для них честный путь = посылка.
- **Пути в apply/verify — только относительные ({dir}-локальные).** `~/.claude/scripts/x.py` на
  Windows-узле НЕ раскрывается и склеивается с dest-каталогом в мусорный путь (замер 15.08:
  канарейка хаба упала `No such file or directory` на `laude\scripts\~\.claude\...`). Команда
  бежит В каталоге назначения — пиши `{py} deploy_payload_install.py <id>`, без `~` и без абс. путей.
- **Windows требует своего**: `python` вместо `python3`, `$LASTEXITCODE` вместо `$?`, вывод не в
  UTF-8. Скилл это уже учитывает.
- **ssh печатает свои баннеры в тот же поток, что и вывод команды.** Первый боевой прогон 29.07
  сравнил md5 с текстом `** The server may need to be upgraded` и объявил расхождение. Поэтому
  скилл ищет строку, которая ЯВЛЯЕТСЯ хэшем (32 hex), и строку с итогом теста, а не «последнюю».
  Общий урок: парся вывод по признаку, а не по позиции.
- **Тест детали может падать только на одном узле.** Тот же прогон дал зелёный Маяк и красный хаб:
  проверка использовала юниксовый `env -i`, которого нет на Windows. Красный тест на одном узле
  = деталь туда НЕ встаёт, и это правильно — чинится тест, а не обход проверки.

## Тест самого скилла
```bash
python3 ~/.claude/skills/local-fleet-install/_test_fleet_install.py
```
Офлайн, на фейковых узлах, ничего никуда не отправляет.

## Границы
Tier-2 не обходит (деньги/секреты/необратимое — к Антону). Канон-файлы не возит. Чужие детали на
чужих узлах молча не сносит.