| name | sync |
| description | Rebuild PROJECT_STATE derived fields from artifact files |
/polisade:sync — Sync State from Artifact Files
Сканирует файлы артефактов, пересобирает derived-поля в PROJECT_STATE.json (readyToWork, inProgress, blocked, waitingForPM, inReview, artifactIndex).
Использование
/polisade:sync # Показать diff (dry-run по умолчанию)
/polisade:sync --apply # Показать diff и записать после подтверждения
Алгоритм
- Определить корень проекта.
- Запустить скрипт в режиме dry-run:
python3 {plugin_root}/scripts/polisade_sync.py {project_root}
- Распарсить JSON-ответ, показать diff пользователю.
- Если пользователь подтверждает — применить:
python3 {plugin_root}/scripts/polisade_sync.py {project_root} --apply --yes
Для неинтерактивного использования (CI, pipe):
python3 {plugin_root}/scripts/polisade_sync.py {project_root} --apply --yes
Формат вывода
polisade_sync.py всегда печатает один JSON-документ на stdout
(контракт OPS-108 — json.loads(stdout) обязан проходить). PM-friendly
сообщения и подтверждение интерактивного prompt'а уходят на stderr.
Полная таблица контрактов — docs/config-reference.md § Script JSON
output contracts.
Если всё синхронизировано
{
"status": "in_sync",
"artifacts_scanned": 12,
"touched_paths": [],
"stage_paths": []
}
Если обнаружен drift (dry-run)
{
"status": "drift_detected",
"artifacts_scanned": 12,
"changes": [
{"field": "readyToWork", "added": ["TASK-005"], "removed": ["TASK-003"]},
{"field": "inProgress", "added": ["TASK-003"]},
{"field": "artifactIndex", "added": ["TASK-005"], "changed": ["TASK-003"]}
],
"touched_paths": [".state/PROJECT_STATE.json", ".state/counters.json"],
"stage_paths": [".state/PROJECT_STATE.json", ".state/counters.json"],
"dry_run": true
}
После --apply --yes
{
"status": "applied",
"artifacts_scanned": 12,
"changes": [{"field": "readyToWork", "added": ["TASK-005"]}],
"touched_paths": [".state/PROJECT_STATE.json", ".state/counters.json"],
"stage_paths": [".state/PROJECT_STATE.json", ".state/counters.json"]
}
touched_paths — всё, что sync тронул (для информации и для diff-сверки
с git status --porcelain).
stage_paths — subset для git add: исключает пути, которые попадают
под .gitignore. Для sync разница обычно нулевая, но контракт един с
polisade_migrate.py, где .env при bitbucket bootstrap оказывается в
touched_paths без stage_paths.
Если состояние не мигрировано (abort, rc=1)
{
"status": "migration_required",
"current_schema": 5,
"required_schema": 6,
"legacy_version_key": true,
"reason": "schemaVersion 5 < 7; legacy `pdlcVersion` key present",
"action": "run /polisade:migrate --apply before this command"
}
sync отказывается реконсилить state, который ещё не прошёл
/polisade:migrate после переименования pdlc→polisade (legacy-ключ
pdlcVersion или schemaVersion < 7) — иначе он переписал бы derived-поля,
оставив legacy-ключи на месте (ADR-0001 / issue #171). При этом статусе
не коммить и не повторяй sync: сначала прогони /polisade:migrate --apply, затем снова /polisade:sync. State при аборте не тронут.
Важно
- По умолчанию dry-run — не записывает ничего без
--apply
- При
--apply показывает diff и спрашивает подтверждение
--apply --yes пропускает подтверждение (для CI/pipe)
- Перестраивает:
readyToWork, inProgress, blocked, waitingForPM, inReview
- Обновляет
artifactIndex — безопасный индекс всех артефактов
- Не перезаписывает
artifacts если в нём структурированные данные (только flat index)
- Для диагностики без изменений используй
/polisade:doctor
После применения — закоммить и открыть PR
После /polisade:sync --apply рабочее дерево обычно содержит изменения
(.state/PROJECT_STATE.json, иногда .state/counters.json,
tasks/TASK-*.md если PM правил статусы вручную, и т. п.). PM в
корп-сессии (issue #108) после этого попросит «закоммить и сделай pr» —
агент должен пройти ровно по этому рецепту, без импровизации.
Контракт: ни одного $(...), бэктиков (command substitution в Bash) или
<(...) / >(...) в шелл-командах ниже. Корп-шелл (GigaCode CLI / codex
sandbox) режет их с сообщением «Command substitution using $(), ``, <(),
or >() is not allowed for security reasons», и весь рецепт обрывается.
WORK_DIR="${POLISADE_WORK_DIR:-.}"
git switch -c <branch>
git add <path1> <path2> ...
git commit -m "<skill>: <scoped summary>"
python3 {plugin_root}/scripts/polisade_vcs.py git-push \
--branch <branch> --set-upstream \
--project-root "$WORK_DIR"
mkdir -p .polisade/tmp
git log -1 --pretty=%B > .polisade/tmp/pr-body.md
python3 {plugin_root}/scripts/polisade_vcs.py pr-create \
--title "<skill>: <scoped summary>" \
--head <branch> --base main \
--body-file .polisade/tmp/pr-body.md \
--project-root "$WORK_DIR"
Why этот рецепт жёсткий:
polisade_vcs.py git-push верифицирует push (exit-code + pattern-scan +
SHA), bare git push — нет.
--body-file обходит ограничение корп-шелла на command substitution.
- Самодельный Python/curl в Bitbucket/GitHub REST API утекает токены из
.env мимо polisade_vcs.py и теряет provider-agnostic мост.
git status --porcelain — fallback, не primary: при параллельных
user-edits даёт лишние файлы.