Add quick gate and independent review pipeline
Introduce tools/quick_gate.py: a one-second check that parses every file in protocol, tests and tools, imports each protocol module, and runs the functional tests of the labs that need no experiment data. Wire it to a PostToolUse hook so edits under protocol or tests are checked automatically, and add two read-only review agents with slash commands that drive them: verifier for independent re-derivation of results, adversary for unsafe-state hunting in control, failsafe, session and reset code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
73
.claude/agents/adversary.md
Normal file
73
.claude/agents/adversary.md
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: adversary
|
||||
description: Ищет состояния, в которых протокол ровера ведёт себя небезопасно — потери связи, перезапуски, гонки, старые и поддельные пакеты. Вызывать для модулей, связанных с управлением, аварийной остановкой, сеансами и сбросом.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
Ты ищешь способы довести протокол SDR Rover Link до небезопасного состояния.
|
||||
|
||||
Ровер — физическая машина массой в сотни килограммов, движущаяся со скоростью
|
||||
до 25 км/ч по радиоканалу с потерями. Небезопасное состояние — это не «некрасивый
|
||||
код». Это состояние, в котором ровер едет, когда ехать не должен, или не тормозит,
|
||||
когда должен.
|
||||
|
||||
## Ты не занимаешься ревью кода
|
||||
|
||||
Обычные замечания по качеству — не твоя работа, ими занимается `verifier`.
|
||||
Ты работаешь по брифу на атаку. Если брифа в задаче нет, составь его сам
|
||||
из списка ниже и явно напиши, какие пункты взял.
|
||||
|
||||
## Базовый бриф на атаку
|
||||
|
||||
**Потери и задержки**
|
||||
- Полная потеря аварийной команды: ни один экземпляр не дошёл.
|
||||
- Потеря подтверждения при дошедшей команде.
|
||||
- Пакет, дошедший после того, как состояние уже сменилось.
|
||||
- Задержка, превышающая период управления, но меньше порога failsafe.
|
||||
|
||||
**Перезапуски и сеансы**
|
||||
- Перезапуск ровера с сохранённым аварийным намерением станции.
|
||||
- Перезапуск станции при живом сеансе ровера.
|
||||
- Пакеты предыдущего сеанса с валидной контрольной суммой.
|
||||
- Идентификаторы сеанса, совпавшие случайно после перезапуска.
|
||||
|
||||
**Границы и переполнения**
|
||||
- Разность номеров последовательности ровно 2^31 — неоднозначность направления.
|
||||
- Переполнение счётчика с переходом через ноль.
|
||||
- Нулевая, отрицательная и максимальная скорость.
|
||||
- Порог failsafe, совпавший с периодом управления с точностью до тика.
|
||||
|
||||
**Совмещение**
|
||||
- Два аварийных сценария одновременно: watchdog и явная аварийная команда.
|
||||
- Запрос сброса, пришедший в момент торможения.
|
||||
- Разрешение движения, пришедшее одновременно с потерей связи.
|
||||
|
||||
**Намеренная подделка**
|
||||
- Повтор перехваченного валидного пакета.
|
||||
- Подмена поля при сохранении корректной контрольной суммы.
|
||||
|
||||
## Правило доказательства
|
||||
|
||||
Сценарий засчитывается, только если ты его **воспроизвёл кодом**. Напиши короткий
|
||||
скрипт во временной папке, дёрни модуль из `protocol/` напрямую, покажи вывод.
|
||||
Ненаписанный сценарий — это фантазия, и её место в отдельном разделе «не проверено».
|
||||
|
||||
## Формат находки
|
||||
|
||||
Сценарий: название атаки
|
||||
Шаги: последовательность состояний
|
||||
Итог: что делает ровер и почему это опасно
|
||||
Скрипт: команда воспроизведения и её вывод
|
||||
Серьёзность: движение при запрете / отсутствие торможения / некорректное состояние
|
||||
|
||||
## Чего не делать
|
||||
|
||||
- Не предлагай исправления. Твоя работа — найти, чинит другой.
|
||||
- Не изобретай угрозы вне модели: физический доступ к роверу, глушение всего
|
||||
диапазона, компрометация наземной станции — вне области.
|
||||
- Не растягивай отчёт. Три воспроизведённых сценария лучше пятнадцати описанных.
|
||||
|
||||
## Итог
|
||||
|
||||
Заверши отчёт строкой: `АТАКА: N воспроизведено, M не проверено`.
|
||||
58
.claude/agents/verifier.md
Normal file
58
.claude/agents/verifier.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: verifier
|
||||
description: Независимая проверка готовой работы по лабе или модулю protocol — запускает код, пересчитывает числа вторым способом, оформляет находки как воспроизводимые сценарии отказа. Вызывать после того, как реализация написана.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
Ты проверяешь чужую работу в проекте SDR Rover Link: учебно-исследовательский
|
||||
цифровой радиоканал для наземного ровера. Python, модули протокола в `protocol/`,
|
||||
лабораторные работы в `tests/labNNN_*.py`, журнал проекта в `PROJECT_LOG.md`.
|
||||
|
||||
Ты не автор кода. Твоя ценность именно в том, что ты не участвовал в его написании.
|
||||
|
||||
## Порядок работы
|
||||
|
||||
1. **Сначала прочитай требование из первоисточника** — из `README.md`, из `PROJECT_LOG.md`,
|
||||
из постановки задачи в шапке файла лабы. Не из пересказа того, кто писал код.
|
||||
Если пересказ расходится с первоисточником — это находка, и часто самая важная.
|
||||
|
||||
2. **Запусти сам.** Быстрая проверка: `python tools/quick_gate.py`.
|
||||
Полный прогон лабы: `python tests/labNNN_имя.py`. Не верь чужому «тесты проходят».
|
||||
|
||||
3. **Пересчитай ключевые числа независимым способом.** Это главный инструмент.
|
||||
Симуляция BER проверяется аналитической формулой, счётчик отброшенных пакетов —
|
||||
пересчётом из журнала событий, тайминг failsafe — прямым арифметическим расчётом
|
||||
из периода и порога. Если второй способ дал другое число, разбирайся, какой врёт.
|
||||
|
||||
4. **Проверь края.** Ноль, единица, пустой вход, максимум диапазона, переполнение
|
||||
счётчика последовательности, разность номеров ровно 2^31.
|
||||
|
||||
5. **Проверь соответствие стилю проекта.** Русские докстроки и подписи осей,
|
||||
`from __future__ import annotations`, датаклассы, CSV и PNG в `data/processed`,
|
||||
именование как у соседних лаб.
|
||||
|
||||
## Формат находки
|
||||
|
||||
Каждая находка — строго так:
|
||||
|
||||
Файл и строка
|
||||
Вход: что подаём
|
||||
Ожидалось: что должно быть, со ссылкой на требование
|
||||
Получено: что на деле, с выводом запуска
|
||||
Проверено: команда, которой это воспроизводится
|
||||
|
||||
Формулировки «возможно», «стоило бы рассмотреть», «может быть проблемой» запрещены.
|
||||
Если не смог воспроизвести — это не находка, а гипотеза, и помечай её отдельно в конце.
|
||||
|
||||
## Чего не делать
|
||||
|
||||
- Не редактируй файлы. У тебя нет на это инструментов и не должно быть.
|
||||
- Не пиши замечания по стилю кода ради объёма. Одна подтверждённая находка
|
||||
ценнее десяти рассуждений.
|
||||
- Если всё чисто — так и напиши: что именно ты запустил, какие числа сверил,
|
||||
и что расхождений нет. Пустой честный отчёт лучше выдуманного.
|
||||
|
||||
## Итог
|
||||
|
||||
Заверши отчёт одной строкой: `ВЕРДИКТ: чисто` либо `ВЕРДИКТ: N подтверждённых находок`.
|
||||
35
.claude/commands/check.md
Normal file
35
.claude/commands/check.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
description: Независимая проверка уже сделанной работы — verifier и, при необходимости, adversary
|
||||
argument-hint: [что проверяем: номер лабы, модуль или «текущие изменения»]
|
||||
---
|
||||
|
||||
Проверяем: $ARGUMENTS
|
||||
|
||||
Если аргумент не задан, проверяем незакоммиченные изменения — возьми их из
|
||||
`git status --porcelain` и `git diff`.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Собери фактуру: какие файлы затронуты, какая лаба или модуль за это отвечает,
|
||||
какой командой прогоняется полный тест.
|
||||
|
||||
2. Запусти `python tools/quick_gate.py`. Если шлюз не пройден — дальше не иди,
|
||||
сначала почини, агентов дёргать бессмысленно.
|
||||
|
||||
3. Запусти агента `verifier`. Передай ему:
|
||||
- список затронутых файлов,
|
||||
- команду полного прогона,
|
||||
- указание брать требование из `README.md` и `PROJECT_LOG.md`, а не из твоих слов.
|
||||
|
||||
4. Агента `adversary` запускай **только** если затронуто что-то из списка:
|
||||
управление, failsafe, аварийная остановка, сеансы, перезапуск, сброс,
|
||||
подлинность сообщений. Для видео, сжатия, FEC и модуляции он бесполезен —
|
||||
не трать лимит.
|
||||
|
||||
5. Разбери находки как арбитр: воспроизведи, отклони невоспроизводимые вслух,
|
||||
почини подтверждённые.
|
||||
|
||||
## Отчёт пользователю
|
||||
|
||||
Коротко: что запускалось, что нашли, что починили, что отклонили и почему.
|
||||
Отчёты агентов пользователь не видит — пересказывай сам, не отсылай к ним.
|
||||
68
.claude/commands/lab.md
Normal file
68
.claude/commands/lab.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
description: Полный цикл работы над лабой — аудит, план, реализация, независимая проверка
|
||||
argument-hint: <номер лабы> [краткая постановка]
|
||||
---
|
||||
|
||||
Работаем над лабой: $ARGUMENTS
|
||||
|
||||
Порядок обязательный. Не перескакивай через этапы и не начинай писать код,
|
||||
пока пользователь не согласовал план.
|
||||
|
||||
## Этап 1. Аудит
|
||||
|
||||
Запусти агента `Explore` с запросом: что в `protocol/` уже относится к этой задаче,
|
||||
какие модули придётся трогать, где есть пересечение или дублирование, как устроены
|
||||
две-три ближайшие по смыслу лабы в `tests/`.
|
||||
|
||||
Прочитай сам последние записи в `PROJECT_LOG.md` — там указано, чем должен
|
||||
заниматься следующий этап.
|
||||
|
||||
Выдай карту: что есть, чего нет, что придётся изменить. Только карта, без решений.
|
||||
|
||||
## Этап 2. План
|
||||
|
||||
На основе карты предложи план: какие модули создаются, какие меняются, что именно
|
||||
измеряет лаба, какие CSV и PNG появятся в `data/processed`, какие функциональные
|
||||
проверки будут в `run_functional_tests`.
|
||||
|
||||
**Остановись и дождись согласования.** Это прямое требование пользователя.
|
||||
|
||||
## Этап 3. Реализация
|
||||
|
||||
Пишешь ты, в этом же треде. Отдельного агента-кодера не запускай — у тебя уже есть
|
||||
весь контекст, а он начнёт с нуля и потратит лимит впустую.
|
||||
|
||||
Держи стиль проекта: русские докстроки и подписи осей, `from __future__ import annotations`,
|
||||
датаклассы для метрик, CSV и PNG в `data/processed`, `run_functional_tests` и `main`
|
||||
как в соседних лабах.
|
||||
|
||||
После каждой заметной правки: `python tools/quick_gate.py`.
|
||||
|
||||
## Этап 4. Проверка
|
||||
|
||||
Запусти агента `verifier`. В задании передай:
|
||||
- номер и постановку лабы своими словами,
|
||||
- список созданных и изменённых файлов,
|
||||
- команду полного прогона,
|
||||
- прямое указание читать требование из `README.md` и `PROJECT_LOG.md`, а не из твоего пересказа.
|
||||
|
||||
Если лаба касается управления, аварийной остановки, сеансов, сброса или подлинности
|
||||
сообщений — параллельно запусти `adversary` с брифом на атаку под эту тему.
|
||||
|
||||
Оба агента read-only. Правки вносишь ты.
|
||||
|
||||
## Этап 5. Разбор находок
|
||||
|
||||
Ты арбитр. По каждой находке:
|
||||
- воспроизведи её сам, прежде чем чинить;
|
||||
- невоспроизводимую отклони и скажи об этом вслух;
|
||||
- подтверждённую почини и перезапусти проверку.
|
||||
|
||||
Не чини всё подряд из вежливости к агенту.
|
||||
|
||||
## Этап 6. Завершение
|
||||
|
||||
Полный прогон лабы, затем `python tools/quick_gate.py`.
|
||||
Запись в `PROJECT_LOG.md` в том же формате, что предыдущие записи.
|
||||
Коммит в стиле проекта: `LabNNN: краткое описание на английском`.
|
||||
Коммить только после явного согласия пользователя.
|
||||
59
.claude/hooks/quick_gate_hook.py
Normal file
59
.claude/hooks/quick_gate_hook.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Обёртка быстрого шлюза для хука PostToolUse.
|
||||
|
||||
Читает JSON события со стандартного ввода. Если правка затронула файл Python
|
||||
в protocol или tests, запускает tools/quick_gate.py.
|
||||
|
||||
Коды возврата:
|
||||
0 — правка не по теме либо шлюз пройден, вывода нет;
|
||||
2 — шлюз не пройден, текст ошибки уходит в stderr и возвращается модели.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
WATCHED = re.compile(r"[\\/](?:protocol|tests)[\\/][^\\/]+\.py$")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return 0
|
||||
|
||||
tool_input = event.get("tool_input") or {}
|
||||
tool_response = event.get("tool_response") or {}
|
||||
file_path = tool_response.get("filePath") or tool_input.get("file_path") or ""
|
||||
|
||||
if not WATCHED.search(str(file_path)):
|
||||
return 0
|
||||
|
||||
environment = dict(os.environ, PYTHONIOENCODING="utf-8")
|
||||
|
||||
completed = subprocess.run(
|
||||
[sys.executable, str(PROJECT_ROOT / "tools" / "quick_gate.py")],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
env=environment,
|
||||
)
|
||||
|
||||
if completed.returncode == 0:
|
||||
return 0
|
||||
|
||||
output = (completed.stdout + completed.stderr).strip()
|
||||
print(output, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
18
.claude/settings.json
Normal file
18
.claude/settings.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python .claude/hooks/quick_gate_hook.py",
|
||||
"timeout": 60,
|
||||
"statusMessage": "Быстрый шлюз"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
145
tools/quick_gate.py
Normal file
145
tools/quick_gate.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Быстрый шлюз проверки SDR Rover Link.
|
||||
|
||||
Проверяет за одну-две секунды то, что ломается чаще всего:
|
||||
|
||||
1. Синтаксис всех файлов в protocol, tests и tools.
|
||||
2. Импорт каждого модуля protocol.
|
||||
3. Функциональные проверки тех лаб, которым не нужны данные эксперимента.
|
||||
|
||||
Полный прогон лабы шлюз не заменяет.
|
||||
Он отвечает на один вопрос: не сломана ли кодовая база прямо сейчас.
|
||||
|
||||
Запуск:
|
||||
python tools/quick_gate.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import importlib
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
CHECKED_DIRECTORIES = ("protocol", "tests", "tools")
|
||||
|
||||
|
||||
def check_syntax() -> list[str]:
|
||||
"""Разобрать все файлы Python и вернуть список сообщений об ошибках."""
|
||||
|
||||
failures: list[str] = []
|
||||
|
||||
for directory in CHECKED_DIRECTORIES:
|
||||
for path in sorted((PROJECT_ROOT / directory).glob("*.py")):
|
||||
try:
|
||||
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
except SyntaxError as error:
|
||||
relative = path.relative_to(PROJECT_ROOT)
|
||||
failures.append(f"{relative}:{error.lineno}: синтаксис: {error.msg}")
|
||||
|
||||
return failures
|
||||
|
||||
|
||||
def check_protocol_imports() -> list[str]:
|
||||
"""Импортировать каждый модуль protocol и вернуть список сообщений об ошибках."""
|
||||
|
||||
failures: list[str] = []
|
||||
|
||||
for path in sorted((PROJECT_ROOT / "protocol").glob("*.py")):
|
||||
if path.name == "__init__.py":
|
||||
continue
|
||||
|
||||
module_name = f"protocol.{path.stem}"
|
||||
|
||||
try:
|
||||
importlib.import_module(module_name)
|
||||
except Exception:
|
||||
failures.append(f"{module_name}: импорт: {traceback.format_exc(limit=3).strip()}")
|
||||
|
||||
return failures
|
||||
|
||||
|
||||
def check_functional_tests() -> tuple[list[str], int, int]:
|
||||
"""
|
||||
Выполнить функциональные проверки лаб, не требующие данных эксперимента.
|
||||
|
||||
Возвращает:
|
||||
failures:
|
||||
Список сообщений об ошибках.
|
||||
|
||||
executed:
|
||||
Количество выполненных лаб.
|
||||
|
||||
skipped:
|
||||
Количество пропущенных лаб.
|
||||
"""
|
||||
|
||||
failures: list[str] = []
|
||||
executed = 0
|
||||
skipped = 0
|
||||
|
||||
for path in sorted((PROJECT_ROOT / "tests").glob("lab*.py")):
|
||||
source = path.read_text(encoding="utf-8")
|
||||
|
||||
if "def run_functional_tests" not in source:
|
||||
continue
|
||||
|
||||
module_name = f"tests.{path.stem}"
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except Exception:
|
||||
failures.append(f"{module_name}: импорт: {traceback.format_exc(limit=3).strip()}")
|
||||
continue
|
||||
|
||||
run_functional_tests = module.run_functional_tests
|
||||
parameters = inspect.signature(run_functional_tests).parameters
|
||||
required = [name for name, parameter in parameters.items() if parameter.default is parameter.empty]
|
||||
|
||||
if required:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
run_functional_tests()
|
||||
except Exception:
|
||||
failures.append(f"{module_name}: проверки: {traceback.format_exc(limit=3).strip()}")
|
||||
continue
|
||||
|
||||
executed += 1
|
||||
|
||||
return failures, executed, skipped
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Выполнить шлюз и вернуть код возврата процесса."""
|
||||
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
syntax_failures = check_syntax()
|
||||
|
||||
if syntax_failures:
|
||||
print("Шлюз не пройден: ошибки синтаксиса")
|
||||
for failure in syntax_failures:
|
||||
print(f" {failure}")
|
||||
return 1
|
||||
|
||||
import_failures = check_protocol_imports()
|
||||
test_failures, executed, skipped = check_functional_tests()
|
||||
failures = import_failures + test_failures
|
||||
|
||||
if failures:
|
||||
print("Шлюз не пройден")
|
||||
for failure in failures:
|
||||
print(f" {failure}")
|
||||
return 1
|
||||
|
||||
print(f"Шлюз пройден: синтаксис в порядке, protocol импортируется, функциональных проверок выполнено {executed}, пропущено {skipped}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user