Compare commits
30 Commits
7504b09b87
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 79adaf928d | |||
| b51657bfe4 | |||
| 48fa29e9dc | |||
| 65a2518a5e | |||
| 84e28c5d1c | |||
| 607a475e28 | |||
| 252e22ec80 | |||
| ac0c41feb8 | |||
| 314b15ea25 | |||
| c310ada38d | |||
| 5c726eb5ec | |||
| d2059b42bd | |||
| 2707ba48b5 | |||
| 9c969cefb9 | |||
| 37f617a461 | |||
| ccac87200f | |||
| b1cc0439a7 | |||
| 3f78c54dd2 | |||
| ddcb552601 | |||
| 74fdbe8070 | |||
| 973e59b083 | |||
| c09901f9a6 | |||
| e2f2e3a342 | |||
| b529503def | |||
| 031268333e | |||
| e499a69bd0 | |||
| 247abbbf12 | |||
| e3f20193f0 | |||
| 1b0118d254 | |||
| 4f8e12aedf |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -90,3 +90,6 @@ coverage/
|
||||
# Missing here made `git status` see `?? .tasks/claims/` → poller skipped every
|
||||
# claim with "working tree dirty". Mirrors .common/.gitignore.
|
||||
.tasks/claims/
|
||||
|
||||
# mappa bootstrap cache (генерируется, не в репо)
|
||||
.mappa/share/
|
||||
|
||||
9
.mappa/config.yaml
Normal file
9
.mappa/config.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# mappa project marker — machine-readable identifier of a mappa project folder
|
||||
schema_version: 1 # версия схемы файла (bump при изменении структуры)
|
||||
protocol_version: 1 # версия протокола интерпретации маркера
|
||||
project: skills
|
||||
tenant: vitya
|
||||
url: https://mappa.vds.kzntsv.site
|
||||
git_provider: gitea
|
||||
git: OpeItcLoc03/skills
|
||||
git_host: git.kzntsv.site
|
||||
@@ -6,7 +6,9 @@ created: 2026-08-12
|
||||
|
||||
# pi-extension headless ritual (agent_end, mode guard, loop-guard)
|
||||
|
||||
Durable lessons from building `session-close-ritual` (репо `OpeItcLoc03/pi-extensions`, extensions/),
|
||||
Durable lessons from building `session-close-ritual` (консолидирован в
|
||||
`extensions/mappa.ts` репо `OpeItcLoc03/pi-extensions`, task:1486; исторически —
|
||||
отдельный файл `session-close-ritual.ts`),
|
||||
the headless injector for the session-handoff closing ritual. All three points
|
||||
were live-verified, not docs-read-only.
|
||||
|
||||
@@ -58,6 +60,8 @@ Cache per-cwd; staleness within a long session is accepted (same as
|
||||
|
||||
## References
|
||||
|
||||
- Source: `~/projects/pi-extensions/extensions/session-close-ritual.ts` (+ `scripts/session-close-ritual.test.mjs`, 12 blocks)
|
||||
- Source: `~/projects/pi-extensions/extensions/mappa.ts` (секция close-ritual;
|
||||
консолидация 6 расширений, task:1486 — исторически `session-close-ritual.ts`
|
||||
+ `scripts/session-close-ritual.test.mjs`, 12 blocks, ныне тесты на mappa.ts)
|
||||
- Skill: `session-handoff` v0.5.0 — «Headless (pi)» section
|
||||
- pi docs: `extensions.md` — lifecycle diagram, `sendUserMessage` (deliverAs/triggerTurn), mode table
|
||||
|
||||
19
AGENTS.md
19
AGENTS.md
@@ -13,3 +13,22 @@ follow tdd-criteria
|
||||
delegate to interns when allowed
|
||||
recommend, don't menu
|
||||
we're on Windows
|
||||
mappa sync: run on start
|
||||
|
||||
<!-- mappa:canon-block (auto; do not edit) -->
|
||||
## Mappa canon
|
||||
**Gates (canon/gates — shared):** краткая суть; полный текст — `wiki_get(slug='canon/gates', full=true)`
|
||||
- Г1. Знание и артефакты → mappa, не файлы — durable-знание и артефакты проекта живут в mappa; файловые каналы (`.brainstorm/`, `.tasks/`, `.wiki/`) и «сохранить рядом с проектом» закрыты.
|
||||
- Г2. Контракт каналов — письмо (inbox) — носитель вердиктов/указаний/находок, полным телом; комментарии на тасках — короткий след. Адресация `about`/`to`/`thread` (XOR); lifecycle `ack`/`resolve`/`cancel`.
|
||||
- Г3. Поиск до угадывания — не угадывать slug/роут: сначала `search`/`wiki_search`; `wiki_get` без project = shared; общая память — `search(mode='recall')`.
|
||||
- Г4. Адресация и слаги — рефы полными именами (`[[task:N]]`/`[[wiki:slug]]`); слаги kebab-case, латиница; номера `task:N` выдаёт сервер.
|
||||
- Г5. .mappa-гейт — папка участвует в mappa-операциях только с маркером `.mappa`; нет маркера → сказать человеку, мутации — отказ.
|
||||
- Г6. Секреты — в mappa не пишутся (422); только `secret:<path>`-рефы, значения мимо.
|
||||
- Г7. Degraded-режим — mappa недоступна: читать кэш `.mappa/` (canon/methodology/runbooks), мутации → `.mappa/pending/`; нет кэша → стоп, не импровизировать.
|
||||
- Г8. Перед работой с вики/каноном — первым действием прочитать канон-блок AGENTS.md проекта.
|
||||
- Г9. Живое состояние до заявления — статус заявлять только по свежему чтению mappa, не по памяти/кэшу/ответу create.
|
||||
**Entity → runbook (runbooks/index — shared):** task → [[runbooks/tasks]] · wiki → [[runbooks/wiki]] · inbox → [[runbooks/inbox]] · **thread** → [[runbooks/threads]] · session → [[runbooks/session]] · search → [[runbooks/search]] · issue → [[runbooks/issue]] · **intent** → [[runbooks/intent]] · requirements → [[runbooks/requirements]] · plan → [[runbooks/plan]] · comment → [[runbooks/comment]] · tag → [[runbooks/tag]] · attachment → [[runbooks/attachment]] · release → [[runbooks/release]] · brainstorm → [[runbooks/brainstorm]] · agent → [[runbooks/agent-operator]] · repo → [[runbooks/repo-commit]] · project → [[runbooks/project]] · skill → [[runbooks/skill]] · entity-слой → [[runbooks/entity]] · sched → [[runbooks/sched-telemetry]]
|
||||
**Methodology:** `methodology/kzntsv`
|
||||
**Canon version:** 4
|
||||
<!-- /mappa:canon-block -->
|
||||
|
||||
|
||||
10
README.md
10
README.md
@@ -25,8 +25,8 @@ A shared workspace where Claude and I author, debug, and ship skills together:
|
||||
git clone <repo> skills
|
||||
cd skills
|
||||
bash scripts/install.sh # copies every skills/* into ~/.claude/skills/
|
||||
# or only specific ones:
|
||||
bash scripts/install.sh mappa-knowledge caveman
|
||||
# or only specific ones (mappa-* skills install from the `mappa` repo — see mappa-bootstrap):
|
||||
bash scripts/install.sh caveman tdd-criteria
|
||||
```
|
||||
|
||||
**Linux / macOS (bash):**
|
||||
@@ -35,8 +35,8 @@ bash scripts/install.sh mappa-knowledge caveman
|
||||
git clone <repo> skills
|
||||
cd skills
|
||||
bash scripts/install.sh # copies every skills/* into ~/.claude/skills/
|
||||
# or only specific ones:
|
||||
bash scripts/install.sh mappa-knowledge caveman
|
||||
# or only specific ones (mappa-* skills install from the `mappa` repo — see mappa-bootstrap):
|
||||
bash scripts/install.sh caveman tdd-criteria
|
||||
```
|
||||
|
||||
The install target can be overridden with `CLAUDE_SKILLS_DIR=/path bash scripts/install.sh`.
|
||||
@@ -122,6 +122,8 @@ an explicit `adapted-from` marker in its frontmatter.
|
||||
| `code-review` | `adapted-from: mattpocock/skills @ 84fdeffd` (MIT) — two-axis + Fowler baseline; output: caveman-review format |
|
||||
| `writing-skills` | `adapted-from: obra/superpowers @ 6.2.0` (MIT) — TDD-for-skills core + ideya 8 self-skill-authoring |
|
||||
| `web-search` | `author: ours` — search_web tool (pi-extension) + policy: when to search, «без поиска» session-off |
|
||||
| `ops-browser` | `author: ours` — свой **скрытый** браузер агента: отдельный профиль + CDP (`eval`/`fetch` из страницы/скриншоты), `handoff` человеку для пароля/капчи; свой замок `ops.lock` |
|
||||
| `browser-operator` | `author: ours` — браузер ОПЕРАТОРА (его Chrome/логины): канал по харнессу (Hermes `browser_exec` / pi тул `browser` / CC `chrome-devtools`), аренда «один водитель за раз», границы «человек vs агент», рецепты тяжёлых страниц. Закрывает провал базового прогона 2026-09-11 («куки из Chrome + curl + ввод пароля» мимо канала); анонимные прогоны — `browser-cdp` |
|
||||
| `review-subagent` | `author: ours` — review_subagent tool (pi-extension): clean-context review by your own model, optional `model` override |
|
||||
| `report-mappa-issue` | `author: ours` — TEMPORARY stopgap: mappa deviation reporting (mail to `mappa` + `.workshop`) while the service is unstable; retire when stabilized |
|
||||
| all other `skills/*` | `author: ours` |
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
git clone <repo> claude-skills
|
||||
cd claude-skills
|
||||
bash scripts/install.sh # копирует все skills/* в ~/.claude/skills/
|
||||
# или конкретные:
|
||||
bash scripts/install.sh mappa-knowledge caveman
|
||||
# или конкретные (mappa-* скилы ставятся из репо `mappa` — см. mappa-bootstrap):
|
||||
bash scripts/install.sh caveman tdd-criteria
|
||||
```
|
||||
|
||||
Цель установки можно переопределить переменной `CLAUDE_SKILLS_DIR=/path bash scripts/install.sh`.
|
||||
@@ -91,6 +91,8 @@ bash scripts/build.sh caveman # один
|
||||
| `code-search` | `author: ours` — rg-first код-поиск (замер: 15 мин → 0 сек; роутинг: rg / git grep / interns repo_read / grep_audit) |
|
||||
| `code-review` | `adapted-from: mattpocock/skills @ 84fdeffd` (MIT) — двухосевость + Fowler-база; формат вывода: caveman-review |
|
||||
| `writing-skills` | `adapted-from: obra/superpowers @ 6.2.0` (MIT) — TDD-for-skills ядро + идея 8 self-skill-authoring |
|
||||
| `ops-browser` | `author: ours` — свой скрытый браузер агента (профиль + CDP + `handoff` человеку, замок `ops.lock`) |
|
||||
| `browser-operator` | `author: ours` — браузер ОПЕРАТОРА (его Chrome/логины): канал по харнессу (Hermes `browser_exec` / pi тул `browser` / CC `chrome-devtools`), аренда «один водитель за раз», границы «человек vs агент»; анонимные прогоны — `browser-cdp` |
|
||||
| остальные `skills/*` | `author: ours` |
|
||||
|
||||
Политика адаптации: клон переписывается под наши конвенции (доски `.tasks/`,
|
||||
|
||||
BIN
dist/browser-cdp.skill
vendored
BIN
dist/browser-cdp.skill
vendored
Binary file not shown.
BIN
dist/browser-operator.skill
vendored
Normal file
BIN
dist/browser-operator.skill
vendored
Normal file
Binary file not shown.
BIN
dist/mappa-vitya-brainstorming.skill
vendored
Normal file
BIN
dist/mappa-vitya-brainstorming.skill
vendored
Normal file
Binary file not shown.
BIN
dist/mappa-vitya-project-discipline.skill
vendored
Normal file
BIN
dist/mappa-vitya-project-discipline.skill
vendored
Normal file
Binary file not shown.
BIN
dist/ops-browser.skill
vendored
Normal file
BIN
dist/ops-browser.skill
vendored
Normal file
Binary file not shown.
BIN
dist/project-bootstrap.skill
vendored
BIN
dist/project-bootstrap.skill
vendored
Binary file not shown.
BIN
dist/project-discipline.skill
vendored
BIN
dist/project-discipline.skill
vendored
Binary file not shown.
BIN
dist/review-kit-pi-method.skill
vendored
BIN
dist/review-kit-pi-method.skill
vendored
Binary file not shown.
BIN
dist/session-health.skill
vendored
BIN
dist/session-health.skill
vendored
Binary file not shown.
BIN
dist/update-skills.skill
vendored
BIN
dist/update-skills.skill
vendored
Binary file not shown.
BIN
dist/using-markitdown.skill
vendored
BIN
dist/using-markitdown.skill
vendored
Binary file not shown.
@@ -55,6 +55,14 @@ skills:
|
||||
mode: auto
|
||||
category: software-development
|
||||
|
||||
browser-operator:
|
||||
mode: auto
|
||||
category: software-development
|
||||
|
||||
ops-browser:
|
||||
mode: auto
|
||||
category: software-development
|
||||
|
||||
using-markitdown:
|
||||
mode: auto
|
||||
category: productivity
|
||||
|
||||
@@ -44,7 +44,7 @@ function New-SkillArchive {
|
||||
[System.IO.Compression.ZipArchiveMode]::Create
|
||||
)
|
||||
try {
|
||||
$files = Get-ChildItem -Path $sourceFull -Recurse -File
|
||||
$files = Get-ChildItem -Path $sourceFull -Recurse -File | Where-Object { $_.FullName -notmatch '__pycache__' }
|
||||
foreach ($file in $files) {
|
||||
$rel = $file.FullName.Substring($sourceFull.Length + 1) -replace '\\','/'
|
||||
$entryName = "$SkillName/$rel"
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
---
|
||||
name: browser-cdp
|
||||
author: ours
|
||||
version: 0.1.0
|
||||
version: 0.1.1
|
||||
description: >
|
||||
Веб-автоматизация через минимальные CDP CLI-тулы в bash — вместо playwright-mcp
|
||||
/ Chrome-DevTools-MCP (подход «what if you don't need MCP»). Запуск Chrome с remote
|
||||
debugging, навигация, eval JS, скриншоты. Trigger: «браузер», «скрейпинг», «открой
|
||||
страницу», «перейди на», «сделай скриншот», «playwright», «веб-автоматизация»,
|
||||
«web scraping», «browser».
|
||||
«web scraping», «browser». Для ЛИЧНЫХ КАБИНЕТОВ оператора (его логины, антибот) —
|
||||
НЕ этот скил, а `browser-operator`.
|
||||
---
|
||||
|
||||
# browser-cdp
|
||||
@@ -20,6 +21,9 @@ description: >
|
||||
снять скриншот, собрать данные (скрейпинг). Использовать **вместо** playwright-mcp или
|
||||
Chrome-DevTools-MCP.
|
||||
|
||||
- ⚠️ **Для личных кабинетов оператора этот путь НЕ годится:** здесь свой Chrome и свой
|
||||
профиль (без его логинов). Нужен браузер оператора — скил `browser-operator`.
|
||||
|
||||
## Процесс
|
||||
|
||||
1. **Прочитай полную справку** (обязательно, первый шаг):
|
||||
|
||||
128
skills/browser-operator/SKILL.md
Normal file
128
skills/browser-operator/SKILL.md
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
name: browser-operator
|
||||
author: ours
|
||||
version: 0.1.0
|
||||
description: >
|
||||
Работа в браузере ОПЕРАТОРА — его Chrome, его профиль, его живые логины: личные
|
||||
кабинеты поставщиков (ЧипДип, ДКО, Промэлектроника, ТМ), Avito, порталы под
|
||||
антиботом. Trigger: «зайди в личный кабинет», «открой ЛК», «собери заказы»,
|
||||
«посмотри в браузере», «нужна его сессия», «ЧипДип/Ozon/Avito», «browser».
|
||||
НЕ для публичных страниц (там сначала обычный fetch/curl) и НЕ для анонимных
|
||||
прогонов (там скил browser-cdp).
|
||||
---
|
||||
|
||||
# Browser operator — браузер оператора
|
||||
|
||||
**Браузер оператора — разделяемый ресурс с его живыми сессиями.** Работай через
|
||||
готовый канал своего харнесса и **только под арендой «один водитель за раз»**.
|
||||
Не вытаскивай его куки, не логинься сам, не поднимай свой Chrome.
|
||||
|
||||
## Когда браузер действительно нужен
|
||||
|
||||
Сначала спроси себя, нужен ли браузер вообще:
|
||||
|
||||
- **Нет** — публичная страница, API, доки: обычный fetch/curl/поиск. Браузер тут лишний.
|
||||
- **Да** — страница требует взаимодействия (клик/форма/навигация), JS-рендер,
|
||||
**его логин** (личный кабинет, Avito), либо портал режет не-браузерный трафик.
|
||||
|
||||
## Канал по харнессу (не изобретай свой)
|
||||
|
||||
| Харнесс | Чем работать | Аренда |
|
||||
|---|---|---|
|
||||
| **Hermes** | тул `browser_exec` (демон `browser-use`, тот же профиль оператора) | автоматически: shell-хук `pre_tool_call` берёт аренду и блокирует вызов при чужой |
|
||||
| **pi** | тул `browser`: `open` / `read` / `js` / `click` / `screenshot` / `lease` | автоматически внутри тула; `lease` покажет, кто держит |
|
||||
| **Claude Code** | MCP `chrome-devtools` (`list_pages`, `navigate_page`, `take_snapshot`, `evaluate_script`, `click`, `fill`, `take_screenshot`) | автоматически: обёртка берёт аренду перед стартом сервера |
|
||||
|
||||
**Никогда:** `curl` с куками из его профиля, `browser_cookie3`-выгрузка куки,
|
||||
свой `chrome --remote-debugging-port` с пустым профилем (там нет его логинов),
|
||||
`pip install`-стек ради одного кабинета.
|
||||
|
||||
## Аренда: «один водитель в браузере за раз»
|
||||
|
||||
Браузер один. Если его держит другой харнесс — **не ломиться**, сказать «занято,
|
||||
держит X» и вернуться позже (или попросить оператора освободить).
|
||||
|
||||
Идёшь в браузер **в обход** тула (например, скриптом или `browser-use` из терминала) —
|
||||
бери аренду сам:
|
||||
|
||||
```bash
|
||||
BL="$HOME/.config/browser-harness/bin/browser-lease.sh"
|
||||
HOLD="manual:$$" # метка держателя
|
||||
MYPID="$(cat /proc/$$/winpid 2>/dev/null || echo 0)" # НАСТОЯЩИЙ windows-PID
|
||||
bash "$BL" acquire --holder "$HOLD" --ttl 900 --pid "$MYPID" --tool browser # 0 = взял, 3 = занято, 2 = ошибка
|
||||
# … работа …
|
||||
bash "$BL" release --holder "$HOLD"
|
||||
```
|
||||
|
||||
**PID — только настоящий.** MSYS `$$` это НЕ windows-PID: по нему живость аренды врёт
|
||||
(мёртвый держатель выглядит живым, живой — мёртвым). В bash бери `/proc/$$/winpid`,
|
||||
в pi/Node — `process.pid`; не знаешь — передай `0` («неизвестен», живость решит TTL).
|
||||
|
||||
**`driver.lock` руками не трогай** (в том числе пустой или «битый» — это окно чужой
|
||||
записи): отбор мёртвой аренды делает CLI по `ts`/TTL/мёртвому PID. Чужую аренду не снимай.
|
||||
|
||||
Контракт аренды (формат файла, TTL, кого связывать): вики mappa
|
||||
`concepts/browser-lease-contract`.
|
||||
|
||||
## Как работать в страницах (рецепты)
|
||||
|
||||
- **Первым делом — своя вкладка.** `ensure_real_tab()` / `new_tab(url)`; не полагайся
|
||||
на активную вкладку оператора: тяжёлая или аудио-страница подвешивает демон
|
||||
(все вызовы падают в таймаут, хотя `browser-use --doctor` говорит «alive»).
|
||||
- **Тяжёлые страницы — читать изнутри, а не обходом.** Если у сайта есть внутренний
|
||||
JSON-эндпоинт, зови его `fetch(path, {credentials:'include'})` из уже открытой
|
||||
страницы: это быстрее и надёжнее десятков навигаций.
|
||||
- **По одной штуке за раз, с паузами.** Паузы — в Python/процессе, **не** в JS `await`
|
||||
(иначе `Runtime.evaluate timed out`). Ориентир оператора: 1 запрос, пауза 7–13 с,
|
||||
перекур каждые ~40, стоп после 3 ошибок подряд.
|
||||
- **Клики:** сначала дерево доступности (`Accessibility.getFullAXTree`) или
|
||||
`querySelector` → центр элемента (`getBoundingClientRect`) → `click_at_xy` → **проверь
|
||||
результат** отдельным `js(...)`/`page_info()`. Не кликай «на глаз» по скриншоту.
|
||||
- **Прогресс — сразу на диск** (jsonl/atomic), а не в конце прогона: длинные обходы
|
||||
обрываются.
|
||||
- **Долгие обходы** — в фоновый процесс, а не в цикл интерактивных вызовов.
|
||||
|
||||
## Границы: где человек, а где агент
|
||||
|
||||
- **Пароли и второй фактор — никогда.** Не вводим и не просим в аргументах командной
|
||||
строки. Уже залогиненная сессия оператора — вот твой доступ.
|
||||
- **Попап «Разрешить удалённую отладку?»** — это человеческое подтверждение: агент его
|
||||
не жмёт, а просит оператора нажать «Разрешить» и повторяет попытку.
|
||||
- **Деньги/платежи/отправка форм с персональными данными** — только с явным
|
||||
подтверждением оператора.
|
||||
- **Секреты и содержимое залогиненных страниц** не пишем в логи, файлы репозитория и
|
||||
вики: в mappa — только агрегаты и идентификаторы.
|
||||
|
||||
## Проверенные адреса и факты
|
||||
|
||||
- **ЧипДип, кабинет заказов:** `https://www.chipdip.ru/order/list`
|
||||
(⚠️ `/cabinet` и `/orders` отдают 404 — не перебирай наугад, ссылка есть в шапке).
|
||||
- Залогиненность видна в шапке кабинета (имя оператора); если видишь форму логина —
|
||||
**сессия потеряна: стоп и скажи оператору**, не логинься сам.
|
||||
- `about:blank`-вкладка с титулом-лошадкой в `document.title` = страницу ведёт агент.
|
||||
|
||||
## Так делать НЕ надо
|
||||
|
||||
| Соблазн | Почему нет |
|
||||
|---|---|
|
||||
| «Вытащу куки из Chrome и пойду `curl`'ом» | мимо канала и мимо аренды; пароль/2FA оказываются рядом; ломается при смене защиты |
|
||||
| «Подниму свой Chrome с отладкой» | там **нет** его логинов → выкинет на логин/капчу, плюс это второй водитель |
|
||||
| «Введу логин/пароль через `read -s`» | пароли и 2FA не вводим никогда |
|
||||
| «Проверю ещё пяток URL кабинета» | адрес подтверждай по ссылке в интерфейсе, а не перебором |
|
||||
| «Налью 20 запросов параллельно, быстрее» | антибот + оператор останавливает такие прогоны; один водитель, одна очередь |
|
||||
|
||||
Основание: базовый прогон без скила (2026-09-11) ушёл именно в «куки + curl + пароль»
|
||||
мимо канала; скил закрывает этот путь.
|
||||
|
||||
## Красные флаги (стоп и перечитай)
|
||||
|
||||
- Собираешься вытащить куки / поднять свой браузер / ввести пароль или код.
|
||||
- Работаешь с браузером **без** аренды (или ломишься, когда «занято»).
|
||||
- Полливаешь запросами без паузы или ждёшь паузу в JS.
|
||||
- Пишешь содержимое залогиненных страниц/секреты в файл, лог или вики.
|
||||
|
||||
## Вне скоупа
|
||||
|
||||
- Анонимные/антидетект-прогоны, свой профиль, `mode: fresh` — скил `browser-cdp`.
|
||||
- Облачные браузеры (Browser Use Cloud и прочие) — не берём.
|
||||
- Свой stdio-MCP-сервер поверх общего демона — отдельная тема.
|
||||
90
skills/ops-browser/SKILL.md
Normal file
90
skills/ops-browser/SKILL.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: ops-browser
|
||||
author: ours
|
||||
version: 0.1.0
|
||||
description: >
|
||||
Use when a task needs a real browser — личный кабинет, заказы, чеки, yt-digest,
|
||||
скриншот живого сайта, «дёрни внутренний API». Свой СКРЫТЫЙ браузер агента:
|
||||
отдельный профиль, CDP, `fetch()` ИЗ страницы, хендофф человеку для пароля/капчи.
|
||||
---
|
||||
|
||||
# ops-browser — свой скрытый браузер агента
|
||||
|
||||
**Есть задача, которой нужен браузер? Работай в своём браузере, скрыто, и не подсовывай куки.**
|
||||
Профиль владеет сессией сам (куки продлеваются браузером, а не руками) — это и есть лечение
|
||||
боли «куки протухли».
|
||||
|
||||
## Когда ЭТОТ инструмент, а когда другой
|
||||
|
||||
| Ситуация | Чем работать |
|
||||
|---|---|
|
||||
| Публичная страница без логина | `web_search` / `web_extract` / curl — браузер не нужен |
|
||||
| Личный кабинет, заказы, чеки, внутренний API сайта, антибот | **`ops-browser.sh`** (этот скил) |
|
||||
| Нужны ЕГО логины и ЕГО Chrome (Avito, кабинеты поставщиков) | канал оператора: Hermes `browser_exec` / pi тул `browser` / CC `chrome-devtools` — всё под арендой `driver.lock` |
|
||||
| Посмотреть глазами, кликнуть по живому сайту, показать ему | панель предпросмотра (`desktop_preview` + `drive_preview`) — без JS, только текст/клики |
|
||||
|
||||
`ops-browser` не заменяет канал оператора: там его сессии, здесь — **мой** профиль.
|
||||
И то и другое живёт под правилом «один водитель» (у ops свой замок `ops.lock`).
|
||||
|
||||
## Команды
|
||||
|
||||
```bash
|
||||
OB="$HOME/.config/browser-harness/bin/ops-browser.sh"
|
||||
bash "$OB" start # headless + аренда (окон нет)
|
||||
bash "$OB" open https://site/path # перейти
|
||||
bash "$OB" read 2000 # видимый текст страницы
|
||||
bash "$OB" eval 'fetch("/api/x",{credentials:"include"}).then(r=>r.json()).then(j=>JSON.stringify(j))'
|
||||
bash "$OB" screenshot [path] # PNG, печатает путь
|
||||
bash "$OB" cookies https://site # имена кук (без значений)
|
||||
bash "$OB" status # аренда + CDP + текущая страница
|
||||
bash "$OB" stop # ШТАТНО закрыть и отпустить
|
||||
bash "$OB" handoff <url> # человеку: пароль/2FA/капча (см. ниже)
|
||||
```
|
||||
|
||||
`eval` выполняет JS **в контексте страницы** — значит `fetch()` идёт с её куками и заголовками:
|
||||
так дёргают внутренние эндпоинты приложения (то, что недоступно извне и чего не умеет панель).
|
||||
|
||||
## Хендофф человеку (пароль, 2FA, капча, согласие)
|
||||
|
||||
Останавливаюсь и отдаю экран **сам**, без «скажи готово»:
|
||||
|
||||
1. `bash "$OB" handoff <url>` — закрывает headless (профиль нельзя открыть дважды) и поднимает
|
||||
**видимое** окно ТОГО ЖЕ профиля на нужной странице;
|
||||
2. человек вводит, что нужно (пароль/2FA/капчу вводит только он);
|
||||
3. признак успеха ловлю сам: URL ушёл со страницы логина/auth **или** выросли куки домена;
|
||||
4. штатно закрываю окно и возвращаюсь в headless — логин остался в профиле.
|
||||
|
||||
Скриншот делаю **до** шага (что от человека хотят) и **после** (что получилось); **во время ввода
|
||||
пароля не снимаю**. Если признак успеха не увидел — окно остаётся открытым, а я говорю об этом прямо.
|
||||
|
||||
## Границы (не двигаются)
|
||||
|
||||
- Пароли, 2FA, капчу — **вводит человек**. Я к ним не подхожу.
|
||||
- Деньги, оформление заказов, формы с перс.данными — только с явного согласия оператора.
|
||||
- Куки не выгружаю и не расшифровываю; содержимое залогиненных страниц не уходит в логи/вику.
|
||||
- Антибот-челленджи не обхожу; если сайт невзлюбил `headless` — это повод для `handoff`, а не для трюков.
|
||||
|
||||
## Грабли (стоили времени, проверены живьём)
|
||||
|
||||
- **Закрывать только штатно** (`stop` → `Browser.close`). Жёсткий kill может не сбросить профиль →
|
||||
потеря логина. Сессионные куки (без срока) перезапуск не переживают по дизайну — настоящие логины живут.
|
||||
- **MSYS-пути**: нативным программам нужен вид `C:/...` (`cygpath -m`), а `powershell -File` —
|
||||
только `C:\...` (`cygpath -w`). Иначе «модуль не найден» / «файл .ps1 не найден».
|
||||
- **Экранирование PowerShell внутри bash-строки тихо ломается** (`$_` подменяется) — PS-запросы
|
||||
держим в отдельных `.ps1`.
|
||||
- **Профиль нельзя открыть дважды** — перед видимым окном headless обязан остановиться (это делает `handoff`).
|
||||
- **Профиль**: `~/.config/browser-harness/profiles/ops`; порт CDP: `OPS_CDP_PORT` (9346).
|
||||
|
||||
## Аренда
|
||||
|
||||
```bash
|
||||
BL="$HOME/.config/browser-harness/bin/browser-lease.sh"
|
||||
bash "$BL" --lock ops status # состояние ops-замка (у Chrome оператора — driver.lock)
|
||||
```
|
||||
|
||||
Чужой держатель `ops.lock` → `start` честно отказывает (rc=1). Мёртвый держатель отпускается по PID/TTL.
|
||||
Идёшь в браузер **сам** (в обход `ops-browser.sh`)? Возьми `ops.lock` так же, как любой другой харнесс.
|
||||
|
||||
Контракт: mappa `concepts/ops-browser-contract` (проект `pi-extensions`),
|
||||
требования `requirements:42`, план `plan:35`. Исходники: репо `pi-extensions/scripts/browser/`
|
||||
(установка `just install-browser`).
|
||||
@@ -39,7 +39,7 @@ Karpathy / `.tasks/` (см. using-wiki/using-tasks, legacy-раздел).
|
||||
| `README.md` | minimal stub | Skipped if file exists. |
|
||||
| вики | mappa (`using-wiki`) | сущности `type=wiki` в сервисе; файловый layout — только вне mappa (легаси). |
|
||||
| таски | mappa (`using-tasks`) | сущности `type=task` в сервисе; файловый `.tasks/` — только вне mappa (легаси). |
|
||||
| `AGENTS.md` | `assets/AGENTS.md.template` | **Canon** — skill triggers (`use project wiki`, `use task management system`, etc.). On non-Windows hosts, swap the `we're on Windows` line for `we're on Linux` / `we're on macOS`. On upgrade, the template is treated as a canonical set and merged idempotently — only missing trigger lines are appended after user confirm. Re-runs are no-ops. |
|
||||
| `AGENTS.md` | `assets/AGENTS.md.template` | **Canon** — mappa-agnostic generic triggers (caveman, pull remote before work, tdd, interns, recommend, platform). Mappa-специфичные триггеры (`inbox monitor: raise on start`, `session sync: write to mappa`, `use project wiki`, `use task management system`, `check across all projects`) инжектятся через mappa-bootstrap (Step 5.7.2). On non-Windows hosts, swap the `we're on Windows` line for `we're on Linux` / `we're on macOS`. On upgrade, the template is treated as a canonical set and merged idempotently — only missing generic trigger lines are appended after user confirm. Re-runs are no-ops. |
|
||||
| `CLAUDE.md` | generated pointer | `Canon is AGENTS.md. Read AGENTS.md.` — legacy pointer for tooling that looks for the old name. |
|
||||
| `.wiki/concepts/bootstrap-manifest.md` | generated | Records which skill versions initialized the project, so cross-project layout drift is debuggable. |
|
||||
|
||||
@@ -53,7 +53,7 @@ Karpathy / `.tasks/` (см. using-wiki/using-tasks, legacy-раздел).
|
||||
3. **Steps 1–5.** Create or skip each piece in order — git, README, вики/таски
|
||||
(mappa, см. using-wiki/using-tasks), `AGENTS.md` + `CLAUDE.md` pointer.
|
||||
4. **Step 5.5.** Write `bootstrap-manifest.md` recording the versions of
|
||||
`project-bootstrap`, `project-discipline`, `setup-interns`, and
|
||||
`project-bootstrap`, `setup-interns`, and
|
||||
`using-interns` used.
|
||||
5. **Step 5.6.** Skill dependencies check. Walk the canonical trigger list
|
||||
in `AGENTS.md`, look each up in an embedded `trigger → fulfiller` map,
|
||||
@@ -102,8 +102,9 @@ target with `CLAUDE_SKILLS_DIR=/path bash scripts/install.sh …`.
|
||||
|
||||
- [`using-wiki`](../using-wiki/) — runtime policy for the mappa wiki (v2).
|
||||
- [`using-tasks`](../using-tasks/) — runtime policy for the mappa task board (v2).
|
||||
- [`project-discipline`](../project-discipline/) — cross-project rules
|
||||
activated by the `follow project discipline` trigger.
|
||||
- kzntsv-flavored cross-project discipline (activated per-project by
|
||||
`mappa-bootstrap`, which selects the methodology flavor) — moved to
|
||||
`victor/mappa-vitya-skills` (легаси, поглощено монорепо) — mappa-kzntsv-project-discipline.
|
||||
- [`setup-interns`](../setup-interns/), [`using-interns`](../using-interns/) —
|
||||
pair behind the `delegate to interns when allowed` trigger; cheap-LLM
|
||||
delegation under a per-session permission grant.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: project-bootstrap
|
||||
author: ours
|
||||
version: 3.1.0
|
||||
version: 3.3.0
|
||||
description: >
|
||||
Initializes or upgrades a project in the current folder: git, .gitignore, README.md,
|
||||
.wiki/ using Karpathy's method, .tasks/ for task tracking, AGENTS.md (canon) with
|
||||
@@ -9,6 +9,7 @@ description: >
|
||||
Creates remote Gitea repo and syncs projects-meta cache for greenfield projects.
|
||||
Includes the mappa-bootstrap-project module (v3, решение 4 mappa-as-product):
|
||||
mappa MCP connect + mappa-конвенции + методика-install (версия в манифест).
|
||||
Creates the `.mappa` marker (wiki:3340) so the folder is a mappa project.
|
||||
Use this skill when the user says "initialize project", "bootstrap", "setup project",
|
||||
"upgrade project", "add wiki", "add tasks", "start project", "set everything up",
|
||||
"create new project", or launches the agent in a new folder and wants a full setup.
|
||||
@@ -36,6 +37,7 @@ ls -A 2>/dev/null | grep -q . && echo "empty:no" || echo "empty:yes"
|
||||
[ -d .tasks ] && echo "tasks:yes" || echo "tasks:no"
|
||||
[ -f CLAUDE.md ] && echo "claude:yes" || echo "claude:no"
|
||||
[ -f README.md ] && echo "readme:yes" || echo "readme:no"
|
||||
[ -d .mappa ] && echo "mappa-marker:yes" || echo "mappa-marker:no"
|
||||
```
|
||||
|
||||
Determine mode:
|
||||
@@ -387,21 +389,22 @@ Template contents (`assets/AGENTS.md.template` — source of truth):
|
||||
# Agent instructions. Each line is a trigger for an installed skill.
|
||||
|
||||
talk like a caveman
|
||||
use project wiki
|
||||
use task management system
|
||||
check across all projects
|
||||
pull remote before work
|
||||
follow project discipline
|
||||
follow tdd-criteria
|
||||
delegate to interns when allowed
|
||||
recommend, don't menu
|
||||
we're on Windows
|
||||
```
|
||||
|
||||
The `check across all projects` trigger activates the **mappa** tooling
|
||||
(`mcp__mappa__*`) — cross-project boards, shared wiki and the project
|
||||
registry live in mappa. The file-based `projects-meta-mcp` and its skills
|
||||
(`using-projects-meta`, `setup-projects-meta`, `meta-host-routing`, `setup-wiki`,
|
||||
Mappa-специфичные триггеры (`check across all projects`, `inbox monitor:
|
||||
raise on start`, `use project wiki`, `use task management system`, `session
|
||||
sync: write to mappa`) **не в шаблоне** — инжектятся через mappa-bootstrap
|
||||
(Step 5.7.2), project-bootstrap mappa-agnostic. `check across all projects`
|
||||
активирует **mappa** tooling (`mcp__mappa__*`) — cross-project boards, shared
|
||||
wiki и реестр проектов живут в mappa.
|
||||
|
||||
The file-based `projects-meta-mcp` and its skills (`using-projects-meta`,
|
||||
`setup-projects-meta`, `meta-host-routing`, `setup-wiki`,
|
||||
`setup-tasks`) were removed 2026-08-25; the shared `projects-wiki` files are
|
||||
stubs «не читать, не править» — канон mappa shared-scope.
|
||||
|
||||
@@ -413,15 +416,6 @@ upstream — never auto-merges, stashes, or pushes. Install the skill on the hos
|
||||
if `pulling-before-work` is not in `~/.claude/skills/`; otherwise the trigger is
|
||||
silently dead like any other absent skill.
|
||||
|
||||
The `follow project discipline` line activates the `project-discipline` skill,
|
||||
which codifies four cross-project rules: (1) project AGENTS.md / CLAUDE.md /
|
||||
.wiki/CLAUDE.md / .tasks/ override defaults from any other skill; (2) all work on master/main,
|
||||
no feature branches without explicit user approval; (3) version bump on every
|
||||
edit of versioned artifacts per semver, recorded in commit; (4) commit freely,
|
||||
push only after explicit per-session approval. Install the skill on the host
|
||||
if `project-discipline` is not in `~/.claude/skills/`; otherwise the trigger is
|
||||
silently dead like any other absent skill.
|
||||
|
||||
The `follow tdd-criteria` line activates the `tdd-criteria` skill, which enforces
|
||||
test-driven development by default with four bright-line carve-outs (visual CSS,
|
||||
spike exploration, oneshot scripts, pure wrappers) and four anti-loophole rules
|
||||
@@ -435,8 +429,8 @@ which lets Claude offload predictable bulk I/O and summarization tasks
|
||||
(reading 3+ files, distilling long transcripts) to cheap intern LLMs via the
|
||||
local `interns` MCP server (`mcp__interns__bulk_text_read`,
|
||||
`mcp__interns__transcript_distill`, etc.) — saves Anthropic quota at ~125× the
|
||||
per-call cost reduction on bulk reads. Per-session permission grant mirrors
|
||||
`project-discipline` Rule 4: ask-mode default, conversational grant / revoke,
|
||||
per-call cost reduction on bulk reads. Per-session permission grant mirrors the
|
||||
`mappa-kzntsv-project-discipline` Rule 4 (skill moved from `victor/mappa-vitya-skills`, легаси, поглощено монорепо): ask-mode default, conversational grant / revoke,
|
||||
always-ask paths for `.env` / secrets / keys / SSH credentials even with an
|
||||
active grant, session-end reset. The skill is a no-op until the `interns` MCP
|
||||
server is registered — install via `setup-interns` on a fresh machine if
|
||||
@@ -479,7 +473,6 @@ Skills used to initialize this project's `.wiki/` and `.tasks/` layout, with the
|
||||
| Skill | Version | Role |
|
||||
|---|---|---|
|
||||
| `project-bootstrap` | <version> | orchestrator |
|
||||
| `project-discipline` | <version> | cross-project policy |
|
||||
| `setup-interns` | <version> | interns MCP server install (one-time, per machine) |
|
||||
| `using-interns` | <version> | interns runtime policy + per-session permission grant |
|
||||
| `mappa-*` (методика, модуль 5.7) | <version of reference-package skills> | mappa-циклы: session-orient / task-work / knowledge / messaging / delegation / brainstorm-promote / closing-ritual |
|
||||
@@ -519,13 +512,13 @@ Mismatch between template and map → silent gaps in the recommendation.
|
||||
| `check across all projects` | mappa (`mcp__mappa__*`) | MCP | `mcpServers.mappa` in `~/.claude.json` | — |
|
||||
| `pull remote before work` | `pulling-before-work` | skill | `~/.claude/skills/pulling-before-work/SKILL.md` | `bash scripts/install.sh pulling-before-work` |
|
||||
| `session handoff: read on start, write on end` | `session-handoff` | skill | `~/.claude/skills/session-handoff/SKILL.md` | `bash scripts/install.sh session-handoff` |
|
||||
| `follow project discipline` | `project-discipline` | skill | `~/.claude/skills/project-discipline/SKILL.md` | `bash scripts/install.sh project-discipline` |
|
||||
| `follow tdd-criteria` | `tdd-criteria` | skill | `~/.claude/skills/tdd-criteria/SKILL.md` | `bash scripts/install.sh tdd-criteria` |
|
||||
| `delegate to interns when allowed` | `using-interns` | skill | `~/.claude/skills/using-interns/SKILL.md` | `bash scripts/install.sh using-interns` |
|
||||
| `recommend, don't menu` | `recommend-dont-menu` | skill | `~/.claude/skills/recommend-dont-menu/SKILL.md` | `bash scripts/install.sh recommend-dont-menu` |
|
||||
| `use project wiki` | `mappa-knowledge` | skill | см. mappa-bootstrap (репо mappa) | `cd <mappa-repo> && bash skills/mappa-bootstrap/assets/install.sh` |
|
||||
| `use task management system` | `mappa-task-work` | skill | см. mappa-bootstrap (репо mappa) | `cd <mappa-repo> && bash skills/mappa-bootstrap/assets/install.sh` |
|
||||
| `inbox monitor: raise on start` | `mappa-session-orient` | skill | см. mappa-bootstrap (репо mappa) | `cd <mappa-repo> && bash skills/mappa-bootstrap/assets/install.sh` |
|
||||
| `session sync: write to mappa` | `mappa-session-orient` | skill | см. mappa-bootstrap (репо mappa) | `cd <mappa-repo> && bash skills/mappa-bootstrap/assets/install.sh` |
|
||||
| `we're on Windows` / `we're on Linux` / `we're on macOS` | `active-platform` | skill | `~/.claude/skills/active-platform/SKILL.md` | `bash scripts/install.sh active-platform` |
|
||||
|
||||
### Algorithm
|
||||
@@ -636,14 +629,22 @@ python -c "import json; d=json.load(open('$HOME/.claude.json')); print('mappa' i
|
||||
mappa-конвенции в AGENTS.md (5.7.2) можно добавлять и без MCP-регистрации
|
||||
— триггеры будут ждать установки сервера (как любой absent-скилл).
|
||||
|
||||
### 5.7.2 — mappa-конвенции в AGENTS.md (idempotent merge)
|
||||
### 5.7.2 — mappa-конвенции в AGENTS.md (инъекция через mappa-bootstrap)
|
||||
|
||||
mappa-специфичные триггеры уже в каноне шаблона (Step 5) — `inbox monitor:
|
||||
raise on start`, `use project wiki`, `use task management system`. Это не
|
||||
отдельный merge: существующая идемпотентная машинерия Step 5 покрывает их.
|
||||
Модуль только **верифицирует**: после Step 5 убедиться, что строки на месте
|
||||
(та же substring-проверка что в Step 5 upgrade-merge). Если пользователь
|
||||
сознательно убрал их из AGENTS.md — не возвращать (уважать выбор).
|
||||
`project-bootstrap` — mappa-agnostic: mappa-триггеры **не хардкодятся** в
|
||||
шаблоне (см. `assets/AGENTS.md.template`). Инъекция канонического набора
|
||||
mappa-триггеров (`inbox monitor: raise on start`, `session sync: write to
|
||||
mappa`, `use project wiki`, `use task management system`, `check across all
|
||||
projects`) — через скил `mappa-bootstrap` (репо mappa, единый источник):
|
||||
|
||||
```bash
|
||||
bash <mappa>/skills-core/mappa-bootstrap/assets/install.sh --triggers <project-dir>
|
||||
```
|
||||
|
||||
После инъекции — **верифицировать** наличие строк (substring-проверка, та же
|
||||
машинерия что Step 5 upgrade-merge). Если пользователь сознательно убрал
|
||||
mappa-триггеры из AGENTS.md — не возвращать (уважать выбор). Источник истины
|
||||
набора — mappa-bootstrap, НЕ шаблон project-bootstrap.
|
||||
|
||||
### 5.7.3 — методика-install (пакет из репо mappa, версия в манифест)
|
||||
|
||||
@@ -694,6 +695,46 @@ install-команда — скил mappa-bootstrap, см. 5.7.3).
|
||||
|
||||
---
|
||||
|
||||
## Step 5.8 — `.mappa` маркер (контракт wiki:3340)
|
||||
|
||||
Машиночитаемый маркер проекта в корне папки (схема v1 — `.mappa/config.yaml`):
|
||||
гейт mappa-скилов («без маркера папка не участвует в mappa-операциях»,
|
||||
task:1546) + признак корня проекта для харнессов. Создаётся на bootstrap —
|
||||
без ручного прогона генератора (task:1583). Детерминированный рендер:
|
||||
фиксированный порядок полей, без секретов, без timestamp — повторный запуск
|
||||
no-op (`keep`).
|
||||
|
||||
1. **Собрать значения** (реестр mappa → локальное знание):
|
||||
- `project` — канон папки (`basename "$PWD"`); если проект уже в реестре
|
||||
(`projects_resolve`) — сверить, не расходится ли;
|
||||
- `tenant` — `MAPPA_TENANT` (по умолчанию `vitya`);
|
||||
- `url` — `MAPPA_CORE_URL` (без trailing slash);
|
||||
- `git_provider` — из реестра `projects.git_provider` (например `gitea`),
|
||||
иначе из шага 1.5 (создано через Gitea API → gitea); опционально;
|
||||
- `git` — `projects.qualified` (owner/repo) из реестра, иначе из remote
|
||||
шага 1.5; опционально (опустить, если неизвестно).
|
||||
2. **Записать маркер** (скрипт — ассет этого скила, реализует контракт
|
||||
wiki:3340; в репо: `skills/project-bootstrap/assets/dot_mappa_marker.py`):
|
||||
|
||||
```bash
|
||||
python assets/dot_mappa_marker.py write \
|
||||
--project "$(basename "$PWD")" --tenant vitya --url "$MAPPA_CORE_URL" \
|
||||
--git-provider gitea --git "$OWNER/$REPO"
|
||||
```
|
||||
|
||||
Без `--git-provider`/`--git`, если поля неизвестны. Повторный прогон —
|
||||
no-op (`keep`); отличающийся существующий маркер без `--force` НЕ
|
||||
перезаписывается — покажи diff и спроси (правило «never overwrite»).
|
||||
3. **Верифицировать**: `python assets/dot_mappa_marker.py check` → exit 0.
|
||||
4. **Контракт-тест** (TDD, task:1583): `python assets/test_dot_mappa_marker.py`
|
||||
— «после bootstrap есть `.mappa/config.yaml`», детерминизм, без секретов,
|
||||
порядок полей, идемпотентность.
|
||||
|
||||
Маркер публичен (без секретов) и попадает в коммит шага 6. Валидный
|
||||
существующий маркер не трогаем.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Commit
|
||||
|
||||
```bash
|
||||
@@ -718,6 +759,7 @@ Print a final report:
|
||||
✅ Done! Created:
|
||||
.wiki/ — project wiki (Karpathy method)
|
||||
.tasks/ — task tracking system
|
||||
.mappa/ — mappa project marker (wiki:3340, schema v1)
|
||||
AGENTS.md — skill triggers (canon)
|
||||
CLAUDE.md — legacy pointer
|
||||
.gitignore — standard template
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# AGENTS.md
|
||||
# Agent instructions. Each line is a trigger for an installed skill.
|
||||
#
|
||||
# Inter-session mail channel is Mappa (mcp__mappa__inbox_send/inbox_monitor),
|
||||
# NOT files. This line opts the project into inbox delivery at session start:
|
||||
inbox monitor: raise on start
|
||||
# mappa-специфичные триггеры (inbox monitor: raise on start, session sync:
|
||||
# write to mappa, use project wiki, use task management system, check across
|
||||
# all projects) НЕ хардкодятся здесь — project-bootstrap mappa-agnostic. Они
|
||||
# инжектятся при создании/апгрейде проекта через mappa-bootstrap:
|
||||
# bash <mappa>/skills-core/mappa-bootstrap/assets/install.sh --triggers <dir>
|
||||
# (см. Step 5.7.2 — mappa-конвенции в AGENTS.md, делегирование в mappa-bootstrap)
|
||||
|
||||
# Search rule
|
||||
|
||||
@@ -16,12 +19,7 @@ result file names, NOT directory traversal — it still walks every node_modules
|
||||
entry; `| grep -v node_modules` filters after the walk, doesn't save you.
|
||||
|
||||
talk like a caveman
|
||||
use project wiki
|
||||
use task management system
|
||||
check across all projects
|
||||
pull remote before work
|
||||
session handoff: read on start, write on end
|
||||
follow project discipline
|
||||
follow tdd-criteria
|
||||
delegate to interns when allowed
|
||||
recommend, don't menu
|
||||
|
||||
263
skills/project-bootstrap/assets/dot_mappa_marker.py
Normal file
263
skills/project-bootstrap/assets/dot_mappa_marker.py
Normal file
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""dot_mappa_marker.py — deterministic render + write of the `.mappa` marker.
|
||||
|
||||
Contract: mappa wiki:3340 (concepts/dot-mappa-marker), schema v1.
|
||||
Used by project-create (step 5.5) and project-bootstrap (step 5.8) so a project
|
||||
folder gets its marker at create time — no manual generator run needed
|
||||
(task:1583). The batch generator (mappa `server/scripts/gen-dot-mappa-markers.ts`)
|
||||
remains for registry-wide migration; this is the per-project create path.
|
||||
|
||||
Guarantees (the contract):
|
||||
* `.mappa/config.yaml` — каталог + файл внутри
|
||||
* fixed field order (schema_version, protocol_version, project, tenant, url,
|
||||
git_provider?, git?)
|
||||
* deterministic render — no timestamps, same input → same bytes
|
||||
* NO secrets — only public registry fields; url with credentials is rejected
|
||||
* optional fields (`git_provider`, `git`) omitted when absent
|
||||
* idempotent write: same content → no-op (keep); different content → refuse
|
||||
without --force
|
||||
|
||||
Usage:
|
||||
python dot_mappa_marker.py render --project NAME --tenant TENANT --url URL \
|
||||
[--git-provider P] [--git OWNER/REPO] # print content to stdout
|
||||
python dot_mappa_marker.py write --project NAME --tenant TENANT --url URL \
|
||||
[--git-provider P] [--git OWNER/REPO] [--dir PATH] [--force] # write marker
|
||||
python dot_mappa_marker.py check --dir PATH # verify existing marker
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
# Canonical header comment — same as the contract example (wiki:3340).
|
||||
HEADER = "# mappa project marker — machine-readable identifier of a mappa project folder"
|
||||
|
||||
# YAML: these are indicator characters / reserved tokens — never plain.
|
||||
_INDICATOR_START = set("!&*{}[],#|>@`\"'%?:~-")
|
||||
_RESERVED_PLAIN = {"null", "Null", "NULL", "~", "true", "True", "TRUE", "false",
|
||||
"False", "FALSE", "yes", "Yes", "YES", "no", "No", "NO",
|
||||
"on", "On", "ON", "off", "Off", "OFF", "-", "?", ":"}
|
||||
|
||||
|
||||
class MarkerConflict(Exception):
|
||||
"""An existing marker differs from the requested one and --force is absent."""
|
||||
|
||||
|
||||
def yaml_scalar(value: str) -> str:
|
||||
"""YAML plain-safe scalar: plain without quotes when safe, else double-quoted.
|
||||
|
||||
Plain-safe = non-empty, no leading indicator, not a reserved token, no flow
|
||||
chars, no embedded newlines, no surrounding whitespace. Double-quoting uses
|
||||
JSON escaping, which is a valid subset of YAML double-quoted style.
|
||||
"""
|
||||
s = str(value)
|
||||
if s == "":
|
||||
return '""'
|
||||
if s.strip() != s:
|
||||
return json.dumps(s)
|
||||
if s[0] in _INDICATOR_START or s in _RESERVED_PLAIN:
|
||||
return json.dumps(s)
|
||||
if s.startswith(("- ", "? ", ": ")):
|
||||
return json.dumps(s)
|
||||
# plain scalars stay plain unless they would confuse the parser:
|
||||
# ": " (mapping indicator), trailing ":", " #" (comment), newlines/tabs
|
||||
if ": " in s or s.endswith(":") or " #" in s or "\n" in s or "\t" in s:
|
||||
return json.dumps(s)
|
||||
return s
|
||||
|
||||
|
||||
def validate_folder_name(name: str) -> str:
|
||||
"""A folder name (canon/tenant/git_provider) must be a single sane segment."""
|
||||
if not name or name in (".", ".."):
|
||||
raise ValueError(f"invalid name {name!r}: must be a non-empty folder name")
|
||||
if any(sep in name for sep in ("/", "\\", "\x00")):
|
||||
raise ValueError(f"invalid name {name!r}: must be a single path segment")
|
||||
if name != name.strip():
|
||||
raise ValueError(f"invalid name {name!r}: no surrounding whitespace allowed")
|
||||
return name
|
||||
|
||||
|
||||
def validate_git_ref(git: str) -> str:
|
||||
"""`git` = projects.qualified (owner/repo) — no credentials, no colon."""
|
||||
g = str(git)
|
||||
if not g or "/" not in g:
|
||||
raise ValueError(f"invalid git ref {g!r}: expected owner/repo")
|
||||
if any(c in g for c in ("@", ":", " ", "\t", "\n", "\\")):
|
||||
raise ValueError(f"invalid git ref {g!r}: no credentials / separators allowed")
|
||||
if g.startswith("/") or g.endswith("/") or ".." in g.split("/"):
|
||||
raise ValueError(f"invalid git ref {g!r}: must be owner/repo, not a path")
|
||||
return g
|
||||
|
||||
|
||||
def normalize_url(url: str) -> str:
|
||||
"""Absolute http(s) URL without credentials and without trailing slash."""
|
||||
u = str(url).strip()
|
||||
if not (u.startswith("http://") or u.startswith("https://")):
|
||||
raise ValueError(f"invalid url {u!r}: must be http(s)://host...")
|
||||
authority = u.split("://", 1)[1].split("/", 1)[0]
|
||||
if "@" in authority:
|
||||
raise ValueError("url must not contain credentials (no secrets in the marker)")
|
||||
return u.rstrip("/")
|
||||
|
||||
|
||||
def render(
|
||||
project: str,
|
||||
tenant: str,
|
||||
url: str,
|
||||
git_provider: str | None = None,
|
||||
git: str | None = None,
|
||||
) -> str:
|
||||
"""Deterministic `.mappa/config.yaml` content per wiki:3340 schema v1."""
|
||||
project = validate_folder_name(project)
|
||||
tenant = validate_folder_name(tenant)
|
||||
url = normalize_url(url)
|
||||
lines = [
|
||||
HEADER,
|
||||
f"schema_version: {SCHEMA_VERSION}",
|
||||
f"protocol_version: {PROTOCOL_VERSION}",
|
||||
f"project: {yaml_scalar(project)}",
|
||||
f"tenant: {yaml_scalar(tenant)}",
|
||||
f"url: {yaml_scalar(url)}",
|
||||
]
|
||||
if git_provider:
|
||||
lines.append(f"git_provider: {yaml_scalar(validate_folder_name(git_provider))}")
|
||||
if git:
|
||||
lines.append(f"git: {yaml_scalar(validate_git_ref(git))}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _sane_dir(directory: str | Path) -> Path:
|
||||
"""Resolve the target directory; reject `..` segments and non-dirs."""
|
||||
p = Path(directory)
|
||||
if ".." in p.parts:
|
||||
raise ValueError(f"invalid directory {str(directory)!r}: '..' segments not allowed")
|
||||
if p.exists() and not p.is_dir():
|
||||
raise ValueError(f"invalid directory {str(directory)!r}: not a directory")
|
||||
return p
|
||||
|
||||
|
||||
def write_marker(directory: str | Path, content: str, force: bool = False) -> tuple[Path, str]:
|
||||
"""Write `.mappa/config.yaml` under `directory`.
|
||||
|
||||
Returns (marker_path, outcome) where outcome is one of
|
||||
"created" | "keep" (idempotent no-op) | "overwrite" (force).
|
||||
Raises MarkerConflict when an existing marker differs and force is False.
|
||||
"""
|
||||
marker = _sane_dir(directory) / ".mappa" / "config.yaml"
|
||||
if marker.exists():
|
||||
existing = marker.read_text(encoding="utf-8")
|
||||
if existing == content:
|
||||
return marker, "keep"
|
||||
if not force:
|
||||
raise MarkerConflict(
|
||||
f"{marker} already exists with different content; "
|
||||
"pass --force to overwrite (contract: no silent overwrite)"
|
||||
)
|
||||
marker.write_text(content, encoding="utf-8")
|
||||
return marker, "overwrite"
|
||||
marker.parent.mkdir(parents=True, exist_ok=True)
|
||||
marker.write_text(content, encoding="utf-8")
|
||||
return marker, "created"
|
||||
|
||||
|
||||
def _parse_marker_lines(body: str) -> list[tuple[str, str]]:
|
||||
"""(key, value) pairs of data lines — comments skipped, first colon splits."""
|
||||
pairs = []
|
||||
for line in body.splitlines():
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if ": " not in line:
|
||||
raise ValueError(f"malformed marker line (no 'key: value'): {line!r}")
|
||||
key, value = line.split(": ", 1)
|
||||
pairs.append((key, value.strip()))
|
||||
return pairs
|
||||
|
||||
|
||||
def check_marker(directory: str | Path) -> tuple[bool, str]:
|
||||
"""Gate check (wiki:3340 / task:1546): is `directory` a mappa project?
|
||||
|
||||
Returns (ok, message). ok means `.mappa/config.yaml` exists and its data
|
||||
lines start with exactly the required fields (schema_version,
|
||||
protocol_version, project, tenant, url) in canonical order with valid
|
||||
values; optional `git_provider`/`git` may follow.
|
||||
"""
|
||||
marker = _sane_dir(directory) / ".mappa" / "config.yaml"
|
||||
if not marker.is_file():
|
||||
return False, f"no marker: {marker} (folder without marker is not a mappa project)"
|
||||
try:
|
||||
pairs = _parse_marker_lines(marker.read_text(encoding="utf-8"))
|
||||
except ValueError as e:
|
||||
return False, f"marker {marker}: {e}"
|
||||
if len(pairs) < 5:
|
||||
return False, f"marker {marker}: fewer than the 5 required fields"
|
||||
required = ["schema_version", "protocol_version", "project", "tenant", "url"]
|
||||
if [k for k, _ in pairs[:5]] != required:
|
||||
return False, f"marker {marker}: field order mismatch ({[k for k, _ in pairs[:5]]})"
|
||||
values = dict(pairs)
|
||||
if values["schema_version"] != str(SCHEMA_VERSION):
|
||||
return False, f"marker {marker}: schema_version must be {SCHEMA_VERSION}"
|
||||
if values["protocol_version"] != str(PROTOCOL_VERSION):
|
||||
return False, f"marker {marker}: protocol_version must be {PROTOCOL_VERSION}"
|
||||
try:
|
||||
validate_folder_name(values["project"])
|
||||
validate_folder_name(values["tenant"])
|
||||
normalize_url(values["url"])
|
||||
if "git_provider" in values:
|
||||
validate_folder_name(values["git_provider"])
|
||||
if "git" in values:
|
||||
validate_git_ref(values["git"])
|
||||
except ValueError as e:
|
||||
return False, f"marker {marker}: {e}"
|
||||
return True, f"marker ok: {marker}"
|
||||
|
||||
|
||||
def _add_common(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--project", required=True, help="канон папки = реестр projects.name (slug)")
|
||||
parser.add_argument("--tenant", required=True, help="тенант, где живёт проект (MAPPA_TENANT)")
|
||||
parser.add_argument("--url", required=True, help="MAPPA_CORE_URL (без trailing slash)")
|
||||
parser.add_argument("--git-provider", default=None, help="projects.git_provider (gitea/...) — опционально")
|
||||
parser.add_argument("--git", default=None, help="projects.qualified (owner/repo) — опционально")
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=".mappa marker per wiki:3340 (schema v1)")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_render = sub.add_parser("render", help="print deterministic marker content")
|
||||
_add_common(p_render)
|
||||
|
||||
p_write = sub.add_parser("write", help="write .mappa/config.yaml into a folder")
|
||||
_add_common(p_write)
|
||||
p_write.add_argument("--dir", default=".", help="project folder (default: cwd)")
|
||||
p_write.add_argument("--force", action="store_true", help="overwrite a differing marker")
|
||||
|
||||
p_check = sub.add_parser("check", help="gate check: is the folder a mappa project?")
|
||||
p_check.add_argument("--dir", default=".", help="project folder (default: cwd)")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.cmd in ("render", "write"):
|
||||
content = render(args.project, args.tenant, args.url, args.git_provider, args.git)
|
||||
if args.cmd == "render":
|
||||
sys.stdout.write(content)
|
||||
return 0
|
||||
marker, outcome = write_marker(args.dir, content, force=args.force)
|
||||
print(f"{outcome}: {marker}")
|
||||
return 0
|
||||
|
||||
if args.cmd == "check":
|
||||
ok, msg = check_marker(args.dir)
|
||||
print(msg)
|
||||
return 0 if ok else 1
|
||||
|
||||
return 2 # unreachable
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
290
skills/project-bootstrap/assets/test_dot_mappa_marker.py
Normal file
290
skills/project-bootstrap/assets/test_dot_mappa_marker.py
Normal file
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Contract test for the `.mappa` marker — mappa wiki:3340 (concepts/dot-mappa-marker).
|
||||
|
||||
The contract under test (task:1583): after the project-create/bootstrap marker
|
||||
step, the project folder contains `.mappa/config.yaml` matching schema v1:
|
||||
fixed field order, deterministic render (no timestamps), NO secrets, optional
|
||||
fields (`git_provider`, `git`) omitted when absent, idempotent write.
|
||||
|
||||
Run: python test_dot_mappa_marker.py (or: python -m unittest test_dot_mappa_marker)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import dot_mappa_marker as dmm # noqa: E402
|
||||
|
||||
CANON = "skills" # registry projects.name — канон папки (slug)
|
||||
TENANT = "vitya"
|
||||
URL = "https://mappa.vds.kzntsv.site"
|
||||
GIT_PROVIDER = "gitea"
|
||||
GIT = "OpeItcLoc03/skills"
|
||||
|
||||
FIELD_ORDER = [
|
||||
"schema_version",
|
||||
"protocol_version",
|
||||
"project",
|
||||
"tenant",
|
||||
"url",
|
||||
"git_provider",
|
||||
"git",
|
||||
]
|
||||
|
||||
|
||||
def field_keys(body: str) -> list[str]:
|
||||
return [
|
||||
line.split(":", 1)[0]
|
||||
for line in body.splitlines()
|
||||
if line and not line.startswith("#") and ": " in line
|
||||
]
|
||||
|
||||
|
||||
def write_contract_marker(tmp: str) -> Path:
|
||||
"""Helper: create a valid marker as the bootstrap step would."""
|
||||
marker, outcome = dmm.write_marker(tmp, dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT))
|
||||
assert outcome == "created"
|
||||
return marker
|
||||
|
||||
|
||||
class ContractTests(unittest.TestCase):
|
||||
"""Contract: after bootstrap there is `.mappa/config.yaml` (task:1583)."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.tmp = tempfile.mkdtemp(prefix="mappa-marker-test-")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
# --- presence / shape -------------------------------------------------
|
||||
|
||||
def test_bootstrap_marker_step_creates_config_yaml(self) -> None:
|
||||
"""The bootstrap marker step leaves `.mappa/config.yaml` in the folder."""
|
||||
content = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT)
|
||||
marker, outcome = dmm.write_marker(self.tmp, content)
|
||||
self.assertEqual(outcome, "created")
|
||||
self.assertTrue(marker.is_file())
|
||||
self.assertEqual(marker.name, "config.yaml")
|
||||
self.assertEqual(marker.parent.name, ".mappa")
|
||||
|
||||
def test_fixed_field_order(self) -> None:
|
||||
body = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT)
|
||||
self.assertEqual(field_keys(body), FIELD_ORDER)
|
||||
|
||||
def test_deterministic_render_no_timestamp(self) -> None:
|
||||
a = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT)
|
||||
b = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT)
|
||||
self.assertEqual(a, b)
|
||||
# no ISO-date-like content
|
||||
self.assertNotRegex(a, r"\d{4}-\d{2}-\d{2}")
|
||||
|
||||
def test_optional_fields_omitted_when_absent(self) -> None:
|
||||
body = dmm.render(CANON, TENANT, URL)
|
||||
self.assertEqual(field_keys(body), FIELD_ORDER[:5])
|
||||
self.assertNotIn("git_provider", body)
|
||||
self.assertNotIn("\ngit:", body)
|
||||
|
||||
def test_no_secrets_in_marker(self) -> None:
|
||||
body = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT)
|
||||
lowered = body.lower()
|
||||
# credentials in the url authority are rejected separately
|
||||
for secret in ("token", "password", "secret", "api_key", "key:", "@"):
|
||||
self.assertNotIn(secret, lowered)
|
||||
|
||||
# --- idempotent write --------------------------------------------------
|
||||
|
||||
def test_idempotent_write_keeps_same_content(self) -> None:
|
||||
content = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT)
|
||||
marker, first = dmm.write_marker(self.tmp, content)
|
||||
marker, second = dmm.write_marker(self.tmp, content)
|
||||
self.assertEqual(first, "created")
|
||||
self.assertEqual(second, "keep")
|
||||
self.assertEqual(marker.read_text(encoding="utf-8"), content)
|
||||
|
||||
def test_refuses_overwrite_of_different_marker_without_force(self) -> None:
|
||||
dmm.write_marker(self.tmp, dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT))
|
||||
with self.assertRaises(dmm.MarkerConflict):
|
||||
dmm.write_marker(self.tmp, dmm.render(CANON, TENANT, URL, "github", GIT))
|
||||
|
||||
def test_force_overwrites_different_marker(self) -> None:
|
||||
dmm.write_marker(self.tmp, dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT))
|
||||
marker, outcome = dmm.write_marker(
|
||||
self.tmp, dmm.render(CANON, TENANT, URL, "github", GIT), force=True
|
||||
)
|
||||
self.assertEqual(outcome, "overwrite")
|
||||
self.assertIn("git_provider: github", marker.read_text(encoding="utf-8"))
|
||||
|
||||
# --- input validation ---------------------------------------------------
|
||||
|
||||
def test_folder_name_path_segments_rejected(self) -> None:
|
||||
for bad in ("../evil", "a/b", "a\\b", ".", "..", ""):
|
||||
with self.assertRaises(ValueError, msg=f"name {bad!r} must be rejected"):
|
||||
dmm.render(bad, TENANT, URL)
|
||||
|
||||
def test_url_trailing_slash_stripped_but_path_kept(self) -> None:
|
||||
body = dmm.render(CANON, TENANT, URL + "//")
|
||||
self.assertIn(f"url: {URL}", body)
|
||||
# a trailing slash after a path must be stripped, the path kept
|
||||
body2 = dmm.render(CANON, TENANT, "https://example.com/mappa/")
|
||||
self.assertIn("url: https://example.com/mappa", body2)
|
||||
|
||||
def test_url_with_credentials_rejected(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
dmm.render(CANON, TENANT, "https://user:pass@mappa.vds.kzntsv.site")
|
||||
|
||||
def test_url_scheme_restricted_to_http_https(self) -> None:
|
||||
for bad in ("ftp://mappa.example", "javascript://x", "mappa.vds.kzntsv.site", "://x"):
|
||||
with self.assertRaises(ValueError, msg=f"url {bad!r} must be rejected"):
|
||||
dmm.render(CANON, TENANT, bad)
|
||||
|
||||
def test_git_ref_with_credentials_rejected(self) -> None:
|
||||
for bad in ("user:pass@host/repo", "victor/repo@token", "../config", "/owner/repo", "owner/repo/", "owner repo", "norepo"):
|
||||
with self.assertRaises(ValueError, msg=f"git {bad!r} must be rejected"):
|
||||
dmm.render(CANON, TENANT, URL, GIT_PROVIDER, bad)
|
||||
|
||||
def test_directory_with_parent_segments_rejected(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
dmm.write_marker("some/../elsewhere", dmm.render(CANON, TENANT, URL))
|
||||
with self.assertRaises(ValueError):
|
||||
dmm.check_marker("../etc")
|
||||
|
||||
# --- YAML scalar edge cases -------------------------------------------
|
||||
|
||||
def test_yaml_scalar_quoting_edge_cases(self) -> None:
|
||||
# reserved tokens / indicators must be double-quoted (never plain)
|
||||
for special in ("~", "@host", "-", "?", ":", "null", "yes", "on", "true",
|
||||
"a: b", "ends:", " #lead", "has tab\tinside"):
|
||||
self.assertTrue(dmm.yaml_scalar(special).startswith('"'),
|
||||
f"{special!r} must be double-quoted, got {dmm.yaml_scalar(special)!r}")
|
||||
# plain-safe values stay plain
|
||||
for plain in ("vitya", "OpeItcLoc03/skills", "https://mappa.vds.kzntsv.site",
|
||||
"a:b", "x#y", "lead#ing", "my-proj"):
|
||||
self.assertEqual(dmm.yaml_scalar(plain), plain)
|
||||
self.assertEqual(dmm.yaml_scalar(""), '""')
|
||||
|
||||
# --- check_marker (gate) ----------------------------------------------
|
||||
|
||||
def test_check_ok_on_valid_marker(self) -> None:
|
||||
write_contract_marker(self.tmp)
|
||||
ok, msg = dmm.check_marker(self.tmp)
|
||||
self.assertTrue(ok, msg)
|
||||
|
||||
def test_check_fails_on_missing_marker(self) -> None:
|
||||
ok, _ = dmm.check_marker(self.tmp)
|
||||
self.assertFalse(ok)
|
||||
|
||||
def test_check_fails_on_wrong_field_order(self) -> None:
|
||||
(Path(self.tmp) / ".mappa").mkdir()
|
||||
(Path(self.tmp) / ".mappa" / "config.yaml").write_text(
|
||||
"# c\nproject: skills\nschema_version: 1\nprotocol_version: 1\n"
|
||||
"tenant: vitya\nurl: https://mappa.vds.kzntsv.site\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = dmm.check_marker(self.tmp)
|
||||
self.assertFalse(ok)
|
||||
|
||||
def test_check_fails_on_extra_field_before_required(self) -> None:
|
||||
(Path(self.tmp) / ".mappa").mkdir()
|
||||
(Path(self.tmp) / ".mappa" / "config.yaml").write_text(
|
||||
"extra: sneaky\nschema_version: 1\nprotocol_version: 1\n"
|
||||
"project: skills\ntenant: vitya\nurl: https://mappa.vds.kzntsv.site\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = dmm.check_marker(self.tmp)
|
||||
self.assertFalse(ok)
|
||||
|
||||
def test_check_fails_on_wrong_versions(self) -> None:
|
||||
(Path(self.tmp) / ".mappa").mkdir()
|
||||
(Path(self.tmp) / ".mappa" / "config.yaml").write_text(
|
||||
"schema_version: 2\nprotocol_version: 1\nproject: skills\n"
|
||||
"tenant: vitya\nurl: https://mappa.vds.kzntsv.site\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = dmm.check_marker(self.tmp)
|
||||
self.assertFalse(ok)
|
||||
|
||||
def test_check_fails_on_malicious_project_value(self) -> None:
|
||||
(Path(self.tmp) / ".mappa").mkdir()
|
||||
(Path(self.tmp) / ".mappa" / "config.yaml").write_text(
|
||||
"schema_version: 1\nprotocol_version: 1\nproject: ../../evil\n"
|
||||
"tenant: vitya\nurl: https://mappa.vds.kzntsv.site\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = dmm.check_marker(self.tmp)
|
||||
self.assertFalse(ok)
|
||||
|
||||
def test_check_fails_on_malformed_line(self) -> None:
|
||||
(Path(self.tmp) / ".mappa").mkdir()
|
||||
(Path(self.tmp) / ".mappa" / "config.yaml").write_text(
|
||||
"schema_version: 1\nprotocol_version: 1\nproject skills\n"
|
||||
"tenant: vitya\nurl: https://mappa.vds.kzntsv.site\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = dmm.check_marker(self.tmp)
|
||||
self.assertFalse(ok)
|
||||
|
||||
def test_check_accepts_url_with_port(self) -> None:
|
||||
(Path(self.tmp) / ".mappa").mkdir()
|
||||
(Path(self.tmp) / ".mappa" / "config.yaml").write_text(
|
||||
"schema_version: 1\nprotocol_version: 1\nproject: skills\n"
|
||||
"tenant: vitya\nurl: https://mappa.example:8443\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = dmm.check_marker(self.tmp)
|
||||
self.assertTrue(ok)
|
||||
|
||||
# --- CLI end-to-end ----------------------------------------------------
|
||||
|
||||
def test_cli_write_creates_marker(self) -> None:
|
||||
"""End-to-end: the documented CLI command produces the marker."""
|
||||
proc = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(Path(__file__).resolve().parent / "dot_mappa_marker.py"),
|
||||
"write",
|
||||
"--project", CANON,
|
||||
"--tenant", TENANT,
|
||||
"--url", URL,
|
||||
"--git-provider", GIT_PROVIDER,
|
||||
"--git", GIT,
|
||||
"--dir", self.tmp,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
marker = Path(self.tmp) / ".mappa" / "config.yaml"
|
||||
self.assertTrue(marker.is_file())
|
||||
self.assertEqual(field_keys(marker.read_text(encoding="utf-8")), FIELD_ORDER)
|
||||
|
||||
def test_cli_check_verifies_marker(self) -> None:
|
||||
write_contract_marker(self.tmp)
|
||||
script = Path(__file__).resolve().parent / "dot_mappa_marker.py"
|
||||
ok = subprocess.run(
|
||||
[sys.executable, str(script), "check", "--dir", self.tmp],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(ok.returncode, 0, ok.stderr)
|
||||
# check on an empty dir fails (gate semantics: no marker → not a mappa project)
|
||||
empty = tempfile.mkdtemp(prefix="mappa-marker-empty-")
|
||||
try:
|
||||
missing = subprocess.run(
|
||||
[sys.executable, str(script), "check", "--dir", empty],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertNotEqual(missing.returncode, 0)
|
||||
finally:
|
||||
shutil.rmtree(empty, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -1,84 +0,0 @@
|
||||
---
|
||||
name: project-create
|
||||
author: ours
|
||||
version: 0.1.1
|
||||
description: >
|
||||
Mappa-side cycle of creating a new project: ask the operator for the hosting
|
||||
address (platform + user/org) FIRST — never derive it from neighbouring
|
||||
projects — then pre-flight checks (free in mappa AND in gitea), then create
|
||||
mappa registration and the gitea repo SIMULTANEOUSLY (repo via .admin: task
|
||||
+ covering letter, priority P0, no paired review for ops), then ask where on
|
||||
disk the project folder goes and what it's named, create the folder, and
|
||||
hand over to project-bootstrap (general skill). Triggers (bilingual):
|
||||
«создай проект», «заведи проект», «новый проект», «создать проект на
|
||||
гите», "create a project", "start a new project", "set up a project",
|
||||
«куда разместить проект». NOT repo content/bootstrap (→ project-bootstrap),
|
||||
NOT renaming/re-hosting an existing project.
|
||||
---
|
||||
|
||||
# Project Create (mappa-side)
|
||||
|
||||
The cycle of creating a **new project** from the mappa side. Built live on the
|
||||
tg-digest case (2026-08-27, brainstorm:129): the operator ran the pattern
|
||||
through a real creation, the skill records it. Guarantees that the hosting
|
||||
address is the operator's decision (not the agent's guess), that mappa and
|
||||
gitea stay in sync (created together, not one before the other), and that the
|
||||
operator picks the on-disk location.
|
||||
|
||||
## When to use
|
||||
|
||||
Before any `projects_register` / gitea repo creation for a **new** project.
|
||||
|
||||
**Activates:** «создай проект», «заведи проект», «новый проект», «создать проект на гите», "create a project", "start a new project", "set up a project".
|
||||
|
||||
**Not applicable:**
|
||||
- Repo content / bootstrap (git init, README, AGENTS.md, .wiki, .tasks, remote push) → `project-bootstrap` (general skill; project-create hands over to it).
|
||||
- Re-hosting / renaming an existing project → read what's there first, no creation.
|
||||
- A project that already exists in mappa registry or gitea → verify with the operator, don't create a duplicate.
|
||||
|
||||
## Hard rule (the original lesson)
|
||||
|
||||
**The hosting address (platform + user/org) is the operator's decision. ASK, never derive.** Do not copy the owner from a neighbouring project (the live failure: `victor/tg-digest` was assumed by analogy with `victor/yt-digest` — wrong, the operator had to correct it). Options to offer: platform (Gitea git.kzntsv.site / GitHub), and for Gitea — the known users/orgs from the registry (e.g. `victor` personal apps, `OpeItcLoc03` infra/meta, `cancel_music` music modules). The registry `mcp__mappa__projects_list` shows the existing `qualified` names — use it to propose options, but the choice is the operator's.
|
||||
|
||||
## Steps (the pattern)
|
||||
|
||||
1. **Ask the hosting address.** Platform (Gitea/GitHub) + user/org. One question, offer the registry-derived options, wait for the answer. Do NOT register anything yet.
|
||||
2. **Pre-flight checks** — can we create in BOTH places:
|
||||
- mappa: the project name is not in the registry (`projects_list` / `projects_resolve`).
|
||||
- gitea: repo name is free, the user/org exists, we have rights (the .admin owns the tokens — it checks on its side).
|
||||
3. **Create SIMULTANEOUSLY** (not one before the other):
|
||||
- `mcp__mappa__projects_register` — mappa registration (id returned).
|
||||
- repo via `.admin` — `task_create` in `.admin` (**priority P0** — it blocks the project chain) + covering letter via `inbox_send` (a task on the board doesn't ping a live session). The repo task is an **ops task → NO paired review** (mappa-delegation: skip review for ops). Acceptance: repo created private, answer letter with clone URLs + which token is needed for push.
|
||||
4. **Ask the operator: where on disk the project folder goes and what it's named** (location AND folder name — both are the operator's call) — do not guess the path or the name.
|
||||
5. **Create the folder** on the agent's local filesystem (the operator's workstation, not a remote host).
|
||||
6. **Hand over to bootstrap** — `project-bootstrap` (general skill) does git init, .gitignore, README, AGENTS.md, remote connect, push. Separate step, after the repo exists.
|
||||
|
||||
## Why the repo goes through `.admin`
|
||||
|
||||
Gitea tokens are secrets — they live only with `.admin` (zone rule: secrets stay out of other agents). Precedent: yt-digest (inbox:2305) and tg-digest (inbox:2463) — `.admin` creates the repo via `POST /user/repos` → transfer to the target owner (the admin endpoint refuses without write:admin), then answers with the clone URLs and the push token (`pass show gitea/admin-token` for OpeItcLoc03, or a user token with repo:write for the target owner).
|
||||
|
||||
## Failure modes
|
||||
|
||||
- The operator hasn't chosen the address → abort, don't create. The address is the operator's call.
|
||||
- mappa name taken → suggest a rename, don't overwrite.
|
||||
- gitea name taken / user missing → report, suggest a rename.
|
||||
- `.admin` repo task failed → report, don't register the project alone (mappa and gitea are created as a pair).
|
||||
- `task_create` in `.admin` → 422 secret-detected on the token scope string (`write:repository` is flagged) → rephrase ("токен с правами на запись в репозиторий"), don't include raw token strings.
|
||||
- The operator answers the on-disk path only after the repo is created → that's the normal order, fine.
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- Don't assume the hosting owner from a neighbouring project — ask (the hard rule).
|
||||
- Don't create mappa registration alone without the repo (they're a pair).
|
||||
- Don't create the repo alone without the mappa registration.
|
||||
- Don't create the on-disk folder in a guessed location — ask.
|
||||
- Don't do the bootstrap inside this skill — that's `project-bootstrap`.
|
||||
- Don't create a paired review for the `.admin` repo task (ops task).
|
||||
- Don't skip the covering letter to `.admin` (task + letter, not just task).
|
||||
|
||||
## Reference
|
||||
|
||||
- Mappa registry: `mcp__mappa__projects_list` / `projects_register` / `projects_resolve`.
|
||||
- Repo creation: `.admin` (task + letter, mappa-delegation canon; ops → no review).
|
||||
- Bootstrap: `project-bootstrap` (general skill).
|
||||
- Live case: brainstorm:129 (tg-digest, .workshop) — the whole pattern was built there.
|
||||
@@ -1,29 +0,0 @@
|
||||
# project-discipline
|
||||
|
||||
Policy skill that codifies four cross-project discipline rules so the same
|
||||
guarantees that hold in a tightly-maintained repo apply everywhere.
|
||||
|
||||
## When it triggers
|
||||
|
||||
- **Session start** — when `AGENTS.md` contains the line `follow project discipline` (added by `project-bootstrap` v2.0.0+).
|
||||
- **In-chat** — when the user says "use project discipline", "соблюди дисциплину", "проектные правила", or close variants.
|
||||
|
||||
## The four rules
|
||||
|
||||
1. **Project conventions over skill defaults.** `AGENTS.md` / `.wiki/CLAUDE.md` / `.tasks/` override any other skill's defaults. Specs go to `.wiki/concepts/`, tasks to `.tasks/`.
|
||||
2. **Master-only.** All work on `master` (or `main`). No feature branches without explicit user approval.
|
||||
3. **Semver discipline.** Bump `version:` in `SKILL.md` / `package.json` / `pyproject.toml` on every edit per MAJOR / MINOR / PATCH; record in commit message; rebuild `dist/` artifacts after.
|
||||
4. **Push freely by default.** No confirmation needed for push; a local push-gate skill (e.g. books — auto-deploy) overrides per project. Force / delete / non-ff push always asks.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
None. The skill is a textual policy document; it takes no actions and has no
|
||||
external dependencies. Activate it by adding `follow project discipline` to
|
||||
`AGENTS.md` (or use `project-bootstrap` v2.0.0+ which adds it automatically).
|
||||
|
||||
## Related
|
||||
|
||||
- `project-bootstrap` (v2.0.0+) — adds the trigger line to new and existing projects' `AGENTS.md`.
|
||||
- `pulling-before-work` — companion skill activated by the canonical template; pulls origin once at session start (`git pull --ff-only`).
|
||||
- `using-tasks` / `using-wiki` — the format conventions Rule 1 routes work into.
|
||||
- `.wiki/concepts/project-discipline-design.md` (in `skills`) — full design rationale.
|
||||
@@ -1,127 +0,0 @@
|
||||
---
|
||||
name: project-discipline
|
||||
author: ours
|
||||
version: 0.2.2
|
||||
description: >
|
||||
Codifies five cross-project discipline rules: (1) project AGENTS.md /
|
||||
.wiki/CLAUDE.md / .tasks/ override defaults from other skills (specs →
|
||||
.wiki/concepts/, tasks → .tasks/); (2) master-only, no feature branches
|
||||
without approval; (3) semver bump on every edit of versioned artifacts
|
||||
(SKILL.md, package.json, pyproject.toml), recorded in commit;
|
||||
(4) push freely by default — no confirmation needed;
|
||||
a local push-gate skill (e.g. books — auto-deploy)
|
||||
overrides per project; force/delete/non-ff always ask;
|
||||
(5) transit/brainstorm workspaces — artifacts to .brainstorm/ or global wiki
|
||||
only via explicit user direction. Activated by "follow project discipline"
|
||||
trigger in AGENTS.md (added by project-bootstrap v2.0.0+).
|
||||
---
|
||||
|
||||
# project-discipline
|
||||
|
||||
> Four cross-project rules. Read at session start. Apply before any other skill's defaults touch paths, branches, versions, or remote pushes.
|
||||
|
||||
## When this runs
|
||||
|
||||
**At session start** — when `AGENTS.md` contains the line `follow project discipline`. The skill is a policy document; the agent reads it and applies the four rules to all subsequent work in the session.
|
||||
|
||||
**On explicit reference** — when the user says "use project discipline", "соблюди дисциплину", "проектные правила", "что у меня по правилам?", or close variants asking about/applying the rules.
|
||||
|
||||
The skill itself takes no actions and has no external side-effects. It instructs the agent how to behave.
|
||||
|
||||
## Rule 1 — Project conventions override skill defaults
|
||||
|
||||
Before applying defaults from any other skill (frontend-design, mcp-builder, etc.), read in this order:
|
||||
|
||||
1. `AGENTS.md` in the project root.
|
||||
2. `.wiki/CLAUDE.md` (if it exists).
|
||||
3. `.tasks/STATUS.md` (if it exists).
|
||||
|
||||
Any path, format, or workflow explicitly stated in those files **overrides the skill default**. (CLAUDE.md, where present, is a legacy pointer — read it, then follow the canon in AGENTS.md.)
|
||||
|
||||
Concrete consequences:
|
||||
|
||||
- **Specs / design documents** go to `.wiki/concepts/<topic>-design.md`.
|
||||
- **Task tracking / implementation plans** go to `.tasks/<slug>.md` plus a board entry in `.tasks/STATUS.md` (the `using-tasks` format).
|
||||
- **Frontmatter, naming conventions, log format** — as described in the project's `.wiki/CLAUDE.md`.
|
||||
|
||||
If no convention is stated explicitly — fall back to the skill default.
|
||||
|
||||
## Rule 2 — Master-only
|
||||
|
||||
All work happens on the repo's main integration branch — usually `master`, but if a project uses `main`, treat `main` as equivalent.
|
||||
|
||||
- No `git checkout -b feature/foo` for solo work.
|
||||
- Sync with remote: `git pull --ff-only` or `git pull --rebase`. **No merge commits** for solo work.
|
||||
- If a task genuinely requires isolation (large experiment, risky refactor with rollback potential, multi-day work with intermediate WIP commits) — **ask** the user: "this needs its own branch, ok?" — and wait for explicit approval. Without approval, work continues on master.
|
||||
|
||||
If the agent finds itself on a non-main branch (after a manual `git checkout`) or in detached HEAD — report it and ask whether to return to master before working.
|
||||
|
||||
## Rule 3 — Versioning discipline
|
||||
|
||||
When editing any artifact with a semver field, **bump the version before committing** per:
|
||||
|
||||
- **MAJOR** (`X+1.0.0`) — breaks the contract. Renames, removed triggers, layout changes, removed public functions, breaking API change.
|
||||
- **MINOR** (`X.Y+1.0`) — adds capability without breaking. New trigger, new optional step, new public function.
|
||||
- **PATCH** (`X.Y.Z+1`) — wording / clarity / typo fixes with no behavior change.
|
||||
|
||||
The bump is recorded in the commit message: `feat(<artifact>): … [vX.Y.Z]` or whatever convention the project uses (see Rule 1).
|
||||
|
||||
**Applies to:** `skills/<name>/SKILL.md` (`version:` in frontmatter), `package.json` (`"version":`), `pyproject.toml` (`version =`), `Cargo.toml` (`version =`), and any other semver field in any other manifest.
|
||||
|
||||
**If the artifact is packaged** as `dist/<name>.skill`, `dist/*.tgz`, etc. — **rebuild** the package in the same or the next commit. Forgotten dist artifacts are a common cause of deploying stale binaries.
|
||||
|
||||
**First edit of an unversioned artifact** that COULD have a semver field (a new skill without `version:`, a new `package.json` without `"version":`) — **add** `version: 0.1.0` (or its equivalent) before committing; do not bump anything.
|
||||
|
||||
**Does not apply to:** artifacts with no semver field and no potential for one (wiki concept pages, README.md, shell scripts without a public interface).
|
||||
|
||||
## Rule 4 — Push freely, gate only where a local gate exists
|
||||
|
||||
**Default: push freely.** An ordinary fast-forward `git push` to the configured
|
||||
upstream needs no per-push confirmation. No ask-before-push mode by default.
|
||||
|
||||
**Per-project push gate.** A project whose push triggers side effects carries a
|
||||
LOCAL project-scope skill (convention: `push-gate`) that replaces this default
|
||||
with ask-before-push for that project. Example: `books` — push to master runs
|
||||
Gitea Actions auto-deploy. Respect the local gate over this rule: if the project
|
||||
has a `push-gate` skill, its semantics win for that project; this rule's
|
||||
free-push default does not apply there.
|
||||
|
||||
**Always ask:**
|
||||
|
||||
- `git push --force` / `--force-with-lease` (history rewrite);
|
||||
- `git push origin --delete <branch>` (branch deletion);
|
||||
- push to a remote/branch other than the current tracked upstream (`git push other-remote ...`, `git push origin other-branch`);
|
||||
- push to the main branch that would require non-fast-forward (i.e. would need force).
|
||||
|
||||
Anything else is an ordinary fast-forward push and proceeds without a gate.
|
||||
|
||||
**What counts as "push":** only `git push` family commands. Local commits, `git stash push`, etc. are not push.
|
||||
|
||||
## Rule 5 — Transit-zone / brainstorm workspaces
|
||||
|
||||
Some workspaces are **transit zones** — discussion areas with no `.tasks/`, where brainstorm artifacts are explicitly NOT auto-promoted to project wikis.
|
||||
|
||||
**Default destination for brainstorm artifacts:**
|
||||
- **In-progress brainstorm outputs** → `.brainstorm/<topic>.md` (or whatever the workspace's README/AGENTS.md declares)
|
||||
- **Mature, cross-cutting outputs** → `~/projects/.wiki/concepts/<topic>-design.md` via `mcp__projects-meta__knowledge_ingest` — **only** when user explicitly directs this
|
||||
|
||||
**Agent must NOT auto-promote** brainstorm artifacts to global wikis by analogy with Rule 1. Convergence-moment (move from workspace to permanent wiki) is a user decision, not an automatic action.
|
||||
|
||||
**Example:** `~/projects/.meeting-room/` is a transit zone. Its AGENTS.md explicitly states "no `.tasks/`, transit zone, artifacts go to `.brainstorm/` or global wiki via user command." Rule 1's "project conventions override" applies, but the override is explicit in the workspace contract — auto-promotion by analogy would violate that contract.
|
||||
|
||||
**When in doubt:** ask the user "this goes to `.brainstorm/`, or should I promote to shared wiki?" rather than assuming.
|
||||
|
||||
## Out of scope
|
||||
|
||||
The skill **does not**:
|
||||
|
||||
- modify `AGENTS.md` (that's `project-bootstrap`'s job);
|
||||
- enforce rules via git hooks / pre-commit / CI (this is agent discipline, not tooling);
|
||||
- manage `settings.json` permissions (that's `update-config`);
|
||||
- check the existence of `.wiki/` / `.tasks/` on disk (мета в mappa, решение 14/15: вики/таски — сущности сервиса через `using-wiki` / `using-tasks`; файловый layout — легаси); if a project has neither, Rule 1 simply finds no overrides and falls back to skill defaults.
|
||||
|
||||
## Why this exists
|
||||
|
||||
In a tightly-disciplined repo (`skills`) the four rules already hold by accident — the agent reads `.wiki/CLAUDE.md`, knows specs go to `.wiki/concepts/`, knows to bump `version:`, knows not to push without confirmation. In **other** projects of the same user, that discipline does not transfer: the agent falls back to vendor-default paths (`docs/specs/`, `docs/plans/`), branches on a whim, forgets `version:` bumps, and pushes without asking. This skill makes the discipline explicit and portable.
|
||||
|
||||
Full design rationale (why one skill instead of four, why a skill instead of inline `AGENTS.md` lines, scope of each rule, push-permission mechanism choice) lives in `.wiki/concepts/project-discipline-design.md` (in this repo; in other projects bootstrapped from this repo, the design lives in `skills`).
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: review-kit-pi-method
|
||||
author: ours
|
||||
version: 0.1.1
|
||||
version: 0.1.2
|
||||
description: >
|
||||
Spawn clean-context non-implementer subagents for review, trigger-testing,
|
||||
and spec validation under pi — the pi-native port of the review-kit method.
|
||||
@@ -124,6 +124,26 @@ non-implementer subagents, anti-priming checklist, negative controls — is
|
||||
agent-agnostic and transfers to any runtime that can spawn a fresh-context
|
||||
subprocess (claude `-p`, codex exec, hermes headless).
|
||||
|
||||
### CC-спавн (claude-code через deepseek) — live-проверен 2026-09-01 (session:974)
|
||||
|
||||
CC (`claude -p`) умеет субагент-ревью БЕЗ скилов: тот же чистый контекст
|
||||
(флаги изоляции) через обёртку `claude-deepseek`
|
||||
(`.common/scripts/claude-switch.ps1`; env `ANTHROPIC_BASE_URL=
|
||||
https://api.deepseek.com/anthropic` + ключ из `~/.deepseek_api_key`):
|
||||
|
||||
```bash
|
||||
claude -p "<question>" \
|
||||
--output-format stream-json --verbose \
|
||||
--model deepseek-v4-flash-vision-exp \
|
||||
--tools "" --disable-slash-commands --no-session-persistence
|
||||
```
|
||||
|
||||
Флаги изоляции (аналог `-nc -ns -nt` pi): `--tools ""` (нет тулов —
|
||||
не читает файлы, не самопраймится), `--disable-slash-commands` (нет
|
||||
команд-интерпретатора), `--no-session-persistence` (эфемерно, без
|
||||
сессионного блода). Анти-прайминг-чеклист и правила промпта — те же, что
|
||||
выше (ask the behavior, one question per run, negative controls).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Does NOT define the review criteria themselves (skill-specific acceptance —
|
||||
|
||||
@@ -16,9 +16,10 @@ description: >
|
||||
|
||||
# session-health
|
||||
|
||||
Что делать, когда поллер pi (`extensions/session-health.ts`) прислал
|
||||
предупреждение о размере контекста — или когда сам агент подозревает, что
|
||||
сессия раздулась. Поллер — единственный источник точных цифр: footer-статус
|
||||
Что делать, когда поллер pi (`extensions/mappa.ts`, секция session-health —
|
||||
консолидация 6 расширений, task:1486, wiki:3325) прислал предупреждение о
|
||||
размере контекста — или когда сам агент подозревает, что сессия раздулась.
|
||||
Поллер — единственный источник точных цифр: footer-статус
|
||||
(`14.6%/1.0M`) и `/session` агент (LLM) **не видит** — это TUI для человека.
|
||||
|
||||
## When to use
|
||||
|
||||
Reference in New Issue
Block a user