{"_id":"@aimana/core","name":"@aimana/core","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@aimana/core","version":"0.1.0","description":"AIMana core: schemas, .aimana/ file layer, templates","type":"module","engines":{"node":">=22"},"main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./catalogue":{"types":"./dist/catalogue/index.d.ts","default":"./dist/catalogue/index.js"}},"publishConfig":{"access":"public"},"dependencies":{"yaml":"^2.9.0","zod":"^4.5.4"},"scripts":{"build":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit"},"_nodeVersion":"25.9.0","_id":"@aimana/core@0.1.0","dist":{"integrity":"sha512-QNJ7oy8+29c+Hw+/LoVgU+S4Ny45FwXnTXFiTg8MPMpsYHzWjLx5kKkipNYwIdkvYW07OKwEvy8eKoebD1TBqA==","shasum":"72bf15cb85bfb4dfa16018e806511cf9b53c9e8e","tarball":"https://registry.npmjs.org/@aimana/core/-/core-0.1.0.tgz","fileCount":162,"unpackedSize":1580512,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBnma+2TUCl8MNgs7qQwGHNwVUTZqChZLMB89DTO25XuAiEAyUSRDAOEMN0unjOhfGtWB0YP27Pvm/K7ke8FbKaOjbU="}]},"_npmUser":{"name":"technofrog","email":"alexei@mikhaltsov.pro"},"directories":{},"maintainers":[{"name":"technofrog","email":"alexei@mikhaltsov.pro"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/core_0.1.0_1788998639339_0.577594600591373"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-10T00:03:58.970Z","0.1.0":"2026-09-10T00:03:59.499Z","modified":"2026-09-10T00:03:59.791Z"},"maintainers":[{"name":"technofrog","email":"alexei@mikhaltsov.pro"}],"description":"AIMana core: schemas, .aimana/ file layer, templates","readme":"# @aimana/core\n\nЕдинственный способ читать и писать каталог `.aimana/` проекта. Схемы, парсер\nmarkdown с YAML-frontmatter, файловый слой, дерево проекта, вычисление состояния.\n\n## Что внутри\n\n| Модуль       | Назначение                                                                                                                                                                                    |\n| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `schema/`    | zod-схемы frontmatter: `Project`, `Feature`, `Task`, `Stage`, `Decision`, `Pipeline`, enum-ы статусов                                                                                         |\n| `document/`  | `parseDocument`, `serializeDocument` (ключи в порядке схемы), `splitFrontmatter`, секции h2: `getSection`, `setSection`                                                                       |\n| `fs/`        | `AimanaFs` (чтение с валидацией, атомарная запись), `AimanaPaths`, `readTree`, ошибки                                                                                                         |\n| `state/`     | `buildGraph` (DAG, циклы, битые ссылки), `computeState` (эффективные статусы, `blockedBy`, прогресс, `nextStage`, доступные к старту), таблицы переходов `canTransition` / `assertTransition` |\n| `ops/`       | Записи в `.aimana/` от лица демона: `createFeature`/`createTask`, `reportProgress`/`reportArtifact`/`logDecision`, план таски (`applyPlan`, `approvePlan`, `setTaskStatus`)                   |\n| `templates/` | движок подстановки, разрешение шаблонов, пайплайны, шаблоны архитектур, пресеты стеков, `buildPromptContext`, `renderStagePrompt` / `renderActionPrompt`                                      |\n| `catalogue/` | справочник опций проекта для CLI и веба: языки, менеджеры пакетов, платформы, имена гейтов, политики с подписями и описаниями                                                                 |\n\n## Пример\n\n```ts\nimport { AimanaFs, computeState, readTree } from '@aimana/core';\n\nconst afs = new AimanaFs('/path/to/repo');\nif (await afs.exists()) {\n  const tree = await readTree(afs);\n  for (const { feature, tasks } of tree.features) {\n    console.log(feature.frontmatter.id, tasks.length);\n  }\n  for (const error of tree.errors) console.warn(error.file, error.message);\n  await afs.updateStageSection('auth', '001-login-form', '02', 'Результат', 'Сделано.');\n\n  const state = computeState(tree);\n  for (const task of state.available) console.log('можно стартовать:', task.ref);\n  for (const issue of state.issues) console.warn(issue.message);\n}\n```\n\n## Правила вывода статусов\n\n`computeState` чистая функция над `ProjectTree`:\n\n- Таска `done`, если сохранено `done` или все её стейджи `done`. `blocked`, если\n  открыты её зависимости из `depends_on` или зависимости её фичи. `in-progress`,\n  если хоть один стейдж сдвинулся с `todo`. Иначе сохранённый статус.\n- Фича `done`, когда все таски `done`; `blocked` при открытых зависимостях;\n  `in-progress`, если есть таска в работе или заблокированная; `archived` как есть;\n  `status_override: true` замораживает сохранённый статус.\n- Доступна к старту: эффективный статус `todo` или `planned`, нет блокеров,\n  фича не `archived`, таска не в цикле.\n\nПереходы статусов, которые демон обязан проверять перед записью:\n`STAGE_TRANSITIONS`, `TASK_TRANSITIONS`, `FEATURE_TRANSITIONS`.\n\n## Гарантии\n\n- Невалидный файл никогда не попадает на диск: запись валидирует перед `rename`.\n- Неизвестные ключи frontmatter сохраняются (совместимость с будущими версиями формата).\n- Сериализация детерминирована: порядок ключей по схеме, `indent: 2`, один `\\n` в конце.\n- `readTree` не падает на одном битом файле, а возвращает его в `errors[]`.\n- Проблемы графа (цикл, битая ссылка) попадают в `state.issues`, а не бросаются.\n\n## Настройки проекта (`ops/project.ts`)\n\n`updateProjectBody` заменяет тело `PROJECT.md` — правила проекта, которые `CLAUDE.md`\nимпортирует целиком. Поля `updated` у проекта в схеме нет, поэтому ничего не проставляется.\n\n`updateProjectSettings` меняет frontmatter **частично**: поля, которых нет в патче, не\nтрогаются. Схема — `looseObject`, у проекта бывают ключи, о которых операция не слышала, и\nформа с пятью полями не должна стирать шестое. Внутри `gates` и `policies.security` мерж\nидёт по ключам, где `null` удаляет ключ; пустая команда гейта — тоже удаление, потому что\nкоманды без содержимого в формате не бывает. `platforms` заменяются целиком: иначе платформу\nне убрать. Результат валидируется схемой **до записи** — невалидное значение бросает\n`AimanaValidationError` и на диск не попадает. Пустой патч — тоже ошибка, а не молчаливая\nперезапись файла.\n\nСправочник опций (`catalogue/project-options.ts`) — списки языков, менеджеров пакетов,\nплатформ, имён гейтов и политик с русскими подписями. Всё это **подсказки, а не\nограничения**: схема принимает любой язык, любую платформу и любое имя гейта. Живёт в core,\nпотому что нужен и `@aimana/cli` (опрос `aimana init`), и `@aimana/web` (форма настроек), а\nтретья копия была бы третьей правдой.\n\n## План таски (`ops/plan.ts`)\n\nМодель отдаёт план как данные — `TaskPlanSchema`:\n\n```ts\n{ stages: [{ kind, title, goal, size?, artifacts, gates, criteria }], risks, out_of_scope }\n```\n\n`parsePlanJson(text)` достаёт то же самое из ответа, когда инструмент `aimana_submit_plan`\nне был вызван: последний ` ```json `-блок, затем любой блок, затем фрагмент от первой\n`{` до последней `}`.\n\nДальше данные превращаются в markdown и **markdown становится источником правды**:\n\n- `applyPlan` пишет `stages` во frontmatter `TASK.md` и разворачивает план в секции\n  `## План` (блок `### Стейдж NN: …` на каждый стейдж), `## Риски`, `## Не входит`.\n  Статус таски → `plan-review`.\n- Человек правит эти секции руками до подтверждения — хоть в вебе, хоть в редакторе.\n- `approvePlan` перечитывает `## План` парсером (`parsePlanSection`), а не то, что записала\n  модель: дописанный руками стейдж попадает во frontmatter и получает свой `STAGE-xx.md`.\n  Статус и `run_id` стейджей, переживших перепланирование (тот же `id`, то же название),\n  сохраняются.\n\n`renderPlanSection`/`parsePlanSection` обратимы, это проверяется round-trip тестом: иначе\nправка руками теряла бы данные. Созданные файлы стейджей получают заготовку ТЗ (цель,\nожидаемые артефакты, критерии приёмки чекбоксами) — само ТЗ генерирует T-032.\n\n## Фикстуры\n\n`test/fixtures/sample` и `test/fixtures/broken` генерируются скриптом\n`test/make-fixtures.mjs` через реальный сериализатор. Перегенерировать после\nизменения формата:\n\n```bash\npnpm --filter @aimana/core build && node packages/core/test/make-fixtures.mjs\n```\n\n## Шаблоны промптов\n\nПромпт для запуска Claude собирается из шаблона и контекста, собранного по `.aimana/`.\n\n### Где лежат шаблоны\n\nКаждый файл ищется по цепочке, побеждает первый найденный (мержа содержимого нет):\n\n1. `<repo>/.aimana/templates/` — переопределение для конкретного проекта, лежит в git;\n2. `~/.aimana/templates/` — личные шаблоны пользователя;\n3. `packages/core/templates/` — встроенные, едут в npm-пакет.\n\n```\ntemplates/\n  pipelines/{default,hotfix,research}.yaml   каркасы стейджей таски\n  stages/<kind>.md                           промпт стейджа: spec, plan, implement, test,\n                                             review, security, docs, custom\n  actions/<action>.md                        plan-task, spec-full, spec-stage, summarize-result\n  partials/{context,stage,system-tail}.md    общие куски, подключаются через {{> name}}\n```\n\nЧтобы переопределить один промпт, достаточно положить свой файл рядом по тому же пути:\n`.aimana/templates/stages/implement.md`. Партиалы переопределяются так же по отдельности,\nпоэтому можно поменять только «системный хвост», не трогая остальное.\n\n### Движок\n\nСвоё подмножество mustache (~230 строк, без зависимостей). `eta` из плана не подошёл: нужен\nсинтаксис `{{...}}` и **строгий режим** — незаполненный плейсхолдер должен ронять рендер, а не\nмолча давать дыру в промпте. Ни `eta`, ни `mustache` так не умеют.\n\n| Конструкция           | Что делает                                                                                                           |\n| --------------------- | -------------------------------------------------------------------------------------------------------------------- |\n| `{{path.to.value}}`   | подстановка; отсутствующее имя — ошибка, объект — ошибка, массив строк склеивается через `\\n`                        |\n| `{{#name}}…{{/name}}` | секция: массив — повтор на каждый элемент, объект или скаляр — кладётся на стек контекстов, пусто или ложь — пропуск |\n| `{{^name}}…{{/name}}` | обратная секция: рендерится, когда значения нет или оно пустое                                                       |\n| `{{.}}`               | текущий элемент стека (элемент массива, значение скалярной секции)                                                   |\n| `{{> partial}}`       | вставка `partials/<partial>.md`; отступ строки переносится на все её строки                                          |\n| `{{! комментарий }}`  | выкидывается                                                                                                         |\n\nСтрока, в которой кроме тега секции, закрытия, комментария или partial-а ничего нет, исчезает\nцеликом: служебные теги не оставляют пустых строк.\n\n### Основные плейсхолдеры\n\n`buildPromptContext` собирает контекст из готового `ProjectTree` (чистая функция, диск не читает).\n\n| Плейсхолдер            | Что внутри                                                                       |\n| ---------------------- | -------------------------------------------------------------------------------- |\n| `{{project.rules}}`    | тело `PROJECT.md` без ведущего `# ...`                                           |\n| `{{policies}}`         | политики безопасности и тестирования готовым markdown-списком                    |\n| `{{stage.spec}}`       | секция `## ТЗ` текущего стейджа, пусто если ТЗ ещё нет                           |\n| `{{artifacts}}`        | ожидаемые артефакты стейджа списком (`stage.artifacts` — тот же список массивом) |\n| `{{previous.results}}` | секции `## Результат` предыдущих стейджей, блоками с заголовками                 |\n| `{{decisionsText}}`    | ADR, чьи теги пересекаются с тегами фичи                                         |\n| `{{gateLog}}`          | лог упавшего гейта, если запуск повторный                                        |\n\nРядом с готовыми блоками лежат структурированные данные (`project.gates`, `task.stages`,\n`stage.artifacts`, `decisions`), чтобы шаблон мог собрать список по-своему.\n\n### Пример\n\n```ts\nimport {\n  AimanaFs,\n  buildPromptContext,\n  createTemplateResolver,\n  loadPipeline,\n  readTree,\n  renderStagePrompt,\n} from '@aimana/core';\n\nconst afs = new AimanaFs('/path/to/repo');\nconst tree = await readTree(afs);\nconst resolver = createTemplateResolver({ root: '/path/to/repo' });\n\nconst pipeline = await loadPipeline(resolver, 'default');\nconst context = buildPromptContext({\n  tree,\n  featureId: 'auth',\n  taskId: '001-login-form',\n  stageId: '02',\n  pipeline,\n});\n\nconst prompt = await renderStagePrompt(resolver, 'implement', context);\n```\n\n`renderActionPrompt(resolver, 'plan-task', context)` рендерит шаблон действия. Обе функции\nстрогие: опечатка в имени плейсхолдера даёт `TemplateRenderError` с именем шаблона и строкой.\n\nПайплайны валидируются `PipelineSchema`: битый YAML или несовпадение `id` с именем файла дают\n`AimanaValidationError`, неизвестный id — `TemplateNotFoundError`.\n\n> Файлы `packages/core/templates/**/*.md` исключены из prettier: он считает `{{#section}}`\n> абзацем и вставляет пустые строки внутрь списков, ломая рендер.\n\n## Свои шаблоны\n\nАрхитектуры, пресеты стеков и политики ищутся по одной цепочке, побеждает первый уровень,\nобъявивший id:\n\n1. `<repo>/.aimana/templates/` — шаблоны этого репозитория, лежат в git;\n2. `~/.aimana/templates/` — личные шаблоны пользователя, общие для всех его проектов;\n3. встроенные в пакет.\n\n```\n~/.aimana/templates/            или  <repo>/.aimana/templates/\n  architectures/<id>/template.yaml   плюс ARCHITECTURE.md, rules.md, structure.yaml, pipeline.yaml\n  stacks/<id>.yaml\n  policies/<id>.yaml\n```\n\n`id` внутри файла обязан совпадать с именем файла или каталога, иначе `load*` и `list*`\nразошлись бы в ответах на один и тот же вопрос.\n\n**Сломанный шаблон не исчезает.** `listArchitectures`, `listStacks` и `listPolicies` возвращают\n`{ items, errors }`: файл, который не проходит схему, попадает в `errors` вместе с абсолютным\nпутём и полями, на которых он развалился, а остальные шаблоны остаются в `items`. Выбрать его\nнельзя — применять нечего; `loadArchitecture`, `loadStack` и `loadPolicy`, которых просят по\nимени, по-прежнему бросают `AimanaValidationError`.\n\n```ts\nconst { items, errors } = await listStacks(createTemplateResolver({ root }));\n// errors: [{ id, source, file, message, issues: [{ path, message }] }]\n```\n\nОбход каталога не уходит по симлинкам за его пределы: сам каталог шаблонов симлинком быть\nможет (`~/.aimana/templates` в дотфайлах — обычное дело), а отдельная запись внутри него,\nведущая наружу, не читается и попадает в `errors`.\n\n## Шаблоны архитектур\n\nКаталог `templates/architectures/<id>/` — каркас архитектуры, который разворачивается в\nпроект. Ищется по той же цепочке (проект → пользователь → встроенные), причём **пофайлово**:\nчтобы поменять только правила встроенного шаблона, достаточно положить свой\n`.aimana/templates/architectures/hexagonal/rules.md`.\n\n```\narchitectures/<id>/\n  template.yaml     id, title, description, tags — манифест, он же объявляет шаблон\n  ARCHITECTURE.md   каркас с заголовками и вопросами, уезжает в .aimana/ARCHITECTURE.md\n  rules.md          правила, которые дописываются в PROJECT.md\n  structure.yaml    (необязательно) рекомендуемое дерево папок: entries[{path, note}]\n  pipeline.yaml     (необязательно) пайплайн, который шаблон предлагает вместо дефолтного\n```\n\nВстроенных восемь: `modular-monolith`, `clean-architecture`, `hexagonal`, `feature-sliced`,\n`microservices`, `mobile-mvvm`, `cli-tool`, `library`.\n\n```ts\nimport {\n  AimanaFs,\n  applyArchitecture,\n  createTemplateResolver,\n  listArchitectures,\n  previewArchitecture,\n} from '@aimana/core';\n\nconst resolver = createTemplateResolver({ root });\nawait listArchitectures(resolver);\n// { items: [{ id, title, description?, tags, source }], errors: [] }\n\nconst preview = await previewArchitecture(new AimanaFs(root), resolver, 'clean-architecture');\n// preview.architecture / preview.rules: { action, current, next, diff } — ничего не записано\nawait applyArchitecture(new AimanaFs(root), resolver, 'clean-architecture', {\n  overwriteArchitecture: preview.overwritesArchitecture,\n});\n```\n\nПравила шаблона живут в теле `PROJECT.md` между маркерами:\n\n```\n<!-- aimana:template rules start -->\n...правила шаблона...\n<!-- aimana:template rules end -->\n```\n\nГраница нужна, чтобы смена шаблона убирала ровно правила прежнего и не трогала дописанное\nруками. Одинокий маркер без пары считается отсутствующей секцией: гадать, где кончался\nнаполовину стёртый блок, значит съесть чужой текст.\n\n`ARCHITECTURE.md`, написанный человеком, без спроса не перезаписывается: `applyArchitecture`\nбросает `AimanaConflictError`, пока не передан `overwriteArchitecture`. Дифф строк считает\n`diffLines` — свой LCS на тридцать строк, ради двух markdown-файлов зависимость не заводится.\n\n## Пресеты стеков\n\nФайл `templates/stacks/<id>.yaml` — пресет стека: чем проект написан, какими командами\nпроверяется и по каким признакам его узнать. Ищется по той же цепочке\n(проект → пользователь → встроенные), побеждает ближайший к проекту.\n\n```yaml\nid: next\ntitle: Next.js\ndescription: 'React на сервере и в браузере: маршруты по файлам…'\nlanguage: typescript\nframework: nextjs\npackage_manager: npm\nplatforms: [web]\ngates:\n  lint: 'npm run lint'\n  typecheck: 'npx tsc --noEmit'\n  build: 'npm run build'\nrules:\n  - 'Компонент серверный по умолчанию…'\ndetect:\n  files: [next.config.ts]\n  dependencies: [next]\n  contains: [{ file: pom.xml, text: spring-boot-starter }]\n  priority: 20\n```\n\nВстроенных восемнадцать: `next`, `remix`, `vite-react`, `nest`, `fastify`, `express`,\n`django`, `fastapi`, `spring-boot`, `quarkus`, `go-std`, `rust-axum`, `swiftui`,\n`kotlin-compose`, `flutter`, `react-native`, `electron`, `tauri`.\n\n**Гейт есть только тогда, когда у стека есть настоящая команда.** У Next нет тестового\nраннера из коробки — гейта `test` в пресете нет; у Express нет ни линтера, ни типов —\nостаётся один `npm test`. Выдуманная команда падает на первом запуске и учит человека\nне верить гейтам, поэтому её лучше не писать вовсе. Имена гейтов не ограничены четвёркой:\nу Django есть `migrations`, у Rust и Flutter — `format:check`.\n\n### Автодетект\n\n```ts\nimport { createTemplateResolver, detectStackPreset } from '@aimana/core';\n\nconst matches = await detectStackPreset(root, createTemplateResolver({ root }));\n// [{ preset, score, signals: [{ kind: 'file', text: 'next.config.ts' }, …] }] — лучший первым\n```\n\nПризнак срабатывает, если файл есть (`*` матчит внутри одного сегмента, поэтому\n`*.xcodeproj` работает), если имя есть среди зависимостей `package.json` (точное совпадение,\nиначе `react` объявил бы React Native реактом) или если в файле встречается строка\n(без учёта регистра — в `requirements.txt` пишут `Django`). Ранжирование — по числу\nсработавших признаков, при равенстве по `detect.priority`: приложение на Electron с Vite\nдаёт два признака обоим пресетам, и специфичный должен побеждать. **Пустой ответ — это\nответ, а не ошибка**: большинство репозиториев написаны на стеке, под который пресета нет.\nБитый пресет в `.aimana/templates/stacks/` при этом всё равно бросает `AimanaValidationError`.\n\n### Применение\n\n```ts\nimport { applyStackPreset, previewStackPreset } from '@aimana/core';\n\nconst preview = await previewStackPreset(new AimanaFs(root), resolver, 'django');\n// preview.settings: [{ field: 'stack.language', current: 'typescript', next: 'python' }, …]\n// preview.rules: { action, current, next, diff } — на диск ничего не записано\nawait applyStackPreset(new AimanaFs(root), resolver, 'django');\n```\n\nПресет пишет `stack.*` (включая `stack.preset`), гейты своих имён и правила — всё через\n`updateProjectSettings` и `updateProjectBody`, то есть документ остаётся валидным по схеме.\nГейт, о котором пресет ничего не говорит, остаётся как был. Платформы **добавляются**, а не\nзаменяются: пресет знает про свой стек, но не знает, что у проекта есть ещё и CLI.\n\nПравила пресета живут в своей паре маркеров:\n\n```\n<!-- aimana:stack rules start -->\n...правила пресета...\n<!-- aimana:stack rules end -->\n```\n\nОтдельно от `aimana:template rules` шаблона архитектуры — иначе выбор стека стирал бы правила\nархитектуры и наоборот.\n\n## Политики\n\nФайл `templates/policies/<id>.yaml` — то, во что разворачивается чекбокс\n`PROJECT.md: policies`: текст правила, дополнительные гейты и дополнительные стейджи. Цепочка\nта же (проект → пользователь → встроенные), побеждает ближайший к проекту. Идентификатор —\nsnake_case: это ключ из `policies.security` или `policies.testing`, а не kebab-case-id шаблона.\n\n```yaml\nid: dependency_audit\ntitle: Аудит зависимостей\nkind: security\ndescription: 'Зависимости проверяются на известные уязвимости.'\nrule: 'Новая зависимость приходит вместе с проверкой на известные уязвимости…'\ngates:\n  - name: audit\n    package_managers:\n      pnpm: 'pnpm audit --audit-level=high'\n      cargo: 'cargo audit'\n    languages:\n      go: 'go run golang.org/x/vuln/cmd/govulncheck@latest ./...'\nstages: []\n```\n\nВстроенных семь: `input_validation`, `no_secrets_in_code`, `dependency_audit`, `auth_review`,\n`unit_required`, `integration_required`, `e2e_required` — ровно чекбоксы справочника\n`catalogue/project-options.ts`.\n\n**Команда гейта зависит от стека**, поэтому её нет одной на всех: `pnpm audit` ничего не значит\nдля проекта на cargo. Сначала спрашивается пакетный менеджер, потом язык, и если ни один не\nзнает команды — `policyGates` честно возвращает пустой список, а гейт не появляется вовсе.\nПо той же причине у `no_secrets_in_code` гейта нет ни для кого: сканеры секретов не входят ни в\nодин пакетный менеджер, а выдуманная команда падает на первом запуске.\n\n### Применение\n\n```ts\nimport { applyPolicies, previewPolicies } from '@aimana/core';\n\nconst preview = await previewPolicies(new AimanaFs(root), resolver); // как есть сейчас\nawait applyPolicies(new AimanaFs(root), resolver, { dependency_audit: true });\n```\n\nВключение пишет флаг, гейты политики и её правила — всё через `updateProjectSettings` и\n`updateProjectBody`. Выключение убирает **ровно своё**: флаг становится `false` (ключ остаётся\nв файле — это же чекбокс), правило уходит из секции, а гейт снимается только пока его команда\nвсё ещё одна из тех, что политика могла написать сама. Команду, переписанную руками, выключение\nне трогает.\n\nПравила политик живут в третьей паре маркеров:\n\n```\n<!-- aimana:policy rules start -->\n...правила включённых политик, их стейджи и гейты...\n<!-- aimana:policy rules end -->\n```\n\nОтдельно от `aimana:template rules` и `aimana:stack rules`: три владельца пишут в один файл, и\nобщая граница означала бы, что каждый выбор стирает два других. Когда не включено ничего,\nсекция убирается целиком вместе с маркерами — пустой заголовок хуже отсутствующего.\n\n### Дополнительные стейджи\n\n`policyStages` собирает стейджи включённых политик, `withPolicyStages` подмешивает их в\nпайплайн, по которому планируется таска (стейдж с тем же видом и названием второй раз не\nдобавляется). **Сейчас планировщик демона этим ещё не пользуется**: до плана дополнительные\nстейджи доезжают текстом — секция «Дополнительные стейджи» в правилах `PROJECT.md`, а правила\nпроекта лежат в каждом промпте. Чтобы стейдж появился и в каркасе пайплайна, `PlanService`\nдолжен позвать `withPolicyStages` — это одна строка, но она в `packages/daemon/src/plans`.\n","readmeFilename":"","_rev":"1-5ae7e8fb0919d12a07526efc4ca65444"}