diff --git a/.tasks/STATUS.md b/.tasks/STATUS.md index 2a7b770..14bc893 100644 --- a/.tasks/STATUS.md +++ b/.tasks/STATUS.md @@ -9,19 +9,18 @@ _Updated: 2026-05-22_ --- -## ⚪ [board-viewer-gitea-reader] — Gitea API reader → normalized task structure -**Status:** ready -**Where I stopped:** разблокирована (pointers done); реализация не начата. Это первая импл-таска, остальные impl-таски зависят от её типов. -**Next action:** TDD по контракту в `board-viewer-gitea-reader.md`. +## 🟢 [board-viewer-gitea-reader] — Gitea API reader → normalized task structure +**Status:** done +**Where I stopped:** TaskRecord контракт реализован (parser+gitea+reader+config); 25 тестов; typecheck clean. Тесты используют 3 реальных STATUS.md fixture'а. +**Next action:** разблокировать html-render — потребляет `readBoard(client, repos) → TaskRecord[]`. **Branch:** master --- ## ⚪ [board-viewer-html-render] — render 5-col kanban from normalized structure **Status:** ready -**Where I stopped:** ждёт `board-viewer-gitea-reader` (использует его типы). -**Next action:** после ready-стейта reader'а — TDD по контракту в `board-viewer-html-render.md`. -**Blocker:** board-viewer-gitea-reader +**Where I stopped:** reader готов, импортируется `TaskRecord` из `src/reader.ts`. Импл не начата. +**Next action:** TDD по контракту в `board-viewer-html-render.md`. **Branch:** master --- diff --git a/.tasks/board-viewer-gitea-reader.md b/.tasks/board-viewer-gitea-reader.md index c482de7..83724e4 100644 --- a/.tasks/board-viewer-gitea-reader.md +++ b/.tasks/board-viewer-gitea-reader.md @@ -10,10 +10,12 @@ ## Key files -- (TBD) `src/gitea-reader.{ts,py}` — main reader module -- (TBD) `src/types.{ts,py}` — exported task structure -- (TBD) `tests/fixtures/` — sample STATUS.md + per-task.md pairs from real repos -- (TBD) `tests/gitea-reader.test.{ts,py}` — TDD assertions +- `src/parser.ts` — STATUS.md → `StatusBlock[]` (pure) +- `src/gitea.ts` — Gitea HTTP client (DI'd fetch, `getFile` / `getLatestCommitIso` / `rawUrl`) +- `src/reader.ts` — `readBoard(client, repos) → TaskRecord[]`; orchestrates parser+client +- `src/config.ts` — loads `~/.config/projects-mcp/auth.toml` → `{ baseUrl, token, repos }` +- `tests/fixtures/` — `status-single-block.md`, `status-multi.md`, real STATUS.md from books / claude-skills / projects-meta-mcp +- `tests/parser.test.ts` (9), `tests/gitea.test.ts` (7), `tests/reader.test.ts` (5), `tests/config.test.ts` (4) — 25 tests total ## Acceptance criteria @@ -48,13 +50,18 @@ ## Open questions -- [ ] TS or Python? (повлияет на структуру cron-deploy). -- [ ] Owner-фильтр: брать репо из `gitea_owners` целиком или whitelist? Возможно нужен `board_viewer_repos` отдельным полем в auth.toml. -- [ ] Кэшировать ответы Gitea API локально между cron-тиками? (опт.: rate-limit, hot-reload). +- [x] TS (Node 22, vitest, ESM) — выбор подтверждён юзером 2026-05-22. +- [x] Whitelist `board_viewer_repos` в `auth.toml` — выбран как явный и контролируемый. +- [x] Без кэширования на MVP — Gitea на том же VDS, latency negligible. ## Completed steps -- [ ] (фиксируется при выполнении) +- [x] парсер STATUS.md (`src/parser.ts`) с маппингом ⚪🔴🟡🔵🟢 → open/in_progress/paused/blocked/done +- [x] Gitea HTTP client (`src/gitea.ts`): getFile / getLatestCommitIso / rawUrl, DI fetch +- [x] reader orchestrator (`src/reader.ts`): объединяет parser+client → TaskRecord[] +- [x] TOML config loader (`src/config.ts`) с whitelist `board_viewer_repos` +- [x] 25 тестов: 9 парсера (включая 3 real-repo фикстуры), 7 gitea, 5 reader, 4 config +- [x] typecheck clean ## Notes diff --git a/package-lock.json b/package-lock.json index c2494cf..b518ce2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "board-viewer", "version": "0.1.0", + "dependencies": { + "smol-toml": "^1.6.1" + }, "devDependencies": { "@types/node": "^22.10.0", "typescript": "^5.7.0", @@ -1205,6 +1208,18 @@ "dev": true, "license": "ISC" }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index c09a5d7..f8deffa 100644 --- a/package.json +++ b/package.json @@ -16,5 +16,8 @@ "@types/node": "^22.10.0", "typescript": "^5.7.0", "vitest": "^2.1.0" + }, + "dependencies": { + "smol-toml": "^1.6.1" } } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..e629e76 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,50 @@ +import { readFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { parse as parseToml } from 'smol-toml'; +import type { RepoRef } from './reader.ts'; + +export interface BoardConfig { + baseUrl: string; + token: string; + repos: RepoRef[]; +} + +export function defaultConfigPath(): string { + return join(homedir(), '.config', 'projects-mcp', 'auth.toml'); +} + +export function loadConfig(path: string = defaultConfigPath()): BoardConfig { + const raw = readFileSync(path, 'utf8'); + const data = parseToml(raw) as Record; + + const baseUrl = stringField(data, 'gitea_url'); + const token = stringField(data, 'gitea_token'); + const repos = parseRepos(data['board_viewer_repos']); + + return { baseUrl, token, repos }; +} + +function stringField(data: Record, key: string): string { + const value = data[key]; + if (typeof value !== 'string' || value.length === 0) { + throw new Error(`config: missing or empty string field "${key}"`); + } + return value; +} + +function parseRepos(value: unknown): RepoRef[] { + if (!Array.isArray(value)) { + throw new Error('config: missing or invalid "board_viewer_repos" (expected array of "owner/repo")'); + } + return value.map((entry) => { + if (typeof entry !== 'string') { + throw new Error(`config: board_viewer_repos entry must be string, got ${typeof entry}`); + } + const parts = entry.split('/'); + if (parts.length !== 2 || parts[0] === '' || parts[1] === '') { + throw new Error(`config: board_viewer_repos entry "${entry}" is not "owner/repo"`); + } + return { owner: parts[0]!, repo: parts[1]! }; + }); +} diff --git a/src/gitea.ts b/src/gitea.ts new file mode 100644 index 0000000..cf5ae0d --- /dev/null +++ b/src/gitea.ts @@ -0,0 +1,58 @@ +export interface GiteaClientOptions { + baseUrl: string; + token: string; + fetch?: typeof fetch; + defaultBranch?: string; +} + +export interface GiteaClient { + getFile(owner: string, repo: string, path: string): Promise; + getLatestCommitIso(owner: string, repo: string, path: string): Promise; + rawUrl(owner: string, repo: string, path: string): string; +} + +interface ContentsResponse { + content?: string; + encoding?: string; +} + +interface CommitEntry { + commit?: { author?: { date?: string } }; +} + +export function createGiteaClient(opts: GiteaClientOptions): GiteaClient { + const fetchImpl = opts.fetch ?? fetch; + const base = opts.baseUrl.replace(/\/+$/, ''); + const branch = opts.defaultBranch ?? 'master'; + const headers = { Authorization: `token ${opts.token}` }; + + async function getFile(owner: string, repo: string, path: string): Promise { + const url = `${base}/api/v1/repos/${owner}/${repo}/contents/${path}`; + const res = await fetchImpl(url, { headers }); + if (res.status === 404) return null; + if (!res.ok) throw new Error(`Gitea getFile ${owner}/${repo}/${path}: HTTP ${res.status}`); + const body = (await res.json()) as ContentsResponse; + if (body.encoding !== 'base64' || typeof body.content !== 'string') { + throw new Error(`Gitea getFile ${owner}/${repo}/${path}: unexpected encoding ${body.encoding}`); + } + return Buffer.from(body.content, 'base64').toString('utf8'); + } + + async function getLatestCommitIso( + owner: string, + repo: string, + path: string, + ): Promise { + const url = `${base}/api/v1/repos/${owner}/${repo}/commits?path=${encodeURIComponent(path)}&limit=1`; + const res = await fetchImpl(url, { headers }); + if (!res.ok) throw new Error(`Gitea getLatestCommitIso ${owner}/${repo}/${path}: HTTP ${res.status}`); + const body = (await res.json()) as CommitEntry[]; + return body[0]?.commit?.author?.date ?? null; + } + + function rawUrl(owner: string, repo: string, path: string): string { + return `${base}/${owner}/${repo}/raw/branch/${branch}/${path}`; + } + + return { getFile, getLatestCommitIso, rawUrl }; +} diff --git a/src/reader.ts b/src/reader.ts new file mode 100644 index 0000000..7ca89f1 --- /dev/null +++ b/src/reader.ts @@ -0,0 +1,56 @@ +import type { GiteaClient } from './gitea.ts'; +import { parseStatus, type Status, type StatusEmoji } from './parser.ts'; + +export interface RepoRef { + owner: string; + repo: string; +} + +export interface TaskRecord { + slug: string; + project: string; + project_owner: string; + status: Status; + status_emoji: StatusEmoji; + title: string; + where_stopped: string | null; + next_action: string | null; + blocker: string | null; + branch: string | null; + last_commit_iso: string | null; + raw_url: string; +} + +export async function readBoard( + client: GiteaClient, + repos: ReadonlyArray, +): Promise { + const records: TaskRecord[] = []; + + for (const { owner, repo } of repos) { + const status = await client.getFile(owner, repo, '.tasks/STATUS.md'); + if (status === null) continue; + + const blocks = parseStatus(status); + for (const block of blocks) { + const taskPath = `.tasks/${block.slug}.md`; + const last_commit_iso = await client.getLatestCommitIso(owner, repo, taskPath); + records.push({ + slug: block.slug, + project: repo, + project_owner: owner, + status: block.status, + status_emoji: block.status_emoji, + title: block.title, + where_stopped: block.where_stopped, + next_action: block.next_action, + blocker: block.blocker, + branch: block.branch, + last_commit_iso, + raw_url: client.rawUrl(owner, repo, taskPath), + }); + } + } + + return records; +} diff --git a/tests/config.test.ts b/tests/config.test.ts new file mode 100644 index 0000000..5fd5002 --- /dev/null +++ b/tests/config.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test } from 'vitest'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { loadConfig } from '../src/config.ts'; + +function writeToml(body: string): string { + const dir = mkdtempSync(join(tmpdir(), 'board-viewer-cfg-')); + const path = join(dir, 'auth.toml'); + writeFileSync(path, body, 'utf8'); + return path; +} + +describe('loadConfig', () => { + test('parses gitea_url, gitea_token, and board_viewer_repos from TOML', () => { + const path = writeToml(` +gitea_url = "https://git.example.com" +gitea_token = "abc123" +board_viewer_repos = ["OpeItcLoc03/board-viewer", "OpeItcLoc03/books"] +`); + + const cfg = loadConfig(path); + + expect(cfg).toEqual({ + baseUrl: 'https://git.example.com', + token: 'abc123', + repos: [ + { owner: 'OpeItcLoc03', repo: 'board-viewer' }, + { owner: 'OpeItcLoc03', repo: 'books' }, + ], + }); + }); + + test('throws when board_viewer_repos field is missing', () => { + const path = writeToml(`gitea_url = "x"\ngitea_token = "t"\n`); + + expect(() => loadConfig(path)).toThrow(/board_viewer_repos/); + }); + + test('throws when gitea_token is missing', () => { + const path = writeToml(`gitea_url = "x"\nboard_viewer_repos = []\n`); + + expect(() => loadConfig(path)).toThrow(/gitea_token/); + }); + + test('throws on malformed "owner/repo" entry', () => { + const path = writeToml(` +gitea_url = "x" +gitea_token = "t" +board_viewer_repos = ["just-owner-no-slash"] +`); + + expect(() => loadConfig(path)).toThrow(/just-owner-no-slash|owner\/repo/); + }); +}); diff --git a/tests/fixtures/real-books-STATUS.md b/tests/fixtures/real-books-STATUS.md new file mode 100644 index 0000000..3af9d88 --- /dev/null +++ b/tests/fixtures/real-books-STATUS.md @@ -0,0 +1,556 @@ +# Task Board +_Updated: 2026-05-21 — `drain-bookva-ym-stocks` 🟢 **DONE end-to-end**. Bookva-Ozon FBS: 5173/5184 → stock=0 (5171 первый прогон + 2 ручной multi-warehouse патч К000405/К000632 на Афонская КГТ + realFBS). YM Bookva: 1049/1049 → count=0. Slovo (id=2) не затронут, торгует штатно: `ozon stocks syncronization` каждую минуту SUCCESS (ctx.data{idSeller:2, otherSellers:[]} читается корректно), `sync stocks with warehouse` идёт. 14 commits на master (включая 4 hot-fix циклов SDK): план/спека → guard fbsPostings → handler TDD (6 unit-тестов) → tasks.json drop YM-cron → drain CLI (7 unit-тестов, basePath + IN_SALE/PUBLISHED фильтры + @books/yandex-market-partner-api wrapper + bypass YM `updatedAt` SDK-bug + multi-warehouse enumeration). YM Agenda docs cleanup: 8 (2 активных + 6 stale dup от старого idempotency) → 0 через mongosh deleteMany. **Leftover: 11 SKU на FBO в Bookva-Ozon** (147720748, 6281157, Б400095, 19687905, Б2003910, Б401223, 2888034, 7754504, К000370, Б601643, Б604747) — обнулить через API продавцу невозможно (склады Ozon), нужен FBO withdrawal в кабинете руками. Bug в `@books/yandex-market-partner-api/stocks.js:59` (updatedAt double-toISOString, идентичен 2026-04-27 fix'у в api service но в shared wrapper не починен) занесён в [business-logic-bugs.md](business-logic-bugs.md). Spec: [.wiki/concepts/drain-bookva-ym-stocks.md](../.wiki/concepts/drain-bookva-ym-stocks.md), plan: [.tasks/drain-bookva-ym-stocks-plan.md](drain-bookva-ym-stocks-plan.md)._ + +_Updated: 2026-05-21 — заведена `drain-bookva-ym-stocks` 🔴 active. Soft drain каналов Bookva-Ozon (`id_seller=1`) и Yandex.Market (тот же seller=1, `ym_seller=1`): push stocks=0/count=0 через API + reconfig scheduler так, чтобы автосинхронизация не возвращала ненулевые значения. Slovo (`id_seller=2`) торгует штатно. 6 изменений: (1) guard в `packages/utils/fbsPostings.js` `length > 0` — закрывает YM-leak через `updateSellersProductsStocks→calculateAndUpdateYmStocks(1)` который дёргается даже при `otherSellers=[]`; (2) `task-runner/tasks/ozonStocksSyncronization.js` читает `ctx.data().idSeller` + `otherSellers`; (3) `tasks.json` data `{idSeller:2, otherSellers:[]}` для obo task'ов; (4) drop YM-cron'ов (`ym stocks syncronization` + `sync ym stocks with warehouse`) из Agenda; (5) one-off CLI `packages/tools/drain-bookva-ym-stocks.js` (hybrid CLI+module, dry-run, --only ozon|ym, `.bookva.cmd`). Spec: [.wiki/concepts/drain-bookva-ym-stocks.md](../.wiki/concepts/drain-bookva-ym-stocks.md). Без архива/`has_stock=false`/выключения канала — только stocks, обратимо. Кода ещё нет — план следующим шагом через `superpowers:writing-plans`._ + +_Updated: 2026-05-21 — `move-promo-bookva-to-slovo` 🟢 **DONE end-to-end**. Bookva долгопрод (promo id=1, 663 active applied, -30%) свёрнут, перенесён на Slovo (новая id=3, 660 SKU, -30%, до 2026-06-04 14:00 MSK). По ходу всплыл **3-й bug в domain-promotion CLI** — `ozon-promotion-deactivate.js:137` сравнивает `expectedPrice = snapshotPrice` (вместо `actionPrice` по спеке [[domain-promotions]]) → 663 SKU помечены diverged, `importPrices` не вызван, Bookva-Ozon цены остались -30%, БД ушла в фантомное ended. Switch на ad-hoc oneshot `packages/tools/_push-prices-from-snapshot.mjs` (CLI: --source-promotion-id --target-channel-id [--mark-restored] [--chunk-size]) — обход defensive, прямой `applyPriceToOzon` + sync БД. Прогон: Step1 Bookva real-restore 663/663, Step2 Slovo cohort-restore 235/235, Step3 Bookva-snap→Slovo 660/663 (3 NOT_FOUND: `19119894`, `Б2008714`, `Б607029` — нет в Slovo каталоге), Step4 create id=3, Step5 add 660/660, Step6 activate 660/660 applied. Bug в deactivate занесён в [business-logic-bugs.md](business-logic-bugs.md) (запись 2026-05-21). Файл `_push-prices-from-snapshot.mjs` оставлен в `packages/tools/` (без хардкод-кред, через `salesChannel.getSeller()`) — пригодится для будущих cross-shop миграций или fix-сессий deactivate.js._ + +_Updated: 2026-05-20 — заведена `fbs-picking-list-pdf-thumbs` 🔴 active. Bugfix на `fbs-picking-list-pdf`: prod run #37944 (idPickingList=162) — email с 38MB PDF режется SMTP-сервером (552 5.3.4). Корень — `FbsPickingPlacePrint.vue:14` ставит `` на CDN full-res JPG, puppeteer встраивает в PDF без даунсемпла (100 items × ~300KB = 30-40MB). Fix — новый thumb-endpoint `/images/thumb` в `@books/api` (sharp resize до 240px JPEG q80), allowlist `*.ozone.ru` (SQL подтверждает 105 347 рядов в `cdn1.ozone.ru` + `nowatermark.ozone.ru`), auth тот же `?token=`. PDF упадёт до ~1-3MB, email пройдёт. TDD: pure-logic `parseThumbParams`+`isAllowedHost` (security/SSRF, rules 3+4). Bump `@books/api` 2.1.0→2.2.0, `@books/web` 2.1.0→2.2.0._ + +_Updated: 2026-05-17 — `slovo-product-creation-recovery` 🟢 **DONE end-to-end**. После закрытия `slovo-in-sale-gap-diagnose` Slovo 4742 → **5153** (+411, модерация Pass 1 #2 досoзревала), gap 676 → **306** (95.3% от Bookva 5406). Остаточные 245 «out_of_stock» — артефакт priority chain bucketize'а в diag tool (`has_stock=false` сел приоритетом выше `is_created=false`); реально Ozon отвергает `updateProductsStocks2` с `PRODUCT_IS_NOT_CREATED` пока карточка не сolzит `is_created=true`. Concept [ozon-product-creation-blockers](../.wiki/concepts/ozon-product-creation-blockers.md) (ингестнут вчера, commit ec3d596) расписан → реализованы 2 tool'а: **Pass 3** `products-fix-creation-errors-from-mirror` (парсит `errors[].attribute_id` по whitelist кодов default `CONDITIONAL_ATTRIBUTE_ERROR`, копирует значения этих атрибутов из Bookva через `UpdateAttributes`) + **Pass 4** `products-reimport-stuck` (auto-detect stuck `is_created=false` + пустой `errors[]`, re-import через `ImportProductsBySKU` без diff'а). TDD: 10 + 8 unit-тестов. Prod smoke: Pass 3 — 74/74 imported (task_id=4479563894, из 169 targeted Slovo / 102 Bookva matched), Pass 4 — 6/6 imported (task_id=4479600569, из 632 stuck / 626 без Bookva master). Bump `@books/tools` 0.9.0→0.10.0 MINOR. 185/185 tests green. **Lesson:** я слил час на параллельное расследование `slovo-stock-sync-bug` (создал и удалил task) **не прочитав wiki** — concept уже описывал точный fix. Юзер дважды redirect'нул (`не там копаешь` → `вики читать с самого начала сессии!`). Стоит запомнить как hard rule._ + +_Updated: 2026-05-16 — `slovo-in-sale-gap-diagnose` 🟢 **DONE**. Diagnostic CLI `analyst/ozon-in-sale-gap.js` (13/13 тестов) выявил два root cause'а: (1) **`sellers.put_on_sale_delay_days=7` для Slovo** — `calculateNewStocks` в `packages/utils/stocks.js` фильтрует `units.put_on_sale_date <= now-7d`, поэтому свежие книги (последний экземпляр, положен <7d назад) не считались available для Slovo → stock=0 → has_stock=false. Юзер поменял delay на 0. Manual trigger `sync stocks with warehouse` (idSeller=2) подтянул **+194 IN_SALE** (4548→4742, 84.0%→87.5%). (2) **Pass 1 фильтр отсекал Б2/К**, юзер: «только У не переносим». Filter обновлён в обоих CLI (`products-import-by-sku.js` + `products-fill-improve-attributes.js`), bump `@books/tools` 0.7.0→0.8.0. Pass 1 #2 с новым фильтром: 444 кандидата отправлено через 5 task_id (443 pending, 1 failed offer_id=3907989, **0 unmatched**) — через 1-3 дня после модерации станут IN_SALE → прогноз ~5180 = ~95.6% от Bookva. Остаточный gap после dust settles: ~232 out_of_stock (5/15 sample — реальные нули, 10/15 — `available>0` но не пушится — потенциальная business-logic-bug в `stocks.js:195` где `productInfo.stocks.present` сравнивается на верхнем уровне, а реально поле внутри `stocks[]`) + ~50 state_failed:imported/validated/declined/etc. Memory: `reference_legacy_offer_id_prefix_filter`, `reference_seller_put_on_sale_delay_days`._ + +_Updated: 2026-05-15 (Pass 2 closed + verified) — **`ozon-products-copy-bukva-to-slovo` 🟢 DONE.** Full prod run `--limit 4000` → 1179 карточек в UpdateAttributes через 12 task_id, 0 payload-ошибок. Post-модерация замер: avg rating **39.37 → 41.60 (+2.23)**, bucket 8-12 сократился на 372 (карточки перешли в 1-7 improve), bucket 13+ на 23. Эффект verified программно через повторный `GetProductRatingBySku`. Sync `product_sales_channels` Slovo (id=2) — 571 новых связей создано, 183 ghost-карточки пропущены. 5 commits локально pending user-go на push._ + +_Updated: 2026-05-15 (вечер #3) — **Phase 1 (Pass 1) полностью выгружен на проде** под IN_SALE-only решением. Prod batch: 608 IN_SALE-карточек Bookva → Slovo, 7 task_ids, **дубликатов нет** (verified `GetProductList` Slovo + `offer_id` count = 1). Случайно прогнал команду дважды (был баг в моём mental model — `grep` после первого `node` я перечитал как «replay summary»); Ozon во втором прогоне ответил `status:skipped` на все 608 (умно). В Slovo также живут 50 карточек из первого ALL+blacklist smoke (могут быть не-IN_SALE) — чистка отдельной задачей если юзер скажет. Push pending user-go. Phase 2 — отдельной сессией._ + +_Updated: 2026-05-15 (вечер #2) — **PIVOT** ozon-products-copy-bukva-to-slovo на IN_SALE-only. Юзер: «нужны только те, котрые в Букве в продаже» (и Pass 2 тоже). visibility ALL+blacklist → IN_SALE-only, 2-слойный клиентский фильтр снят (IN_SALE Ozon-side уже отсекает archived/declined/state_failed). Local dry-run после pivot: Bookva IN_SALE **5506** → legacy 4976 → diff vs Slovo 4368 → **608 новых кандидатов** (а не 13832). 4 commits: `0047eea` spec, `b2965f6` code, `c460a6b` README, `c261dd8` bump 0.5.0→0.5.1. **608 — реальный объём Pass 1 на проде; 50 импортнутых из предыдущего batch'а — могли быть не-IN_SALE (вне нового scope), они в Slovo, чистка — отдельно если юзер скажет.** Next: prod batch (юзер сказал «давай, нужно»)._ + +_Updated: 2026-05-15 (вечер) — `ozon-products-copy-bukva-to-slovo` **Phase 1 done end-to-end**, 6 commits на master, **не push'ено**. Smoke discovery → secondary-фильтр верифицирован (`moderate_status='declined' || status_failed!==''`). Refactor + .cmd + README + bump 0.4.0→0.5.0. Local dry-run: 13832 кандидата → secondary отсёк 131 → cap=50. **Prod smoke `--limit 50` verified программно:** `task_id=4462621390`, **50/50 в `imported`**, `GetProductList` Slovo по тем же offer_id вернул **50/50** ✅. Criterion 10 спеки — done без UI-проверки юзера. Phase 2 (Pass 2 — `products-fill-improve-attributes.js`) — отдельной сессией. Pending: user-go на push._ + +_Updated: 2026-05-15 (поздний день) — заведена `ozon-products-copy-bukva-to-slovo` 🔴 active. Два прохода CLI в `packages/tools/`: Pass 1 — расширение существующего [products-import-by-sku.js](../packages/tools/products-import-by-sku.js) (visibility=ALL + blacklist archived/not_moderated/state_failed, CLI-флаги для кред вместо хардкода, `.cmd`-обёртка `bookva-to-slovo`); Pass 2 — новый CLI `products-fill-improve-attributes.js` (для существующих в Slovo карточек с непустым `improve_attributes[]` — забирает значения этих атрибутов из Bookva через `GetProductAttributesV4` и пушит через `UpdateAttributes`). Доменная область books не используется — только Ozon Seller API; ключ соответствия — `offer_id`. Префикс-фильтры У/Б2/К — legacy, оставлены, разберёмся потом. Spec: [.wiki/concepts/ozon-products-copy-bukva-to-slovo.md](../.wiki/concepts/ozon-products-copy-bukva-to-slovo.md). Кода ещё нет — юзер ушёл на другую работу, разработка автономная._ + +_Updated: 2026-05-15 — `analyst-workflow-claude-md` 🟢 **DONE** ✅ (commit `44d97b0`): добавлена секция «Analyst workflow» в корневой [CLAUDE.md](../CLAUDE.md) (39 строк) — триггер-фразы, 3 шага (load design context → check existing → write new), inline-блок стиля (ESM, commander+isDirectRun, JSDoc, bin+exports, ozon-seller-api-client напрямую, sequelize+raw SQL), read-каналы, write-через-CLI, anti-patterns. `tools-ci-path-filter` 🟢 **DONE (частично)** ✅ (commit `102a92e`): сужено до CLI-only-tools подпутей (`analyst/`, `plans/`, `not-for-qwen/`, `output/`, README/QWEN.md, *.cmd) — исключение `packages/tools/` целиком сломало бы prod runtime (fbs-ship/sync-prices импортятся в task-runner+api). Оба коммита локально на master, не push'ены. `analyst-workflow-review` 🔵 remains blocked — будет разблокирован после push'а._ + +_Updated: 2026-05-14 (ночь) — `ozon-revenue-per-product` 🟢 **DONE end-to-end** ✅. CLI-tool [`packages/tools/ozon-revenue-per-product.js`](../packages/tools/ozon-revenue-per-product.js) реализован по плану (11 tasks через `superpowers:subagent-driven-development`): 4 pure-функции под TDD (18 unit-тестов), live Finance + FBS/FBO Posting API, pluggable formatters (console/json/xlsx все ✅ на smoke), `.bookva.cmd` + `.slovo.cmd`. README + bump `@books/tools` 0.3.2 → 0.4.0. Bonus fix: `xlsx.js` formatter null-cell crash (shared между analyst tools). Smoke Bookva 2026-05-01..02: 118 postings, take rate 37.76%. Wiki sync. **Локально на master, не push'ено.**_ + +_Updated: 2026-05-14 (вечер) — заведена `ozon-revenue-per-product` 🔴 active. CLI-tool в `packages/tools/`: за период по Ozon-магазину live из API считает net/gross/avg на проданный товар + take rate + FBS/FBO breakdown. Per-channel — `.cmd`-обёртки (паттерн репо). Spec одобрен: [.wiki/concepts/ozon-revenue-per-product-design.md](../.wiki/concepts/ozon-revenue-per-product-design.md). Не БД, не MCP, не `analyst/` (Finance API нет в БД — gap #1 [[db-analytics-baseline]]; user явно хочет live). Next: implementation plan через `superpowers:writing-plans`._ + +_Updated: 2026-05-14 (день) — `slovo-long-sale-promo` 🟢 **DONE end-to-end** ✅. Cohort1 (67) + cohort2 (168) = 235 SKU добавлены, activate отработал, **235/235 applied + 0 failed** (после retry'я 1 SKU). По следам — 2 bag в `ozon-promotion-activate.js` занесены в [business-logic-bugs](business-logic-bugs.md): (1) MinPrice clamp `9582fbe` пропущен в activate.js строка 155 (только в add-products.js); (2) `if (failed === 0)` строка 173 блокирует transition `scheduled→active` при любой failure. Workaround — ad-hoc `_retry-slovo-minprice.mjs` + manual SQL. Промо `id=2 long-sale-slovo` живая до 2026-05-27 14:00 UTC. Wiki sync pending._ + +_Updated: 2026-05-14 — заведена `slovo-long-sale-promo` 🟡 paused. Аналог bookva long-sale для канала Slovo (`id_sales_channel=2`). Воронка собрана по playbook'у [[ozon-promo-candidate-selection]]: cohort1 ≥60д+price∈[900,1500) → 67; cohort2 [30,60)д+price≥900 → 168; total **235 SKU** (bookva был 664, slovo меньше — 81% slovo IN_SALE дешевле 900₽). Акция `id=2 long-sale-slovo` создана в `scheduled`, -30%, до 2026-05-27. Pending: `ozon-promotion-add-products` обе партии + `ozon-promotion-activate`. Кандидаты в `tmp-designs/slovo-long-sale/`._ + +_Updated: 2026-05-13 (поздний вечер) — `ozon-promotions-rename-and-rekey` **DONE locally (awaiting push)** ✅. Скоуп расширили в процессе: помимо `ozon_` префикса + drop `id_product` + `snapshot_min_price`, переехали `promotions.id_seller` → `id_sales_channel` (FK к `sales_channels`). Ozon-creds остались на `sellers` (переезд на channels — отдельная задача). Migration 017 применена на проде, упала дважды (safe-update-mode + FK-prefix-index drop), оба fix зашиты в файл. 103/103 tools-tests green. Bumps @books/data 0.0.11, @books/tools 0.3.0. Live promo id=1 (12 SKU долгопрод) — данные сохранены через RENAME+UPDATE JOIN._ + +_Updated: 2026-05-13 (вечер) — заведена `ozon-promotions-rename-and-rekey` 🔴 active. После запуска live-promo выявлены 2 фундаментальных design-bug'а брейнсторма 12.05: (1) таблицы `promotions/promotion_products/promotion_price_snapshots` без префикса `ozon_` хотя фича строго про Ozon (конвенция books — `ozon_*` для Ozon-domain); (2) FK `id_product` к `books.products` для чисто Ozon-сущности (Ozon `offer_id` + `product_id` достаточны, FK создаёт ложную связь и заставляет резолвить `products.sku → offer_id` в активате). Fix: rename 3 таблиц на `ozon_*`, drop `id_product` FK, использовать `offer_id` как натуральный ключ, добавить `snapshot_min_price` для proper restore. Live promo (id=1, 12 SKU) сохраняется через RENAME TABLE. `domain-promotions-fix-real-ozon-wiring` 🟢 closed by commit afb423f._ + +_Updated: 2026-05-13 — заведена `domain-promotions-fix-real-ozon-wiring` 🔴 active. Pre-flight check для «12 SKU long-sale -30%» (12 товаров из `tmp-designs/long-sale-prices-result.json`, канал Bookva-Ozon, -30%, 2 недели) выявил блокер: все 4 CLI домен-промо шипнуты с stub'ами `importPrices: async()=>{}` и `getOzonPrice: async()=>({price:0,oldPrice:0})` — в Ozon ничего не уходит, snapshot=0, иллюзия успеха (БД пишется). Плюс bug в `promotion-add-products.js`: lookup `productSalesChannel` по `{idProduct}` ожидает поля `product_id`/`offer_id` отсутствующие в схеме `product_sales_channels`. `ozon_seller_products` устарел на 4 года (last update 2022-03-30) — не использовать. Решение: `products.sku == offer_id`, Ozon `product_id` через `productAPIGetProductInfoPrices(filter:{offer_id})`. Long-sale-promo заблокирована до починки._ + +_Updated: 2026-05-08 (ночь, prod-verified) — `docker-runner-env-decoupling` **CLOSED end-to-end** ✅ (scheduler 1.5.3 на проде, commit `bbbb428`). Восстановлен инвариант «добавить новый docker-tool = 0 правок volume default.json». `${config:KEY}` resolver в `dataToEnv(data, taskEnv, rootConfig)`, taskDef.env, `hashTask` включает env, `docker.containerEnv` удалён, добавлены `web.internalUrl`/`api.internalUrl` (внутренние имена для docker network proxy), PDF defaults 100×100mm в tasks.json (literal, не в default.json). 4 итерации релиза: 1.5.0 → 1.5.1 (internal split + ошибочно pdf section в default.json) → 1.5.2 (revert pdf section) → 1.5.3 (PDF 100×100). 21/21 unit-тест. Прод volume default.json обновлён юзером (api/web internalUrl, s3.bucket, docker engine settings, puppeteer.browserWSEndpoint = ws://chrome:3000?token=..., docker.containerEnv удалён). Smoke `createPickingListPdf` на проде успешен. Wiki sync ([sources/docker-runner-env-decoupling.md](../.wiki/sources/docker-runner-env-decoupling.md))._ +_Updated: 2026-05-08 (ночь) — `docker-runner-env-decoupling` **CLOSED** ✅ (scheduler 1.5.0). Восстановлен инвариант «добавить новый docker-tool = 0 правок volume default.json». `${config:KEY}` resolver в `dataToEnv(data, taskEnv, rootConfig)`, taskDef получает поле `env { ... }`, `hashTask` теперь включает env, `docker.containerEnv` удалён. 12 новых unit-тестов (21/21 green). Wiki + log sync. **One-time prod migration** на VPS volume `/opt/books/job-scheduler/config/default.json` — pending push approval (см. .tasks/docker-runner-env-decoupling.md → "Resume protocol")._ +_Updated: 2026-05-08 (поздний вечер) — `fbs-picking-list-pdf` end-to-end на проде ✅. Run 15814 (idPickingList=88): chrome render 21KB → S3 → api PATCH success, `pdf_remote_path` в БД. По ходу 5 hot-fix циклов: (1) sqlite3+cpu-features YN0009 на task-runner+web → root `dependenciesMeta.built=false`; (2) chrome WS 404 → restart 2-week-old browserless; (3) PATCH api fetch failed → API_URL `http://books-api:3021` internal; (4) [id].vue + [id]/print.vue Nuxt collision → rename в [id]/index.vue; (5) **WEB_URL puppeteer ходил на `books.kzntsv.site` который Traefik роутит на api**, исправлено на `http://books-web:3000`. По следам — заведена `docker-runner-env-decoupling` ⚪ (рефакторинг архитектуры env'ов docker-tools, делать в новой сессии). Pending: подобрать `PDF_HEIGHT` (юзер; default 177mm избыточен для 1-2 книг), full reserve-цепочка smoke (`fbsPickingReserve` с 2 attachments в email), wiki sync, close STATUS._ +_Updated: 2026-05-08 (вечер) — `fbs-picking-list-pdf` Tasks 1-17 + wiki сделаны локально (13 commits на master, **не push'ено**). 5 версий bumped (web/api/tools/scheduler/task-runner). 27 unit-тестов (10 api + 12 tools + 8 waitForTaskRun + 7 spawnPdfAndAttachReport — все green). Деривации: `ctx.agenda` нет → `lib/agendaClient.js` singleton; puppeteer-core `^5.5.0` (workspace) вместо `^23`; tool kebab-case вместо camelCase. Pending user: (a) local smoke 4 сервисов; (b) push (project-discipline Rule 4 ask-mode); (c) prod migration 015; (d) prod containerEnv API_TOKEN через volume-mount; (e) prod trigger fbsPickingReserve verify._ +_Updated: 2026-05-08 (день) — `fbs-picking-list-pdf` implementation plan расписан → [.tasks/fbs-picking-list-pdf-plan.md](fbs-picking-list-pdf-plan.md). 21 task, 7 phases (DB → print page → tool → scheduler config → chain → smoke → wiki). 3 open questions резолвлены: (1) image=`node:22-bookworm-slim` без chrome, переиспользуем remote chrome `wss://chrome.kzntsv.site` через `puppeteer.connect()`; (2) token-auth=query `?token=` через existing `02-auth.js` + bypass в `auth.global.ts` для print-роутов; (3) sync email-PDF=in-process polling `task_runs` (timeout 5min, на fail email с XLS-only + warning). Не новый workspace `pdf-generator/`, а tool в `packages/tools/` + matrix entry. DB-write через api `PATCH /fbs/pickingLists/:id/pdf`, не direct sequelize._ +_Updated: 2026-05-08 — `scheduler-docker-runner` **CLOSED** ✅. Smoke testEcho на проде: run id=15456, status=success, log с 5×`[N/5] hello` + stderr `done after 5 iterations`, container_id non-null, --rm отработал, agendaJobId Bug#3 pattern. Phase 5 готов end-to-end. Инцидент по пути: Gitea API stuck → решился рестартом `gitea`+`gitea-db` (вики `concepts/gitea-stuck-recovery.md`). Native build deps (python3+make+g++) добавлены в scheduler/task-runner Dockerfiles. `fbs-picking-list-pdf` unblocked → 🔴 active. Total 27 commits в master (T1-T20 + 4 fix-коммита: yarn.lock, native build, summary)._ +_Updated: 2026-05-07 (T20 wrap-up) — `scheduler-docker-runner` Phase 5 готов локально (20 commits, T1-T19 на master локально). Smoke тест `testEcho` ждёт push → build-tools.yml matrix → deploy. После verify закроется до ✅. `fbs-picking-list-pdf` 🟡 → 🔴 unblocked._ +_Updated: 2026-05-07 (поздний вечер) — implementation plan для `scheduler-docker-runner` написан: [.tasks/scheduler-docker-runner-plan.md](scheduler-docker-runner-plan.md). 20 tasks с TDD red→green→commit, 4 pure-logic utils + handler с mocked dockerode + параметризованный Dockerfile + matrix workflow + smoke testEcho. Open questions резолвлены (pull retry 3x exp 5/15/45s, heartbeat 10s, smoke image bash:5.2 или dedicated). Next: запустить execution через `superpowers:subagent-driven-development` (recommended) или `superpowers:executing-plans`._ +_Updated: 2026-05-07 (вечер) — после TDD-разнарядки и discovery что Phase 5 docker-runner — stub. Заведена `scheduler-docker-runner` 🔴 active (Phase 5 в [pluggable-scheduler](../.wiki/concepts/pluggable-scheduler.md), `handlers/docker.js` отсутствует). `fbs-picking-list-pdf` переведена в 🟡 paused — blocker на Phase 5. PDF теперь рендерится через docker-runner (новый workspace `packages/pdf-generator/`), task-runner image остаётся чистым. TDD: Phase 5 — TDD целиком (Q2/Q3/Q4); PDF — композит (visual/oneshot/wrapper для UI/migration/Dockerfile + TDD для pdf-generator core/auth/reserve-цепочки/email-mod)._ +_Updated: 2026-05-07 — заведена `fbs-picking-list-pdf` 🔴 active. PDF-версия листа подбора рядом с XLS в reserve-цепочке, карточки по местам, портрет A4, mobile-first для сборки с телефона при RKN-блокировке web. Спека утверждена: [.wiki/concepts/fbs-picking-list-pdf.md](../.wiki/concepts/fbs-picking-list-pdf.md). Implementation plan ещё не написан._ +_Updated: 2026-05-04 — заведена `sync-prices-ozon-to-yandex` 🔴 active. Tool в `packages/tools/` который тянет live цены с указанного Ozon-канала и проставляет в YM-канал (1:1 на поле `price`, `old_price` генерится по legacy-алгоритму `generateOldPrices.js`). Hybrid CLI+module + handler-обёртка в одном файле; thin re-export stub в `task-runner/tasks/` для auto-discovery реестра. tasks.json entry без cron (manual trigger). Дизайн одобрен (brainstorming sesh), код в работе. Также: paтern `packages/task-runner/tasks/` объявлен устаревшим — новые handler-обёртки в `packages/tools/`. +_Updated: 2026-05-03 — `analyst-report-formats` **CLOSED** ✅. Реализованы 5 pluggable formatters (console/md/xlsx/html/json), CLI-dispatcher, output-path helper, 11 unit тестов. Wiki обновлена. Хук yarn.lock улучшен (проверяет dependencies/devDependencies, не весь package.json). Push в master (commit b6007f8). Pattern готов для первых analyst tools (staff-performance / listings-vs-orders / sales-seasonality). +_Updated: 2026-05-01 (поздний вечер²) — `books-analyst-mcp` **CLOSED целиком**. Phase 1.3 (`books-data-mcp`) отменён архитектурно: вместо stdio MCP-сервера над `@books/data` аналитика будет писаться как обычные JS-файлы в `packages/tools/analyst/.js` (паттерн hybrid CLI+module — соседи `ozon-transactions-analysis.js`, `ozon-products-price-analysis.js`). LLM работает в `books/`, sequelize-модели открываются `Read`-инструментом напрямую → MCP-stdio + ssh-tunnel + saved queries — лишний слой. План `docs/superpowers/plans/2026-04-30-books-data-mcp.md` помечен OBSOLETE первой строкой. Phase 1.4 (analyst-каталог) отменён вместе с 1.3. Phase 1.2 (yandex-market-mcp) остаётся отдельной задачей на полке. Решение в [.wiki/concepts/analyst-tooling.md](../.wiki/concepts/analyst-tooling.md)._ +_Updated: 2026-05-01 (поздний вечер) — заведена `ozon-knowledge-base-ingest` 🟡 paused. Цель — слить базу знаний `seller-edu.ozon.ru/libra` (комиссии/тарифы + цены/акции + дайджест) в `.wiki/raw/seller-edu/` для LLM-аналитика. Эксперимент: Ozon Antibot палит CDP-протокол → markitdown/curl/playwright (headless/headful/stealth) — всё 403 либо "Доступ ограничен". Юзер параллельно saved страницу через **obsidian-web-clipper** → референсный MD (290 KB из 369 KB HTML, таблицы/якоря/frontmatter сохранены). **Архитектурный вывод:** clipper использует под капотом open-source `defuddle` (MIT, [github.com/kepano/defuddle](https://github.com/kepano/defuddle)) — node.js библиотека для clean content extract + turndown. Финальная архитектура задачи: **A3 (playwright CDP-attach к НЕ-playwright Chrome юзера)** для HTML acquisition + **defuddle npm** для HTML→MD = byte-совместимо с тем что clipper делает руками. Pipeline сделать в `packages/seller-edu-ingest/`. Artefacts/scaffold в [tmp-designs/seller-edu-experiment/](../tmp-designs/seller-edu-experiment/). Memory: `reference_ozon_seller_edu_antibot`._ +_Updated: 2026-05-01 (вечер) — `books-analyst-mcp` Phase 1.1 ✅ closed. Real-API smoke 11/11 на кабинете bookva/буква (10 OK + reviews.list 403 как ожидалось без Premium). В smoke'е вылез баг handler-обёртки SDK request'а (все 11 tools посылали пустой body, Ozon возвращал 400 «filter required»); пофикшен двумя коммитами: `1af4fcf` (wrapper показывает HTTP body в error text, bump 0.0.4) → `de3360c` (handler-ы переключены на плоский request, bump 0.0.5; покрыто `handler-body.test.js` 10 кейсов через `Configuration.fetchApi`-перехват). Wiki + log + decision log синхронизированы. Phase 1.2 (YM-MCP), 1.3 (books-data-mcp), 1.4 (analyst-wiring) — отложены, реактивация по запросу._ +_Updated: 2026-05-01 (день) — `books-analyst-mcp` 🔴 active. Scope Phase 1.1 сужен до **только `ozon-mcp`** (юзер: «набъём шишки на Озон, Яндекс потом будет легче»). 11 read-only tools, multi-shop через env-vars, SDK 0.0.30 покрывает все 3 TODO плана (Returns/Analytics/Reviews) реальными классами. Документация — через npm-mcp + результат в `.wiki/packages/ozon-mcp.md`. Plan: [docs/superpowers/plans/2026-04-30-ozon-mcp.md](../docs/superpowers/plans/2026-04-30-ozon-mcp.md). Per-task: [books-analyst-mcp.md](books-analyst-mcp.md). YM-MCP / data-mcp / analyst-wiring — отложены за Phase 1.1._ +_Updated: 2026-05-01 (утро) — `books-ops-mcp` **Phase 2 CLOSED**. 4 mariadb tools (query/describe + scheduler.recentRuns/failedRuns) на проде с APP_VERSION=master-d426d03. Smoke 7/7 green; `failedRuns(since='7d')` вытащил 8 known bugs из business-logic-bugs end-to-end. CI был сломан днём (502 Bad Gateway → runner+gitea restart + workflow_dispatch unblock; recovery pattern в memory `reference_gitea_ci_stuck_recovery`). Spec+plan: `docs/superpowers/{specs,plans}/2026-04-30-books-ops-mcp-phase2*`. Wiki `packages/ops-mcp.md` расширена per-phase секцией. Skill `.claude/skills/books-ops-mcp/SKILL.md` обновлён на 12 tools._ +_Updated: 2026-04-30 (поздний вечер ²) — `ntfy` Bug #1 fix запушен (pending verify): `gitea.run_id` → `gitea.run_number` в `RUN_URL` обоих workflow ([build.yml](../.gitea/workflows/build.yml), [deploy.yml](../.gitea/workflows/deploy.yml)). Root cause — Gitea UI route `/repo/actions/runs/{run}` парсит `{run}` как `runIndex` (per-repo sequential = `run_number`), а не глобальный `run.ID`. Build URL «работал» (HTTP 200, но открывал чужой run), deploy URL — 404 (run_id выше суммарного count). Verify: `git commit --allow-empty -m "verify ntfy URL" && git push` → tap ссылки в обоих ntfy-каналах. После verify закрыть `ntfy` или открыть Phase 3 (disk-alert)._ +_Updated: 2026-04-30 (поздний вечер) — закрыты `registry-gc` (Phase 1+2+3+5 на проде, цикл retention работает на двух фронтах Gitea+VDS) и `switch-npm-mcp-to-local-stdio` (`.mcp.json` переведён с docker на локальный node — убирает «MCP unavailable» при daemon/volume-mount issue'ах). `ntfy` переведена в 🟡 paused — Phase 1+2+4 на проде, замечен баг #1 (битая ссылка на Gitea в deploy-уведомлении, в build-уведомлении ОК) — не блокер, ждёт возврата._ +_Updated: 2026-04-30 (вечер close) — `books-ops-mcp` **Phase 1 CLOSED** ✅. 8 tools на проде, hot-fix `agendaState` (aggregate pipeline) задеплоен и verified end-to-end. Skill, wiki, business-logic-bugs обновлены. Phase 2 (recentRuns/failedRuns через MariaDB) не делаем без конкретного триггера. Юзер закрыл досрочно (без 24h soak) — все evidence для close уже были._ +_Updated: 2026-04-30 (вечер) — `books-ops-mcp` Phase 1 на проде закрыт по фактическим возможностям. После Phase 1 deploy первый же вызов `ops.scheduler.agendaState` показал output 95 KB (превышал Claude cap). Hot-fix `5b56dc3`: aggregate pipeline по name → 23 ряда (~9 KB), 33/33 unit-теста, push'ен и задеплоен (APP_VERSION=master-5b56dc3 в env). Все 8 tools работают на проде. По следам прохода — 4 бага бизнес-логики занесены в business-logic-bugs (commit `86be892`). Создан project-scoped skill `.claude/skills/books-ops-mcp/SKILL.md` (commit `4470ce1`) чтобы Claude не лез через ssh за тем, что покрыто tools. Wiki-страница [@books/ops-mcp](../.wiki/packages/ops-mcp.md) добавлена._ +_Updated: 2026-04-30 (день) — scheduler два hot-fix'а пушены (`0d5ffd6` + `b6dcdfc`): (1) `productsIndexSyncronization` legacy zombie-handler убран из `tasks.json` после disable=true в Mongo; (2) **критический**: cron http-jobs не выполнялись из-за idempotency check на shared Mongo `_id` — все ~20 cron'ов годами не sync'ились (sync остатков!), fix `agendaJobId = ${_id}:${Date.now()}` в http-handler. После recreate'а scheduler юзер подтвердил «задвигался». Также написаны spec + 4 плана для `books-analyst` MCP-стека (`docs/superpowers/{specs,plans}/2026-04-30-*`), но **юзер поставил `books-ops-mcp` ВЫШЕ analyst в приоритете** — для ускорения фиксов и развязывания рук в SSH._ +_Updated: 2026-04-29 — registry-gc Phase 5 (VDS-side `vdsDockerCleanup` через `books-docker-proxy`) committed локально, ждёт user-go на push. Юзер уточнил: Gitea — 5TB пофиг, горит на VDS. Phase 1+2+3 (Gitea side) ранее запушены. poll-pending-ozon-imports Phase 1-5 запушена, pending Phase 6 verify._ +_Updated: 2026-04-28 — заведён scheduler-ui-improvements (имя модуля в логе + copy-all-info кнопка). business-logic-bugs пополнен fbsPickingReserve `recipients: []` SMTP-вспышкой + закрыт loadProductsToWarehouse double-run (fix локально). Заведён scheduler-bugs (404 на detail-страницах тасков из-за отсутствующего tasks.json в api image, запушен). Заведён scheduler-core-replace (концепт замены Agenda на свой core, idea-only). agenda-admin-ui CLOSED. Заведена npx-runner-pattern (документирована, не реализована)._ + +## 🟢 [move-promo-bookva-to-slovo](move-promo-bookva-to-slovo.md) — DONE 2026-05-21 — свёрнут Bookva долгопрод, перенесён на Slovo (-30% до 04.06) +**Status:** done +**Outcome:** Bookva promo id=1 (663 SKU active, -30%) → ended + restored. Slovo promo id=2 (235 SKU active, -30%) → ended + restored (превентивно, чтобы snapshot copy не утянул slovo-цены в новой id=3). Создана slovo-promo id=3 `long-sale-slovo-2026-05-21`, -30%, до 2026-06-04 14:00 MSK, 660/660 applied. По пути найден 3-й bug в `ozon-promotion-deactivate.js` (defensive expected=snapshot вместо action) — обход через oneshot `_push-prices-from-snapshot.mjs`. 3 SKU `19119894`, `Б2008714`, `Б607029` отсутствуют в Slovo каталоге (добавлены в Bookva после Pass 1 миграции) — не критично для текущей задачи. Bookva-каталог остался в продаже с нормальными ценами (по решению юзера). Артефакты прогона в `tmp-designs/bookva-cancel-to-slovo/`. +**Acceptance:** все 4 пункта задачи юзера ✅ (отменены акции в Букве с сохранением списка / цены в Слове = ценам Букве / создана акция в Слове на 2 недели / применена к 660 из 663). +**Blocker:** — +**Branch:** `master` (никаких коммитов; БД-state на проде через ad-hoc oneshot) + +--- + +## 🔴 [fbs-picking-list-pdf-thumbs](fbs-picking-list-pdf-thumbs.md) — bugfix: PDF листа подбора 38MB → SMTP 552; thumb-endpoint в api +**Status:** active +**Where I stopped:** написан task-файл, корень диагностирован (CDN full-res обложки puppeteer вшивает в PDF без даунсемпла); готов писать TDD red-tests для `thumbLib.js`. +**Next action:** написать `packages/api/server/routes/images/thumbLib.test.js` (parseThumbParams + isAllowedHost), убедиться что красные, затем impl + handler + sharp в package.json + swap src в `FbsPickingPlacePrint.vue`. +**Blocker:** — +**Branch:** `master` (commits локально, push после verify) + +--- + +## 🟢 [slovo-product-creation-recovery](slovo-product-creation-recovery.md) — DONE 2026-05-17 — Pass 3 + Pass 4 для снятия is_created=false блокеров +**Status:** done (awaiting push approval) +**Outcome:** Реализованы 2 новых CLI согласно concept'у [ozon-product-creation-blockers](../.wiki/concepts/ozon-product-creation-blockers.md). **Pass 3** `products-fix-creation-errors-from-mirror.js` (TDD 10/10, pure helpers `chunkArray` + `extractErrorAttrIdsByOfferId(items, codesWhitelist)`, reused `filterCopyableAttrs` из Pass 2): Slovo GetProductList(ALL) → GetProductInfoList → парсит `attribute_id` из `errors[]` по whitelist (default `CONDITIONAL_ATTRIBUTE_ERROR`) → Bookva matching → GetProductAttributesV4 → UpdateAttributes Slovo чанками 100. **Pass 4** `products-reimport-stuck.js` (TDD 8/8, pure helpers `chunkArray` + `isStuckCreating(item)`): auto-detect Slovo карточек с `is_created=false`+пустой `errors[]` → Bookva IN_SALE match (silent skip без master) → ImportProductsBySKU **без diff'а**. **Prod smoke apply:** Pass 3 task_id=4479563894 **74/74 imported** (из 169 targeted / 102 Bookva matched), Pass 4 task_id=4479600569 **6/6 imported** (из 632 stuck / 626 без Bookva master). Bump `@books/tools` 0.9.0→0.10.0 MINOR (+2 bin/exports). README +2 секции. Wiki concept updated. **185/185 tests green** (+18). Ожидаемый эффект: 80 карточек после next cron syncStocksWithWarehouse (каждые 8ч) попадут в IN_SALE. +**Acceptance:** все 7 пунктов task-файла ✅. +**Blocker:** — +**Branch:** `master` (5 коммитов локально, **не push'ено** — project-discipline Rule 4 ask-mode) + +--- + +## 🟢 [slovo-in-sale-gap-diagnose](slovo-in-sale-gap-diagnose.md) — DONE 2026-05-16 — diag tool + два root cause fix (delay + filter) + Pass 1 #2 +**Status:** done +**Outcome:** Diagnostic CLI `analyst/ozon-in-sale-gap.js` (TDD 13/13: chunkArray + bucketizeInfoItem с priority chain archived > out_of_stock > no_price > failed > state_failed > moderate > status > state) + `.cmd`-обёртка + pluggable formatters через `runFormat`. Root cause #1: **`sellers.put_on_sale_delay_days`** для Slovo=7 vs Bookva=0 → 433 свежих книг с `put_on_sale_date >=2026-05-09` не попадали в available SQL → stock=0. Юзер поставил delay=0. Manual `syncStocksWithWarehouse(idSeller=2)` +194 IN_SALE (4548→4742). Root cause #2: фильтр legacy У/Б2/К в Pass 1+2 был слишком широкий, юзер: «только У не переносим». Filter обновлён. Bump `@books/tools` 0.6.0→0.8.0 (0.7.0 — diag tool MINOR, 0.8.0 — filter change MINOR). Pass 1 #2 на проде: 444 кандидата (включая Б2/К), 5 task_id, 0 unmatched. Прогноз через 1-3 дня после модерации: Slovo ~5180 IN_SALE (~95.6% от Bookva 5418). Локально 1 коммит pending push. +**Acceptance:** все 7 пунктов из task-файла ✅ (CLI создан / pipeline 5 шагов / bucket-разбивка / .cmd-обёртка / README / bump MINOR / prod smoke с numbers). +**Blocker:** — +**Branch:** `master` + +--- + +## 🟢 [ozon-products-copy-bukva-to-slovo](ozon-products-copy-bukva-to-slovo.md) — DONE 2026-05-15 — Bookva → Slovo: массовая привязка по SKU + докачка контент-рейтинга +**Status:** done (Phase 1+2 prod-shipped; ожидает user-go на push) +**Outcome:** Pass 1 — `products-import-by-sku.js` extended (CLI-flags, IN_SALE pivot), prod 608 IN_SALE-кандидатов в Slovo через 7 task_id, 0 дубликатов. Pass 2 — новый CLI `products-fill-improve-attributes.js` (TDD 13/13, pure helpers chunkArray/buildImproveIdsByOfferId/filterCopyableAttrs), prod smoke `--limit 100` → `task_id=4463148527` → 31/31 `imported` (verified GetImportProductsInfo, 10/31 имеют пост-импортные модерационные warnings — исходные проблемы каталога). `@books/tools` 0.5.1 → 0.6.0 MINOR. 4 Pass 2 commits локально (`77a1acf`, `56d5150`, `03b0e88`, `c5ae6e7`) — **не push'ено**, pending user-go. SDK discovery закрыл open вопросы 3-4 (chunks 100/1000/100, dry-run console-table); скорректирована спека §pipeline §8 (UpdateAttributes возвращает только `{task_id}`, per-item через GetImportProductsInfo). +**Next action:** user-go на push origin/master. Дальнейшая батч-докачка — повторные `--limit 100` (3825 ост. кандидатов). +**Spec:** [.wiki/concepts/ozon-products-copy-bukva-to-slovo.md](../.wiki/concepts/ozon-products-copy-bukva-to-slovo.md) +**Acceptance:** Pass 1 — **10/10 ✅**. Pass 2 — **9/9 ✅**. + +--- + +## 🟢 [ozon-revenue-per-product](ozon-revenue-per-product.md) — DONE 2026-05-14 — выручка с проданного товара по магазину Ozon +**Status:** done +**Outcome:** CLI-tool `packages/tools/ozon-revenue-per-product.js` + `.bookva.cmd` + `.slovo.cmd`. 4 pure-функции под TDD (18 тестов). Live Finance + FBS/FBO API, pluggable formatters. Smoke на Bookva ✅ (console/json/xlsx). README. `@books/tools` 0.4.0. Bonus fix shared `xlsx.js` formatter. +**Blocker:** — +**Branch:** `master` + +--- + +## 🟢 [slovo-long-sale-promo](slovo-long-sale-promo.md) — DONE 2026-05-14 — аналог bookva long-sale для slovo (id=2) +**Status:** done +**Outcome:** End-to-end. Воронка собрана (cohort1 67 + cohort2 168 = 235 SKU), оба add-products пробежали без fail. Activate упал на 1 SKU (139258946) с MinPrice violation → ad-hoc retry script + manual SQL transition → **235/235 applied, 0 failed**. Промо `id=2 long-sale-slovo` `active`, -30%, до 2026-05-27 14:00 UTC. По следам — 2 bag в [business-logic-bugs](business-logic-bugs.md) (activate.js: clamp пропущен + status transition блокируется failure). Кандидаты + retry-log в `tmp-designs/slovo-long-sale/`. Deactivate в календарь на 2026-05-27. + +--- + +--- + +## ✅ [scheduler-docker-runner](scheduler-docker-runner.md) — CLOSED 2026-05-08 — Phase 5 в pluggable-scheduler `runner: 'docker'` +**Status:** closed +**Outcome:** **End-to-end на проде.** 4 pure-logic utils + handler.js (full lifecycle: pull-on-miss → semaphore → attach+demux+ring-buffer → wait+timeout/SIGTERM/SIGKILL → mapExit → finalize → remove always) + 12 unit-тестов + параметризованный `packages/tools/Dockerfile` (`BASE_IMAGE`+`BIN_NAME`) + matrix `build-tools.yml`. **Smoke testEcho:** run id=15456, status=success, container_id non-null, log = `[1/5] hello`×5 + stderr `done after 5 iterations`, --rm отработал, agendaJobId `${_id}:${ts}` Bug#3 pattern. По пути решены: yarn.lock drift (YN0028), native build deps в alpine (python3+make+g++), volume-mount sync default.json на VPS, инцидент Gitea API stuck (рестарт `gitea`+`gitea-db`, вики обновлена). Bumped `@books/job-scheduler` 1.2.3→1.3.0. + +--- + +## 🟢 [ozon-promotions-rename-and-rekey](ozon-promotions-rename-and-rekey.md) — fix 3 design proeb'а: ozon_ prefix + drop id_product + id_seller→id_sales_channel +**Status:** done (awaiting push) — 103/103 tools tests green, migration 017 применена на prod, live promo id=1 (12 SKU) данные сохранены +**Where I stopped:** 2026-05-13 вечер. Scope расширили в процессе: помимо `ozon_` prefix + drop `id_product` + `snapshot_min_price`, переехали `promotions.id_seller` → `id_sales_channel` (FK к sales_channels). Ozon-creds остались на sellers — отдельная задача. Migration упала на проде дважды (safe-update-mode + FK-prefix-index) — оба fix зашиты в файл. +**Acceptance:** (1) ✅ migration 017 + RENAME 3 таблиц, drop id_product, id_seller→id_sales_channel, snapshot_min_price; (2) ✅ 3 sequelize моделей переименованы, FK ассоциация на salesChannel; (3) ✅ 5 CLI переименованы, --product-ids→--offer-ids, workaround через books.products.sku удалён (probe Ozon by offer_id); (4) ✅ _lib/ozon-promotions/ + side-effects через salesChannel.getSeller(); (5) ✅ tests 103/103 (26 lib + 77 CLI); (6) ✅ bumps @books/data 0.0.10→0.0.11, @books/tools 0.2.2→0.3.0. +**Blocker:** — +**Branch:** `master` + +--- + +## ✅ [domain-promotions-fix-real-ozon-wiring](domain-promotions-fix-real-ozon-wiring.md) — CLOSED 2026-05-13 — real Ozon SDK + min_price + 5 багов +**Status:** closed (commits 89f4522 + afb423f, not pushed) +**Outcome:** Подключены real Ozon SDK вызовы к 5 domain-promotion CLI. Фикс через несколько итераций после pre-flight check'а на live-promo «long-sale -30% / 12 SKU»: (1) stub `importPrices/getOzonPrice` → real (`createOzonSideEffects`); (2) bug `productSalesChannel` lookup → `Product.findByPk → sku → Ozon API`; (3) `@books/data` пропустил регистрацию 3 promotion-моделей — добавлены; (4) `applyPriceToOzon` throws на failure (не глотает в results[].success=false); (5) `min_price` default = price (Ozon отвергал push ниже min_price); (6) `ozon-side-effects._unwrap` достаёт body из `ResponseError`. **Live promo `id=1` применена** через recovery oneshot — 12/12 цен -30% в Ozon верифицировано через `ozon_prices_list`. 58 unit-тестов green. Bump @books/tools 0.2.0→0.2.2, @books/data 0.0.9→0.0.10. Замечен в processе фундаментальный design defect — заведена follow-up `ozon-promotions-rename-and-rekey`. + +--- + +## 🟡 [fbs-picking-list-pdf](fbs-picking-list-pdf.md) — PDF-версия листа подбора в reserve-цепочке (auto в email рядом с XLS) +**Status:** implementation done (Tasks 1-17 + wiki), awaiting prod verify +**Where I stopped:** 2026-05-08 вечер. Tasks 1-17 + Task 20 (wiki) сделаны, 13 commits локально. 27 unit-тестов green. 5 версий bumped. Не push'ено. +**Next action:** (a) локально запустить 4 сервиса (api/web/scheduler/task-runner) и сделать end-to-end smoke (открыть `/fbs/picking-lists/:id/print?token=` в браузере, проверить что layout без navbar, page-break per place; trigger `fbsPickingReserve` через UI, ждать `[pdf]` записей в task-runner консоли, ждать email с двумя attachment'ами); (b) **разрешить push** в master — auto-build+deploy всех 4 стеков через Gitea Actions; (c) на проде apply миграцию 015 (`packages/data/migrations/015-add-pdf-remote-path.sql`, idempotent); (d) на проде в scheduler default.json через volume-mount выставить `docker.containerEnv.API_TOKEN` реальным значением (текущий placeholder ``); (e) trigger fbsPickingReserve на проде, verify `pdf_remote_path` заполнено + email с двумя attachment'ами. +**TDD:** компонент'ы по-разному покрыты. Q3 (api endpoint Q3 identifier security) — TDD pure-logic helpers (10 тестов). Q2/Q3 (tool puppeteer/S3/api PATCH) — 12 unit-тестов с mocked deps. Q4 (waitForTaskRun + spawnPdfAndAttachReport branching) — 15 unit-тестов. Visual/oneshot/wrapper — `[skip-tdd: …]` маркеры в commits. +**Blocker:** — +**Branch:** `master` (13 commits локально, awaiting push approval) + +--- + +## 🟡 [sync-prices-ozon-to-yandex](sync-prices-ozon-to-yandex.md) — копир цен Ozon→YM (`packages/tools/`) +**Status:** paused — Phase 1 закоммичен, smoke OK, real apply упёрся в YM rate-limit на ~37 chunk'е. +**Where I stopped:** 2026-05-04. Local apply (dryRun=false) на каналах 1→3: первые 37 chunks (~18500 цен) ушли успешно, остальные ~164 chunks (~81 800 SKU) упали в `ym-update-failed` с generic «Response returned an error code» (SDK прячет body). Diag-probe одиночными запросами те же sku — оба `200 OK`, значит payload и ключ валидные → причина похожа на YM rate-limit/quota windowing. **Side effect: первые ~18500 SKU в YM-кабинете Bookva уже имеют новые цены + новые рандомные `discountBase`** (sku 100001+ из верха списка по id_product). Не страшно — repeated apply просто перезапишет, цены идемпотентны. +**Next action:** (a) добавить fetchApi-middleware в `sync-prices-ozon-to-yandex.js` который читает body на не-2xx и кидает с status+body (по аналогии с `de3360c` для Ozon SDK); (b) тротлинг между chunks (1-2 sec) + retry-on-429 с backoff 30-60s (сейчас 2/4/8/16/32 — мало для YM-окон); (c) опционально — chunk size <500. После (a) re-probe одного из failed chunk'ов чтобы увидеть конкретный YM-error → root cause → fix → re-apply. +**Blocker:** — +**Branch:** `master` + +--- + +## 🟡 [ozon-knowledge-base-ingest](ozon-knowledge-base-ingest.md) — слить seller-edu.ozon.ru/libra в .wiki/raw/seller-edu/ +**Status:** paused — архитектура **A3 + defuddle** выбрана, не реализована. Сотни страниц вручную не вариант; defuddle (open-source MIT, движок obsidian-web-clipper) даёт референсное MD, playwright `connectOverCDP` к настоящему Chrome юзера обходит anti-bot. +**Where I stopped:** 2026-05-01 поздний вечер. Эксперимент: markitdown solo / curl / playwright headless / headful / stealth — всё 403 либо "Доступ ограничен" (Ozon палит CDP-fingerprint). Юзер параллельно вручную saved страницу через obsidian-web-clipper — MD-качество референсное, **показал что defuddle (=движок clipper'а) — путь**. Финальная архитектура: A3 (CDP-attach к НЕ-playwright Chrome) для HTML acquisition + defuddle npm для HTML→MD + frontmatter писать самим. Артефакты эксперимента в [tmp-designs/seller-edu-experiment/](../tmp-designs/seller-edu-experiment/), пример качественного MD — [.wiki/raw/2026-05-01-вознаграждения-...md](../.wiki/raw/). Лессон в memory `reference_ozon_seller_edu_antibot`. +**Next action:** реализовать pipeline (см. шаги 1-9 в task-файле). Prereq — юзеру настроить Chrome с `--remote-debugging-port=9222` постоянно. Pipeline: discovery sitemap → defuddle pipeline test на 1 URL → seed комиссии/тарифы (throttle 8s/req, ≤30/сессия) → seed цены/акции → дайджест-cron в job-scheduler. Параллельно — проверить публичный Telegram-канал Озон как резервный канал дайджеста. +**Blocker:** — +**Branch:** `master` + +--- + +## ✅ [pluggable-scheduler](pluggable-scheduler.md) — CLOSED 2026-04-28 (+ Phase 9 + private S3 fix 2026-04-28) +**Status:** closed +**Outcome:** Phase 1-7 + Phase 9 в проде. 23 http-таска в [tasks.json](../packages/job-scheduler/config/tasks.json) с `label`/`description`/`params` для админки. Pure reconciler (`config.jobs[]`+`lib/jobs/` удалены), все таски рендерят markdown-отчёт + always-upload в S3 (`reportS3Key` контракт после 2026-04-28 — bucket private, api-proxy с `requireAdmin`), `task_runs` self-cleanup. Mongo-флаг `_fromYml` мигрирован на `_fromConfig` в startup-миграции. Playbook `docs/playbooks/pluggable-scheduler.md` обновлён + ALS log-capture как Phase 5b + грабли 21-25. + +--- + +## ✅ [agenda-admin-ui](agenda-admin-ui.md) — CLOSED 2026-04-28 +**Status:** closed +**Outcome:** Все 5 фаз + post-launch polish + все 4 deferred follow-up закрыты в одной сессии. Раскатано в `74254ea`. Главное: ALS console-capture в `task_runs.log` (ctrl-через-Z для дебага), markdown-отчёты через api-proxy на приватный S3 (старый MinIO Browser UI больше не вылезет), human-readable filename при скачивании, RunsTable порядок+ссылки, UPagination, sales-channels автоподсказки. Wiki синхронизирована: новая страница [Task log capture](../.wiki/concepts/task-log-capture.md), обновлены [Task-runner handlers](../.wiki/concepts/task-runner-handlers.md) и [SalesChannel](../.wiki/entities/sales-channel.md). + +--- + +## 🟡 [poll-pending-ozon-imports](poll-pending-ozon-imports.md) — split loadProductsToOzon на submit + cron poll +**Status:** implementation done, awaiting prod verify +**Where I stopped:** Phase 1-5 + wiki сделаны локально (resume 2026-04-29). Tool/handler/poll-job/tasks.json/UI polling/api no-cache/wiki — всё в индексе. Push готов; следующий шаг — push в master, дождаться auto-deploy всех 4 стеков, manual trigger main run в проде, через 5-10 мин проверить что poll-job tick прошёл и mutate'ил result. +**Next action:** push → wait deploy → trigger main run на проде → verify (см. Resume protocol в task-файле). +**Blocker:** — +**Branch:** `master` + +--- + +## ✅ [docker-runner-env-decoupling](docker-runner-env-decoupling.md) — CLOSED 2026-05-08 — расщеплены infra-creds и task-contract +**Status:** closed (implementation done, локально, awaiting push approval + one-time prod volume migration) +**Outcome:** Восстановлен инвариант «**добавить новый docker-tool = 0 правок volume default.json scheduler-а**». Реализован syntax `${config:KEY}` (string-form, partial interpolation supported) в `dataToEnv(data, taskEnv, rootConfig)`. taskDef получил поле `env { ... }` в tasks.json. `default.json` причесан: `docker.containerEnv` удалён, появились `api.{url,authToken}`, `web.url`, `s3.bucket`. `hashTask` включает env → правка блока в tasks.json триггерит reconcile. 12 новых unit-тестов (21/21 green: `dataToEnv: ${config:KEY}` whole-string / partial / multiple refs / literal / unknown ref throws / data+taskEnv merge / numeric/boolean stringify / null skipped / undefined arg + handler-level taskDef.env merged into Env / no-env passthrough). Bumped `@books/job-scheduler` 1.4.0→1.5.0 [MINOR]. Wiki [scheduler-docker-runner](../.wiki/concepts/scheduler-docker-runner.md) обновлён, decision-record в [log.md](../.wiki/log.md). **One-time prod migration** на VPS volume `/opt/books/job-scheduler/config/default.json` — добавить секции `api.{url,authToken}`, `web.url`, `s3.bucket`, удалить блок `docker.containerEnv`. После — никаких правок при добавлении следующих docker-tools. + +--- + +## ⚪ [npx-runner-pattern](npx-runner-pattern.md) — npx-таски как третий способ запуска (без third runner-type) +**Status:** ready +**Where I stopped:** Phase 5c в [pluggable-scheduler playbook'е](../docs/playbooks/pluggable-scheduler.md) задокументирован (2026-04-28). Два пути: A) handler+`spawn('npx',...)` для http-runner-а, B) docker с `image: node:N` + `command: ['npx',...]`. В коде ничего ещё нет. +**Next action:** при первом реальном npx-кандидате (например `audit-ci` weekly или `npm-check-updates`) — Phase 1: reference-handler `runNpxCommand.js`. Phase 2 (docker `command` override) откладывается до имплементации docker-runner-а. +**Blocker:** — +**Branch:** — + +--- + +## 🟡 [scheduler-bugs](scheduler-bugs.md) — реестр багов /scheduler UI и /admin/scheduler/* api +**Status:** 2 hot-fix'а пушены 30.04, awaiting verify (юзер подтвердил «задвигался»), real-fix rewrite позже +**Where I stopped:** +- **Bug #1** (2026-04-28): 404 на `/admin/scheduler/tasks/` — open / fixing. +- **Bug #2** (2026-04-30): `productsIndexSyncronization` legacy handler с зомби-локами. **Mitigated** через `disabled:true` в Mongo + удалён task definition из `tasks.json` (`5c17135`). Real fix (rewrite или delete handler) — отдельной задачей. +- **Bug #3** (2026-04-30): **критический** — cron http-jobs не выполнялись годами. `handlers/http.js:14` шлёт `String(job.attrs._id)` как `x-agenda-job-id`. Для cron через `agenda.every()` это shared id → task-runner `findByAgendaJobId` находит старый run от первого dispatch'а → возвращает 200 без создания нового → handler не выполняется. Все ~20 cron http-handler'ов (sync остатков, postings, prices…) thinks работают (Mongo `lastFin` обновляется), реально не делают ничего. **Fix:** `agendaJobId = ${_id}:${Date.now()}` (`ddbbffd`, push `b6dcdfc`). Юзер 30.04 ~04:00 МСК подтвердил «задвигался шедулер». +**Next action:** наблюдать стабильность 24h. Если возникнут новые failures — копать. Real rewrite `productsIndexSyncronization` — отдельной задачей или delete (если не нужен). +**Blocker:** — +**Branch:** `master` + +--- + +## ✅ [books-ops-mcp](books-ops-mcp.md) — CLOSED 2026-05-01 (Phase 1+2: 12 read-only tools для prod-диагностики) +**Status:** closed +**Outcome:** **12 tools на проде** (8 Phase 1 + 4 Phase 2). Phase 1 (`5b56dc3`, 30.04): 4 docker + 2 mongo + 2 scheduler-mongo. Phase 2 (`d426d03`, 01.05): `mariadb.query/describe` (generic) + `scheduler.recentRuns/failedRuns` (narrow). Mongo + MariaDB через docker network `proxy` (172.20.0.0/16), без exposing ports. **Defence-in-depth** Phase 1 (3 слоя: proxy-ro POST→403, mongo whitelist, env mask) + Phase 2 (6 слоёв: ops_ro@'172.%' GRANT SELECT, multipleStatements:false, checkSql regex+ALLOWED_STARTS, enforceLimit force LIMIT, query timeout, capPayload 1MB cap). 3 unit-tested helpers (TDD) + integration tests via docker-compose + smoke 7/7 на проде. Один review-fix (comment-strip в enforceLimit) и один hot-fix `failedRuns` (prod-схема: status='error' + колонка `error`, не 'failed'/fail_reason). Wiki [@books/ops-mcp](../.wiki/packages/ops-mcp.md) расширена per-phase. Skill `.claude/skills/books-ops-mcp/SKILL.md` обновлён на 12 tools. **Phase 3** (write tools под двойным confirm: unlockAgendaJob, disableAgendaJob) — только при 2-3 повторяющихся ручных операциях такого класса. + +--- + +## ✅ [books-analyst-mcp](books-analyst-mcp.md) — CLOSED 2026-05-01 (поздний вечер²) — целиком +**Status:** closed (Phase 1.1 на проде; Phase 1.3+1.4 отменены архитектурно; Phase 1.2 — на полке) +**Outcome:** `@books/ozon-mcp@0.0.5` — 11 read-only tools на проде (Phase 1.1, `ozon-seller-api-client@0.0.30`, multi-shop bookva/слово, smoke 11/11). **Phase 1.3 (`books-data-mcp`) отменён 2026-05-01 (поздний вечер²)**: brainstorming показал — MCP-stdio + ssh-tunnel + saved queries в `*.sql` лишний слой, LLM работает в `books/` напрямую и `packages/tools/` уже задаёт паттерн hybrid CLI+module (соседи `ozon-transactions-analysis.js`, `ozon-products-price-analysis.js`). Аналитика теперь пишется как обычные JS-файлы в `packages/tools/analyst/.js` по запросу, без отдельной инфраструктуры. План `docs/superpowers/plans/2026-04-30-books-data-mcp.md` помечен OBSOLETE первой строкой. Phase 1.4 (analyst-каталог) отменён вместе с 1.3 — отдельный каталог не нужен. Phase 1.2 (`yandex-market-mcp`) — план остаётся отдельной задачей, реактивация по запросу. Решение зафиксировано в [.wiki/concepts/analyst-tooling.md](../.wiki/concepts/analyst-tooling.md). + +--- + +## ✅ [analyst-report-formats](analyst-report-formats.md) — CLOSED 2026-05-03 — pluggable formatters для analyst tools +**Status:** closed +**Outcome:** Реализован pattern pluggable formatters для аналитических CLI-tools. **5 formatters:** console (stdout, console.table), json (stdout), md (GFM таблицы + chart placeholder), xlsx (ExcelJS, sheets per table, bold headers, auto-width), html (HTML table + vega-embed, CDN default + --standandalone). **Infrastructure:** `_lib/cli-format.js` (dispatcher с --format/--out/--standalone), `_lib/output-path.js` (timestamped paths в `tmp-designs/analyst-reports/`), `_test/` (11 unit тестов). **Wiki:** обновлены `analyst-tooling.md` (секция "Output formats"), новая страница `tools-analyst.md`, `index.md` + `log.md` entries. **Хук yarn.lock улучшен:** теперь проверяет dependencies/devDependencies изменения, не весь package.json (ложные срабатывания при scripts changes устранены). Push в master (commit b6007f8). Тестовые кейсы (staff-performance, listings-vs-orders, sales-seasonality) — отдельными задачами по мере возникновения. + +--- + +## 🟡 [scheduler-ui-improvements](scheduler-ui-improvements.md) — UX-идеи для /scheduler админки +**Status:** paused +**Where I stopped:** заведено 2026-04-28. Две идеи: (1) имя исполняемого модуля в логе (через stack-walk на каждую console-строку при `data.debug=true`); (2) кнопка «Copy all» на /scheduler/runs/[id] которая собирает status+params+log+result в plain-text для вставки Claude'у в чат. +**Next action:** реализуется при первом серьёзном дебаг-сценарии. Низкий приоритет. +**Blocker:** — +**Branch:** — + +--- + +## ⚪ [scheduler-core-replace](scheduler-core-replace.md) — заменить Agenda+Mongo на свой core поверх MariaDB +**Status:** idea (not started) +**Where I stopped:** 2026-04-28 концептуальное обсуждение pros/cons/подводных камней. Crash recovery был преувеличен мной как блокер — это `setInterval` + `UPDATE WHERE locked_at < NOW() - X` ~50 LOC. Реальные сложности: lock TTL trade-off (handler в task-runner отдельным процессом), idempotency на retry, catch-up policy. Middle-ground — тонкая обёртка `cron-parser` + MariaDB `scheduled_tasks` ~300 LOC. +**Next action:** не трогать. Триггеры пересмотра: `@agendajs/mongo-backend` ломается на npm-bump, нужны priority/dependencies/per-task concurrency, или желание снять Mongo-контейнер с VPS. +**Blocker:** soak Phase 7 pluggable-scheduler ещё свежий — преждевременно ломать стабильное. +**Branch:** — + +--- + +## ⚪ [tools-architecture-revision](tools-architecture-revision.md) — ревизия границ packages/tools vs packages/task-runner/tasks +**Status:** ready (defined, не приоритет) +**Where I stopped:** задача заведена 2026-04-27 при дизайне `productSalesChannel`-sync — старая конвенция «tools атомарны, не лезут в БД» отменена (выбран X: tool пишет в БД сам). Юзер: «подходы нужно будет подвергнуть ревизии». Накопить 2-3 sync-таска, потом ревизировать pattern. +**Next action:** дождаться 2-3 примеров реализации, потом brainstorm границ + обновить `.wiki/packages/tools.md` и `concepts/task-runner-handlers.md`. +**Blocker:** — +**Branch:** — + +--- + +## 🟡 [feedback-changes](feedback-changes.md) — фидбэк юзеров + изменения API маркетплейсов +**Status:** paused +**Where I stopped:** Все коммиты партии запушены (`46c20e3` upload polling, `6ae3a12` главная, `ebe1ebd` /jobs 500, `9d61850` web FBS-роуты, `ae9b6dd` иконки Pagination/SelectMenu). Юзер не приносил новой партии. +**Next action:** ждать фидбэка; в бэклоге — блок метрик дашборда + переименование API-роутов FBS (web уже переименован, API не тронут). +**Blocker:** — +**Branch:** `master` + +--- + +## 🟡 [pwa](pwa.md) — installable PWA для @books/web +**Status:** paused +**Where I stopped:** spec (`2a48230`) + impl (`b0a6820`) + task-wiki (`7a28f91`) запушены. На проде раскатано. +**Next action:** ручная проверка на устройствах — iPhone Safari (Add to Home Screen), Android Chrome (install prompt), Lighthouse PWA audit. По итогам — закрыть или завести follow-up. +**Blocker:** — +**Branch:** `master` + +--- + +## 🟡 [web-design-system](web-design-system.md) — единая дизайн-система @books/web +**Status:** paused +**Where I stopped:** Большая итерация UX/UI запушена (lucide bundle вместо CDN, мобильный navbar с hamburger, ProductSkuLink + useTabTarget, /fbs/orders как сетка карточек, USwitch вместо чекбокса в /fbs/summary, hover-open dropdown через UPopover, scrollbar-gutter:stable, spinner-overlay, реордер orders header, label «Канал продаж», SKU+title pattern на /epz/search). +**Next action:** ждать новой партии фидбэка по UX. +**Blocker:** — +**Branch:** `master` + +--- + +## 🟡 [business-logic-bugs](business-logic-bugs.md) — реестр багов бизнес-логики +**Status:** paused +**Where I stopped:** bug #2 (Ozon labels portrait, `09962bd`) и bug #3 (upload-history badge, `0aae243` + `4358806`) закрыты. bug #1 (`ozonFbsProduct` notNull, `2940123`) запушен — ждёт прод-логи task-runner на пропадание `notNull Violation`. +**Next action:** при возврате — проверить прод-логи task-runner после раскатки 4 локальных коммитов (заодно с bug #1 verification). Либо ждать новых багов. +**Blocker:** — +**Branch:** `master` + +--- + +## 🟡 [vps-migration](vps-migration.md) — переезд CentOS 7 → Debian 12 +**Status:** paused +**Where I stopped:** план в [concepts/vps-os-migration](../.wiki/concepts/vps-os-migration.md), оценить сроки. +**Next action:** при готовности — оценить сроки и blast radius. +**Blocker:** — +**Branch:** `master` + +--- + +## ✅ [registry-gc](registry-gc.md) — CLOSED 2026-04-30 (Gitea-side retention + VDS-side prune) +**Status:** closed +**Outcome:** Полный цикл retention в проде на двух фронтах. Gitea-side: `mode=min` build cache + `registryGc` cron `0 4 * * 0` MSK (keep last 3 versions per package) + Gitea built-in `[cron.cleanup_packages] OLDER_THAN=24h` (blob compaction). VDS-side: `vdsDockerCleanup` cron `0 5 * * *` MSK через `books-docker-proxy` (4 stage prune containers/dangling/old-images/buildkit-cache, volumes намеренно не трогаем). Notify в `books-alerts` через ntfy summary после каждого run. Phase 4 (disk-pressure alert) опциональный — открыть отдельной задачей при надобности. + +--- + +## 🟡 [ntfy](ntfy.md) — push-уведомления (Telegram заблокирован РКН) +**Status:** paused — Phase 1+2+4 на проде; Bug #1 fix запушен 30.04 поздний вечер², pending verify. +**Where I stopped:** 2026-04-30 поздний вечер². Bug #1 (Click-URL → 404 в `books-deploys`, чужой run в `books-builds`) — root cause: Gitea UI route `/repo/actions/runs/{run}` парсит `{run}` как `runIndex` (per-repo seq = `gitea.run_number`), а не `gitea.run_id`. Поправлены оба workflow ([build.yml](../.gitea/workflows/build.yml) env `RUN_URL`, [deploy.yml](../.gitea/workflows/deploy.yml) env `RUN_URL`). +**Next action:** push верификационный коммит `git commit --allow-empty -m "verify ntfy URL" && git push` → дождаться build+deploy → tap ссылки в обоих ntfy-каналах, должен открыться ИМЕННО smoke run сверху списка. После verify — закрыть task или начать Phase 3 (disk-alert). +**Blocker:** — +**Branch:** `master` + +--- + +## 🟡 [playbook](playbook.md) — оперативный playbook для инцидентов VPS / API +**Status:** paused (scope сужен — реюзабельные плейбуки выехали) +**Where I stopped:** scope-расщепление 2026-04-27: реюзабельные «как сделать» плейбуки (CI/CD, scheduler) живут в `docs/playbooks/` и закрыты. Эта задача теперь только про **incident playbooks** (что делать когда сломалось) — не начато. +**Next action:** структурировать сценарии (API down, registry full, certificate expiry), оформить в `docs/runbooks/` или `docs/playbooks/incidents/`. +**Blocker:** — +**Branch:** `master` + +## ✅ [switch-npm-mcp-to-local-stdio] — DONE 2026-04-30 — переключён `npm-mcp` в `.mcp.json` с docker на локальный node stdio. Контекст и обоснование — в общей вики: `concepts/local-stdio-mcp-deploy` (там же таблица альтернатив B/C/D на случай, если разделяемый конфиг пакетов не подойдёт для books). + +Текущий блок в `books/.mcp.json` (entry `npm-mcp`): +```json +"npm-mcp": { + "type": "stdio", + "command": "docker", + "args": [ + "run", "--rm", "-i", + "-v", "c:/Users/vitya/projects/npm-mcp/packages.config.json:/app/packages.config.json:ro", + "-v", "npm-mcp-cache:/data", + "registry.kzntsv.site/npm-mcp:latest" + ] +} +``` + +Заменить на: +```json +"npm-mcp": { + "type": "stdio", + "command": "node", + "args": ["C:/Users/vitya/projects/npm-mcp/build/index.js"] +} +``` + +Прочие записи (`context7`, `books-ops`) **не трогать**. Путь к `build/index.js` указан под Windows-машину; на другой машине, если books открывается там — подставить локальный путь к клону `npm-mcp` (или замаппить через user-scope `claude mcp add --scope local`, вариант B из общей вики, чтобы не коммитить машинно-специфичный путь). + +Решение принято в сессии npm-mcp 2026-04-30: docker-путь периодически даёт «MCP unavailable» (daemon, volume mounts на Windows), а сервер уже скомпилирован локально и реестр доступен с хоста — Docker не даёт ничего, кроме лишней точки отказа. + +**Status:** done 2026-04-30 +**Outcome:** В `books/.mcp.json` entry `npm-mcp` переключён с `docker run` на `node C:/Users/vitya/projects/npm-mcp/build/index.js`. Прочие записи (`context7`, `books-ops`) не трогали. Юзеру: после следующего рестарта Claude Code в books — проверить что `mcp__npm-mcp__*` инструменты доступны и `search_docs` отвечает. Если открываем books на другой машине — подставить локальный путь к клону `npm-mcp` (или вариант B из общей вики `concepts/local-stdio-mcp-deploy` — user-scope mcp add, чтобы не коммитить машинно-специфичный путь). +**Branch:** `master` + + +--- + +## ⚪ [refresh-project-bootstrap] — Run the `project-bootstrap` skill in this repo to refresh its layout to the canonical state (git, .gitignore, README.md, .wiki/, .tasks/, CLAUDE.md). The skill handles both greenfield bootstrap and refresh of existing repos. + +**Status:** ready +**Where I stopped:** (not started) +**Next action:** Invoke the `project-bootstrap` skill at the repo root, confirm each file write, push to main. +**Branch:** n/a + + +--- + +## 🟢 [bootstrap-upgrade-pilot] — CLOSED 2026-05-07 — `project-bootstrap` v1.10.0 upgrade pilot успешен +**Status:** done +**Outcome:** Bootstrap v1.10.0 идемпотентно добавил 3 новых триггера в `CLAUDE.md`: `follow tdd-criteria`, `delegate to interns when allowed`, `recommend, don't menu`. Существующие строки нетронуты, пользовательские комментарии (первые 6 строк) сохранены. **Идемпотентность подтверждена** — повторный прогон находит 0 missing triggers. Skill-deps check (Step 5.6) корректно валидирует наличие всех 11 fulfiller'ов на хосте. Commit `5ca16d9`. Массовая раскатка на остальные проекты — следуя приоритету: `_meta` (координатор) → `claude-skills` → `npm-mcp` → `books` (уже done) → остальные. + + + +--- + +## 🟢 [domain-promotions-pointers] — CLOSED 2026-05-12 — Bootstrap-pointers для design `domain-promotions`. Секция "Domain conventions" добавлена в `.wiki/CLAUDE.md`. +**Status:** done +**Outcome:** В `.wiki/CLAUDE.md` добавлена секция "Domain conventions" с блоком "Mandatory: read design context before implementation". Три источника в порядке: (1) global wiki `concepts/domain-promotions` через `mcp__projects-meta__knowledge_get`, (2) brainstorm trace `~/projects/.workshop/.archive/2026-05-12-books-domain-promotions.md`, (3) локальный `overview.md` для быстрой ориентации. Следующие агенты будут знать где лежит полный design context а не угадывать из `where_stopped` one-liner'ов. + + + +--- + +## 🟢 [domain-promotions-schema] — CLOSED 2026-05-13 — Миграция + Sequelize-модели для доменных акций-скидок +**Status:** closed +**Outcome:** Migration 016 (`b732360`): три `CREATE TABLE IF NOT EXISTS` — promotions, promotion_products, promotion_price_snapshots. Event-based таймстампы в promotion_products (addedAt/appliedAt/restoredAt/removedAt/failedAt). Индексы: promotions(status, start_at), promotion_products(id_promotion, removed_at), promotion_price_snapshots(id_promotion, id_product) UNIQUE. FK: promotion→seller (CASCADE), product→promotion (CASCADE), snapshot→promotion+product (CASCADE). 3 sequelize-модели с camelCase/snake_case field mapping. `[skip-tdd: oneshot] [skip-tdd: wrapper]` — migration (Q7) + model declarations ≤10 significant lines (Q8). Smoke idempotency — pending prod apply. + + + +--- + +## ✅ [domain-promotions-lib](domain-promotions-lib.md) — CLOSED 2026-05-13 — `f61c679` — 4 pure-logic модуля + 24/24 unit-теста green +**Status:** closed +**Outcome:** `resolve-cheapest-winner.js` (pure, 5 тестов), `snapshot-source.js` (thunk-injected, 4 теста), `defensive-restore.js` (tolerance-based, 7 тестов), `apply-price-to-ozon.js` (chunked+retry, 8 тестов). Pure ядро изолировано от I/O через thunk-инжекцию. CLI-таски импортируют через `import { resolveCheapestWinner } from './_lib/promotions/resolve-cheapest-winner.js'`. Разблокирует 5 CLI-таск. + + + +--- + +## ✅ [domain-promotions-create-cli](domain-promotions-create-cli.md) — CLOSED 2026-05-13 — `35a0308` — validation TDD 8/8 green + Commander wrapper +**Status:** closed +**Outcome:** `promotion-create.js` с гибридным export+CLI. Валидация: endAt>startAt (2 теста), discountPercent 0-100 (2 теста), ISO8601 даты (2 теста), seller exists (1 тест), успешное создание (1 тест). Commander обёртка `[skip-tdd: wrapper]`. + + + +--- + +## ✅ [domain-promotions-add-products-cli](domain-promotions-add-products-cli.md) — CLOSED 2026-05-13 — `2573454` — inline apply + idempotency 14/14 green +**Status:** closed +**Outcome:** `promotion-add-products.js`. Scheduled: товары добавляются с addedAt без appliedAt. Active: inline snapshot+resolve+apply через lib модули. Идемпотентность: повторный вызов skip'ает уже добавленные, воскрешает removed. Commander обёртка `[skip-tdd: wrapper]`. + + + +--- + +## ✅ [domain-promotions-remove-products-cli](domain-promotions-remove-products-cli.md) — CLOSED 2026-05-13 — `9d4f2f4` — soft remove + defensive restore 7/7 green +**Status:** closed +**Outcome:** `promotion-remove-products.js`. Soft remove: запись остаётся с removedAt, без DELETE. Active+applied: inline defensive restore с cheapest-wins. Divergence не валит операцию. Commander обёртка `[skip-tdd: wrapper]`. + + + +--- + +## ✅ [domain-promotions-activate-cli](domain-promotions-activate-cli.md) — CLOSED 2026-05-13 — `f68ff22` — scheduled→active + snapshot+apply 8/8 green +**Status:** closed +**Outcome:** `promotion-activate.js`. Status guard: только scheduled→active. На каждый товар: snapshot source → resolve cheapest-winner → apply price. Поддержка actionPriceOverride. API ошибка на одном товаре не валит остальных. Commander обёртка `[skip-tdd: wrapper]`. + + + +--- + +## ✅ [domain-promotions-deactivate-cli](domain-promotions-deactivate-cli.md) — CLOSED 2026-05-13 — `31fde95` — active→ended + defensive restore 7/7 green +**Status:** closed +**Outcome:** `promotion-deactivate.js`. Status guard: active→ended, не ended/cancelled. Каждый applied товар: resolve cheapest-winner (self-excluding) → defensive restore (push/diverged). Pending товары: soft mark removedAt. Commander обёртка `[skip-tdd: wrapper]`. + + + +--- + +## 🟢 [domain-promotions-review] — Code-review checkpoint для брейнсторма domain-promotions (промоушен 2026-05-12). + +**Спецификация:** `concepts/domain-promotions` в global wiki (читать через `mcp__projects-meta__knowledge_get` slug=`concepts/domain-promotions`). +**Pre-impl bootstrap:** `domain-promotions-pointers` (заполнил `.wiki/CLAUDE.md` Domain conventions — без него review бы читал stub). +**Импл-таски (review против их acceptance criteria):** `domain-promotions-schema`, `domain-promotions-lib`, `domain-promotions-create-cli`, `domain-promotions-add-products-cli`, `domain-promotions-remove-products-cli`, `domain-promotions-activate-cli`, `domain-promotions-deactivate-cli`. + +**Кто делает:** **не имплементер.** Следующая сессия в этом проекте (другая модель / другой день / другой агент) поднимает таску с чистым контекстом. «Я только что это написал» bias = главный риск. + +**Чек-лист ревью:** +- Прочитать спецификацию (acceptance criteria каждой импл-таски). +- `git log --oneline` shipped-коммитов (по slug или scope в commit-message). +- Для каждой импл-таски: прогнать соответствующие тесты, реально проверить что они доходят до своих веток (не coverage-illusion). +- Сверить дизайн-decisions со shipped-кодом (signature, params, error-paths, безопасность). +- Особое внимание: cheapest-wins overlap (сценарии `A→B→A end→B end` и `A→B→B end→A end`), defensive-restore divergence detect, soft-remove семантика (`removedAt` без DELETE). +- Findings — отдельные follow-up tasks (`domain-promotions--fix` или подобное) через `tasks_create`. + +**Закрытие:** только когда все findings зафайлены ИЛИ ревьюер подтвердил «нет findings» в close-note. + +**Status:** done +**Where I stopped:** Review done 2026-05-13. 7 findings filed (3 critical, 3 medium, 1 low) → 4 follow-up tasks created: fix-snapshot-persistence, fix-status-timing, fix-removed-filter, fix-divergence-persist. 68/68 tests green but coverage illusion — production paths (snapshot write, Ozon API calls) not covered. +**Next action:** (none — kept until merged) +**Blocker:** bootstrap: domain-promotions-pointers; impl-tasks: domain-promotions-schema, domain-promotions-lib, domain-promotions-create-cli, domain-promotions-add-products-cli, domain-promotions-remove-products-cli, domain-promotions-activate-cli, domain-promotions-deactivate-cli +**Branch:** n/a + + + +--- + +## 🟢 [domain-promotions-fix-snapshot-persistence] — Fix CRITICAL F1+F2+F3 from review: (1) idPromotionProduct→idPromotion+idProduct in snapshot lookup, (2) write PromotionPriceSnapshot.create() in activate+add-products inline, (3) pass product_id to defensiveRestore() + +**Status:** done +**Where I stopped:** Fixed F1 (idPromotion+idProduct lookup instead of idPromotionProduct), F2 (PromotionPriceSnapshot.create in activate+add-products), F3 (product_id passed to defensiveRestore). 5 tests added (activate F2, deactivate F1+F3, add-products F2, remove-products F1+F3). 80/80 green. +**Next action:** (none — kept until merged) +**Branch:** n/a + + + +--- + +## 🟢 [domain-promotions-fix-status-timing] — Fix MEDIUM F4 from review: move promo.status change AFTER processing products in activate+deactivate, not before + +**Status:** done +**Where I stopped:** Fixed F4: status change moved after product processing loop with `if (failed === 0)` guard in both activate and deactivate. 2 tests added. 80/80 green. +**Next action:** (none — kept until merged) +**Branch:** n/a + + + +--- + +## 🟢 [domain-promotions-fix-removed-filter] — Fix MEDIUM F5 from review: activatePromotion must filter removedAt IS NULL, per spec "Snapshot+apply всех removedAt IS NULL" + +**Status:** done +**Where I stopped:** Fixed F5: added `removedAt: null` filter to PromotionProduct.findAll in activatePromotion. 1 test added verifying where clause. 80/80 green. +**Next action:** (none — kept until merged) +**Branch:** n/a + + + +--- + +## 🟢 [domain-promotions-fix-divergence-persist] — Fix LOW F6+F7 from review: (1) write divergedAt/divergedFrom/divergedTo to PromotionPriceSnapshot on divergence, (2) add integration tests covering production paths (snapshot write→find→restore→diverge) + +**Status:** done +**Where I stopped:** Fixed F6+F7: divergence fields (divergedAt/divergedFrom/divergedTo) written to snapshot on divergence in both deactivate and remove-products. Integration test (F7) added covering full snapshot chain. 3 tests added. 80/80 green. +**Next action:** (none — kept until merged) +**Branch:** n/a + + + +--- + +## 🟢 [analyst-workflow-claude-md] — DONE 2026-05-15 — секция Analyst workflow в корневом CLAUDE.md + +**Status:** done +**Where I stopped:** Закоммичено `44d97b0`. Секция 39 строк после `we're on Windows`: триггер-фразы (выручка / средний чек / сезонность / staff perf / X by month / export / one-off), 3 шага (load `.wiki/concepts/` → check `packages/tools/{README,analyst/}` → write new), inline-блок стиля 11 пунктов (ESM, hybrid CLI+module, commander+isDirectRun, JSDoc, bin+exports, ozon/yandex client напрямую, sequelize+raw SQL, camel/snake mapping, `{meta, tables}` return shape, форматтеры `_lib/`, `.cmd`-обёртки), read-каналы (wiki/README/plans/sequelize), write-канал, anti-patterns (6 пунктов). +**Next action:** (none — kept until merged) +**Branch:** master (local, not pushed) + + + +--- + +## 🟢 [tools-ci-path-filter] — DONE 2026-05-15 (частично, спека ложна) — skip api/sched/tr build на CLI-only-tools diff + +**Status:** done (narrowed scope) +**Where I stopped:** Закоммичено `102a92e` в [.gitea/workflows/build.yml](../.gitea/workflows/build.yml). Спека «исключить весь packages/tools» — ложная: `@books/tools/*` импортится в `packages/task-runner/tasks/*.js` (13+ файлов: fbs-ship-*, sync-prices-*, load-products-to-ozon, awaiting-packaging-*, sync-product-sales-channel-*, etc.) и в `packages/api/server/routes/fbs/*` (4 файла). Выпиливание `tools/` из `api_pat`/`sched_pat`/`tr_pat` целиком сломало бы prod runtime. Сужено до `tools_skip_pat='^packages/tools/(analyst|plans|not-for-qwen|output)/|^packages/tools/(README|QWEN)\.md$|^packages/tools/[^/]+\.cmd$'` — эти подпути проверены grep'ом на 0 импортов из runtime. Top-level `packages/tools/*.js` НЕ исключаем (там смесь runtime-imported и CLI-only). Test/lint job для tools — в `.gitea/workflows/*` отсутствуют вообще, спека приняла желаемое за сущее. +**Next action:** (none — kept until merged). Follow-up для расширения win: move analyst-CLI с top-level (ozon-revenue-per-product, ozon-transactions-analysis, ozon-products-price-analysis, archive-product, unarchive-product, products-import-by-sku, get-products-info-by-sku, update-drugs-blocked-products, ozon-promotion-*) в `analyst/` subdir. +**Branch:** master (local, not pushed) + + + +--- + +## 🔵 [analyst-workflow-review] — Review-checkpoint для брейнсторма books-analyst-workflow (промоушен 2026-05-14). Спецификация: workshop archive 2026-05-14-books-analyst-workflow.md. Impl-таски: analyst-workflow-claude-md, tools-ci-path-filter. Кто делает: НЕ имплементер. Следующая сессия с чистым контекстом (другая модель, другой день, другой агент) — бороться с bias я только что это написал. Чек-лист: 1) прочитать спецификацию из workshop archive; 2) проверить books CLAUDE.md секция Analyst workflow добавлена и читается на чистый старт сессии без двусмысленностей; 3) реально прогнать 2-3 триггер-фразы в свежей сессии (посчитай выручку, сезонность, отчёт) и убедиться что workflow корректно срабатывает: я читаю packages tools README/analyst до того как писать новый; 4) проверить CI: коммит strictly в packages tools не запускает deploy-job; обратное (коммит в packages api/data) триггерит как раньше; 5) finding ones — отдельные follow-up tasks через tasks_create. Закрытие: только когда все findings зафайлены или ревьюер подтвердил нет findings. + +**Status:** blocked +**Where I stopped:** (not started) +**Next action:** Дождаться зелёных blocker-тасок (analyst-workflow-claude-md и tools-ci-path-filter). Затем прогнать чек-лист (см. description). Findings — отдельные follow-up tasks через tasks_create. +**Blocker:** impl-tasks: analyst-workflow-claude-md, tools-ci-path-filter +**Branch:** n/a + + +--- diff --git a/tests/fixtures/real-claude-skills-STATUS.md b/tests/fixtures/real-claude-skills-STATUS.md new file mode 100644 index 0000000..8fb8abf --- /dev/null +++ b/tests/fixtures/real-claude-skills-STATUS.md @@ -0,0 +1,1339 @@ +# Task Board +_Updated: 2026-05-22 (interns-grep-audit-skill-updates closed 🟢 — using-interns v0.2.2→0.3.0 with 3 routing rows for `grep_audit` deterministic intern plus consistency adds; unblocks half of `[interns-grep-audit-review]`.)_ + + + +## 🟢 [using-yt-tools-skill-body-venv-invocation] — SKILL.md не документирует venv-activation / full-path CLI invocation; concurrent сессия после `/reload-plugins` упёрлась в `command not found` на bare `yt-frames`. + +**Observed (2026-05-20):** в concurrent CC-сессии (после `/reload-plugins` + Skill(using-yt-tools)) агент попытался Flow B напрямую — `yt-frames URL --timestamps ...` через Bash. Результат: `bash: yt-frames: command not found`. Далее `Get-Command yt-frames` пусто. Агент попытался `python -m pip install -e ~/projects/.common/lib/yt-tools/` в **системный** Python — wrong path (ломает global env, дублирует уже-настроенный venv в `.common/lib/yt-tools/.venv/`). + +**Корень:** `pip install -e` в taзе `[yt-tools-impl]` положил entry-point'ы (`yt-transcript`, `yt-frames`, `yt-watch`, `yt-tools`) в `.venv/Scripts/` (Windows) / `.venv/bin/` (Linux/macOS), а не на user PATH. Без активации venv агент бинарь по имени не находит. SKILL.md body (Prereq / Inputs / Steps) этого требования не упоминает — обещает «вызови `yt-frames URL --timestamps T1,T2`», как будто на PATH. Имплементер в своей сессии случайно работал в активированном venv shell'е и проблему не увидел. + +**Acceptance:** +- SKILL.md Prereq явный block (≥1 из): + - (a) активация venv: `& ~/projects/.common/lib/yt-tools/.venv/Scripts/Activate.ps1` (Windows PS) / `source ~/projects/.common/lib/yt-tools/.venv/bin/activate` (Linux/macOS), + - (b) full-path invocation: `& ~/projects/.common/lib/yt-tools/.venv/Scripts/yt-frames.exe URL ...`, + - (c) обе альтернативы с trade-off-комментом. +- Steps-примеры (Flow A + Flow B) обновлены: либо активация в первом шаге, либо full-path в каждом вызове. Не оставлять bare `yt-frames` в Steps. +- bump `version: 0.2.2` → `0.2.3` (PATCH — docs-fix без поведенческих изменений). +- `scripts/install.ps1 -Names using-yt-tools` пройден, новая версия в `~/.claude/skills/using-yt-tools/`. + +**Status:** done (2026-05-20) +**Where I stopped:** done +**Closed by:** commit `971bcd9` — SKILL.md Prereq теперь содержит explicit table с двумя вариантами (activate venv vs full-path), Flow A и Flow B имеют шаг 0 (активация venv), version bump 0.2.2→0.2.3, reinstall пройден. +**Branch:** master + + +--- + +## 🟢 [using-yt-tools-windows-powershell-path-doc-fix] — README warn про restart-shell-after-winget покрывает только git-bash subshell CC, та же проблема симметрично у PowerShell-subshell. + +**Observed (2026-05-20):** в concurrent CC-сессии PowerShell-subshell не видел ffmpeg (`where.exe ffmpeg` → `INFO: Could not find files`), хотя ffmpeg уже установлен через `winget install Gyan.FFmpeg` на user PATH. Симметричная проблема: env унаследован от родительского shell процесса CC, до его рестарта новый user-PATH не подтягивается ни в bash-subshell, ни в PowerShell-subshell. + +**Корень:** `[using-yt-tools-windows-path-doc-fix]` (закрыта 2026-05-20, README warn добавлен про bash-subshell) формулировка пропустила PowerShell-кейс. Subshell-семантика Windows process-env одинакова для обоих shell flavors — нужно покрыть оба. + +**Acceptance:** +- README пакета `~/projects/.common/lib/yt-tools/README.md` warn-block расширен: явная пометка «restart CC требуется для **обоих** shell flavors внутри сессии — git-bash subshell И PowerShell subshell — потому что process-env унаследован от родительского CC-процесса». +- bump `~/projects/.common/lib/yt-tools/pyproject.toml` version `0.1.5` → `0.1.6` (PATCH — docs-only). +- commit в `.common`. + +**Status:** done (2026-05-20) +**Where I stopped:** done +**Closed by:** commit `ec9784d` в `.common` — README.md warn-block теперь explicitly покрывает **оба** shell flavors (git-bash И PowerShell), pyproject.toml bump 0.1.5→0.1.6. +**Branch:** master + + +--- + +## 🟢 [using-yt-tools-install] — Install env deps (ffmpeg + python venv + 3 pip libs) for using-yt-tools on agent machine. + +**Этапы:** +1. **Сейчас** — system-level: `ffmpeg` через OS-пакетник (`winget install Gyan.FFmpeg` / `brew install ffmpeg` / `apt install ffmpeg`); python venv в `~/projects/.common/lib/yt-tools/.venv`; `pip install youtube-transcript-api pyscenedetect yt-dlp`. +2. **После реализации `OpeItcLoc03/.common` task** (ручной post-promotion task на импл python-пакета `lib/yt-tools/`) — `pip install -e ~/projects/.common/lib/yt-tools/` в venv; smoke `yt-transcript --help`, `yt-frames --help`, `yt-watch --help`. + +Cross-platform: повторить на каждой агент-машине (Windows / Linux / macOS). README пакета должен документировать команды под все три ОС. + +**Источник дизайна:** `.workshop/.archive/2026-05-20-yt-tools.md` (разделы «Зависимости» и «Упаковка»). + +**Status:** done (2026-05-20, vitya@DESKTOP-NSEF0UK / Windows) +**Where I stopped:** done — both stages green +**Closed by:** ffmpeg 8.1.1 (Gyan.FFmpeg) on user PATH; yt-dlp 2026.3.17; venv at `~/projects/.common/lib/yt-tools/.venv` with youtube-transcript-api 1.2.4, yt-dlp 2026.3.17, scenedetect[opencv] 0.7, opencv-python 4.13.0.92; `pip install -e` of yt-tools (v0.1.0) already in place; all 4 CLI entry-points (`yt-transcript/yt-frames/yt-watch/yt-tools`) print help; 67/67 unit tests pass (`python -m pytest tests/ -q`). +**Branch:** n/a + + + +--- + +## 🟢 [using-yt-tools-hermes-mapping] — Register `using-yt-tools` in `~/projects/claude-skills/hermes/mapping.yaml` (mode: pending). + +Mode: **`pending`** — скил вызывает внешние CLI (yt-dlp/ffmpeg) и пишет файлы в `./yt-cache/`. Требует ручного аудита перед переходом на `auto`. + +**Триггеры** (русские/английские) — из SKILL.md frontmatter description: +- ru: «транскрипт видео», «расшифровка YouTube», «что в этом ролике», «о чём ролик», «покажи кадр на N», «посмотри момент N», «что показано в видео на N» +- en: «watch this video», «video summary», «youtube transcript» +- URL-pattern: `youtube\.com|youtu\.be` (подавление WebFetch — он не отдаёт ни транскрипт, ни кадры на YouTube) + +**Источник дизайна:** `.workshop/.archive/2026-05-20-yt-tools.md` (раздел «Triggers для skill»). + +**Status:** done (2026-05-20) +**Where I stopped:** done — entry added, build green +**Closed by:** `hermes/mapping.yaml` got a new pending entry `using-yt-tools` with `intended: { mode: auto, category: research }` and audit reason. `python scripts/build-hermes.py` outputs 26 skills (14 auto / 2 manual / 9 skip / 1 pending); SKIPPED.md lists the entry under Pending with the intended block. NB: triggers/url_pattern from the task sketch don't fit the mapping schema (build script reads only mode/category/reason/intended/replace-rules); triggers live in SKILL.md frontmatter description and are picked up automatically by Hermes. Promotion to `auto` after `using-yt-tools-test-trigger` 🟢. +**Branch:** n/a + + + +--- + +## 🟢 [using-yt-tools-test-trigger] — Behavioral smoke-test using-yt-tools on a clean session + e2e CLI pipeline on a test YouTube URL. + +**Что проверяется:** + +1. **Активация на своих триггерах** — каждая из 10 фраз description'а активирует скил на чистой сессии. +2. **False-positive check** — НЕ активируется на 2-3 близких чужих: + - «скачай это видео» (только yt-dlp, не watching) + - «расшифруй подкаст» (audio-only — нет STT в скиле) + - «что в этой лекции на Vimeo» (не YouTube) +3. **CLI pipeline e2e** (после `using-yt-tools-install` Этап 2): + - `yt-transcript ` → markdown с metadata header + параграфами + `[mm:ss]`-якорями (copy-paste-friendly в формат `--timestamps`) + - `yt-frames --timestamps 1:00,2:30` → 2 jpg в `./yt-cache//frames/` + - **Iterative-флоу (primary use-case):** транскрипт → агент выбирает таймкоды по содержанию → `yt-frames --timestamps` → `Read frame_*.jpg` → vision-anchored ответ + +**Источник дизайна:** `.workshop/.archive/2026-05-20-yt-tools.md` (разделы «Primary use-case — iterative agent-driven viewing» и «Triggers для skill»). + +**Status:** done (2026-05-20, partial — see scope split) +**Where I stopped:** CLI e2e + iterative-флоу пройдены; trigger smoke (части 1+2) вынесена в отдельную таску под чистую сессию. +**Closed by:** Тестовый URL — 3blue1brown «Vectors, Chapter 1, Essence of linear algebra» (`https://www.youtube.com/watch?v=fNk_zzaMoSs`, 9:51, EN community subs, плотный визуал). Результаты: +- `yt-transcript` отработал, выдал md с header (title/channel/duration/lang/url) и body. ❌ **Paragraph segmentation сломан**: весь 9:51-минутный ролик в **одном блоке** с единственным `[0:00]`-якорем — iterative-флоу primary use-case рушится (агенту нечем выбирать таймкоды по содержанию). Корень: `_group_paragraphs` группирует по >4s gap, а у профессиональных/community-submitted субтитров snippet'ы текут без пауз. → finding [using-yt-tools-transcript-paragraphs-fix]. +- `yt-frames --timestamps 0:30,3:00,6:00` отработал: source.mp4 закешировался в `./yt-cache//`, ffmpeg-seek выдал 3 валидных jpg в `frames/`. ❌ **Stderr swallow**: первый вызов (без ffmpeg на PATH) упал с `error: yt-dlp source download failed:` — после двоеточия пусто, реальная причина (отсутствие ffmpeg, нужного yt-dlp как mux-dep) скрыта. → finding [using-yt-tools-frames-stderr-fix]. +- **Iterative-флоу:** Read трёх кадров → vision-anchored синтез ОК. frame_0030 — 3 pi-человечка «Physics/Mathematician/CS student» (совпадает с интро «three perspectives»); frame_0300 — пустые xy-оси (артефакт slепого выбора без paragraph-якорей, не баг тула); frame_0600 — числовая прямая «2+5» с жёлтой и розовой стрелками (совпадает с местом про vector addition как «step-then-step»). End-to-end pipeline валиден когда таймкоды известны. +- ⚠️ Windows-gotcha: ffmpeg в user-PATH не подхватывается git-bash subshell'ом CC до рестарта сессии. README уже документирует winget install, но не упоминает restart-shell-after. → finding [using-yt-tools-windows-path-doc-fix]. +- ⏸️ **Части 1+2 (trigger smoke + false-positive)** не выполнены в этой сессии — требуют чистого CC-инстанса. → вынесено как [using-yt-tools-trigger-smoke-clean-session]. +**Branch:** n/a + + + +--- + +## 🟢 [using-yt-tools-transcript-paragraphs-fix] — Fix paragraph segmentation in yt-transcript — 4s-gap heuristic collapses densely-captioned videos into one block. + +**Status:** done +**Where I stopped:** Closed by `a0e4cc8` (yt-tools 0.1.1). Гибрид #4 в `_group_paragraphs`: gap > 4s OR elapsed > 45s OR sentence-end (`.?!…`) после ≥15s от начала параграфа. E2E на `fNk_zzaMoSs` (9:51): **31 anchor** at ~15-25s intervals (был 1). 70→74 unit-тестов 🟢 (3 новых в `test_markdown.py`: dense-snippets, sentence-end-after-min, sentence-end-no-split-before-min). Параметры `max_paragraph_seconds` / `sentence_split_seconds` пробрасываются через `snippets_to_markdown` для тюнинга. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [using-yt-tools-frames-stderr-fix] — yt-frames swallows yt-dlp/ffmpeg stderr, hiding real failure cause. + +**Status:** done +**Where I stopped:** Closed by `6961796` (yt-tools 0.1.2). Pre-check `ffmpeg` в `_ensure_source_mp4` (yt-dlp требует его для mux) → missing-ffmpeg теперь выдаёт `"ffmpeg not found on PATH"` до запуска yt-dlp. Helper `_format_subprocess_failure(proc, label)` собирает `exit N | stderr/stdout tail | no output hint`, применён в обоих сайтах failure (`_ensure_source_mp4`, `_ffmpeg_extract_from_file`, `_ffmpeg_extract_streaming`). `--no-warnings` дропнут на yt-dlp invocations чтобы warnings попадали в captured output. 4 новых теста в `tests/test_frames.py` (missing ffmpeg / stderr surfaced / stdout-only surfaced / empty-empty hint). 74/74 🟢. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [using-yt-tools-windows-path-doc-fix] — README install section should warn about restart-shell-after-winget on Windows. + +**Status:** done +**Where I stopped:** Closed by `2471228` (yt-tools 0.1.3). README Install section получил `> **Windows note.**` блок: winget пишет в per-user PATH, текущий shell (вкл. git-bash subshell внутри CC) его не перечитывает; canonical fix = restart terminal + Claude Code session; verify via `where.exe ffmpeg`. In-session workaround упомянут (lookup путь через `winget show ` → `$env:Path +=`). Заодно sync test count 67 → 74 (drift от 0.1.1 + 0.1.2 additions). +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [using-yt-tools-skill-body-fill] — Fill `skills/using-yt-tools/SKILL.md` body (When/Prereq/Inputs/Steps/Failure/Side/NOT) with both flows documented. + +**Why:** Skill промоутился v0.1.0 со stub-body (`<пусто>` в каждой секции) + description описывал только iterative-flow, хотя триггеры включали targeted-frames фразы («покажи кадр на N» / «посмотри момент N»). Это создавало dissonance: агент мог активироваться на targeted-frames запрос, но prescriptive body не существовал, а description предписывал избыточный transcript fetch first. Также блокировало `[using-yt-tools-review]` acceptance criteria по Steps / Failure modes / What NOT to do. + +**Scope:** +- Description rewrite: документировать оба flow (iterative + targeted-frames) с разделёнными trigger phrase lists. Уложиться ≤900 chars (memory: hard limit ~1024). +- Body: When/Prereq/Inputs/Steps/Failure/Side/NOT — два flow явно, с командами и failure-table. +- Bump `version: 0.1.0` → `0.2.0` (MINOR — new capability документирован: targeted-frames flow). +- Install в `~/.claude/skills/using-yt-tools/` через `scripts/install.ps1 -Names using-yt-tools`. + +**Override caveat:** review-task originally specified «body пишется не имплементером (другая сессия / другой агент)». В этой сессии override дан юзером явно — body написан имплементером. Это ослабляет «fresh-eyes» гарантию body-quality, но review-pass на чистой сессии (по trigger-smoke-clean-session) даст symmetric coverage всё равно. + +**Status:** done +**Where I stopped:** Closed by `4956beb`. Description 744 chars (под 900-budget). Body: 6 sections, Flow A + Flow B explicitly distinguished. Trigger smoke в harness skill-listing — description полностью отображается (не truncated к H1). После body-fill — `[using-yt-tools-review]` остаётся 🔵 только на trigger-smoke-clean-session (один из двух исходных blockers снят). +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [using-yt-tools-trigger-smoke-clean-session] — Behavioral trigger smoke for using-yt-tools — requires a fresh CC session. + +**Scope (вынесено из using-yt-tools-test-trigger):** + +1. На **чистой** CC-сессии (без yt-tools context'а в prompt history) прогнать 10 триггер-фраз из `SKILL.md` description — каждая должна активировать `using-yt-tools`: + - ru: «транскрипт видео», «расшифровка YouTube», «что в этом ролике», «о чём ролик», «покажи кадр на N», «посмотри момент N», «что показано в видео на N» + - en: «watch this video», «video summary», «youtube transcript» +2. Прогнать 3 false-positive: «скачай это видео», «расшифруй подкаст», «что в этой лекции на Vimeo» — НЕ активируют скил. + +**Почему вынесено:** trigger-resolution в Claude Code зависит от чистоты session-history. Текущая сессия после имплементации install+hermes+e2e полностью загружена контекстом yt-tools — false-positive проверка теряет смысл. + +**Acceptance:** 10/10 positive, 0/3 false-positive. Findings (если есть) — обновить SKILL.md description. + +**Status:** done (2026-05-20) +**Where I stopped:** 13/13 expected outcomes met. P1-P10 (positive): all activate. N1-N3 (false-positive): all not-activate. Honest-first-impulse protocol followed (no real CLI runs). Findings: 0 follow-up fix-tasks; 2 design notes (description «Skip for ...» disclaim line is load-bearing → preserve через rewrites; priming caveat — fully-clean rerun in second CC instance possible if real-user false-positive shows up). +**Closed by:** `13ee8d3` — acceptance met (10/10 positive activate, 0/3 false-positive activate). See per-task file `.tasks/using-yt-tools-trigger-smoke-clean-session.md` for full table + reasons + findings. +**Branch:** n/a + + +--- + +## 🟢 [using-yt-tools-review] — Skill-review checkpoint для using-yt-tools (промоушен 2026-05-20). + +**Источник дизайна:** `.workshop/.archive/2026-05-20-yt-tools.md` (process trace: GitHub research + iterative-сценарий + 4 user-utверждённых default'а). +**Импл-таски:** using-yt-tools-install, using-yt-tools-hermes-mapping, using-yt-tools-test-trigger. +**Также блокируется** таской `[yt-tools-impl]` ⚪ ready в `OpeItcLoc03/.common` (создана 2026-05-20, commit `7779f8e`). + +**Кто делает:** **не имплементер.** Другая сессия / другой день / другой агент. Identity-not-location: ревьюер работает в любой папке, где есть доступ к файлам. + +**Поведенческий smoke-test (это и есть acceptance):** +- Скил активируется в чистой сессии на каждой триггер-фразе из `description` (русский И английский — список в test-trigger таске). +- Скил **не** активируется на 2-3 близких но не своих фразах (false-positive list в test-trigger таске). +- Каждый шаг секции `Steps` отрабатывает на тестовом буфере без ошибок (NB: после второго прохода body — body сейчас пустой stub). +- `Failure modes` уводят в abort, не в частичный успех с грязным состоянием. +- `What NOT to do` соответствует реальности — нет дыры между правилом и реализацией. + +**Iterative-флоу (key acceptance):** transcript → агент выбирает таймкоды → frames → Read → vision-anchored ответ работает end-to-end на тестовом YouTube URL. + +Findings → follow-up tasks (`using-yt-tools--fix`) через `tasks_create` в `claude-skills` (или локально пока Gitea лежит). + +**Закрытие:** только когда все findings зафайлены ИЛИ ревьюер подтвердил «нет findings» в close-note. + +**NB по семверу:** `version: 0.1.0` записан промоутером. Дальнейшие инкременты — ответственность владельца `claude-skills/`, **не** этого скила и не ревьюера. + +**Status:** done +**Where I stopped:** Closed by fresh-eyes review 2026-05-20 (subagent reviewer, no impl-priming, identity-not-location satisfied). Acceptance: **Flow A PASS** — URL `OmJ-4B-mS-Y` (Domain of Science — Map of Mathematics, 11:06, en captions); transcript header + paragraph segmentation OK; 3 frames at 4:23/4:41/8:16 visually consistent with chosen paragraphs. **Flow B PASS** — URL `gCfzeONu3Mo` (TED-Ed — How miscommunication happens), 1 frame at 2:00, no transcript dependency. **Failure modes PASS** — broken URL exit 1 clean; `--lang zz` proxy for no-captions exit 1 with helpful available-langs hint; non-YouTube URL `cannot extract video id`. **What NOT to do PASS** — rules are agent-side policy; CLI doesn't contradict; `--mode interval/scene` flags exist but spec explicitly names them in the prohibition (intentional). 3 nice-to-have findings filed as ⚪ siblings (`empty-cache-dir-on-failure`, `frames-multiline-stdout`, `warning-mojibake`). No blockers, no functional break. +**Next action:** n/a (closed). +**Branch:** n/a + + + + + + +--- + +## 🟢 [using-yt-tools-empty-cache-dir-on-failure] — `yt-transcript` создаёт `yt-cache//` ДО фетча captions; при abort оставляет пустую папку, что противоречит Failure-modes интенту SKILL.md «никогда не оставляй полусостояние». + +**Reviewer observed** на broken-URL тесте (URL=`https://www.youtube.com/watch?v=AAAAAAAAAAAA`): exit 1 + stderr clean, но пустая `yt-cache/AAAAAAAAAAAA/` остаётся на FS. + +**Status:** done +**Closed by:** `OpeItcLoc03/common@fc400b7` — option (a) chosen: `out_dir.mkdir(parents=True, exist_ok=True)` deferred в `transcript.py` к месту прямо перед `out.write_text`; в `watch.py` — снято upfront, dir создаётся через `_ensure_source_mp4(...)`'s downstream mkdir после успешного `_fetch_snippets`. Regression: `tests/test_failure_modes.py::test_yt_transcript_no_empty_cache_dir_on_fetch_failure` + `::test_yt_watch_no_empty_cache_dir_on_snippets_failure` (mock `_fetch_snippets` raise → assert `yt-cache//` не существует). Plus happy-path sanity `::test_yt_transcript_succeeds_creates_dir`. pytest 77/77 🟢. yt-tools bump 0.1.4→0.1.5 PATCH. +**Next action:** (none — kept until merged) +**Branch:** n/a + + + +--- + +## 🟢 [using-yt-tools-frames-multiline-stdout] — Spec/реальность mismatch в SKILL.md: «Last line каждого CLI's stdout — absolute path артефакта (CLI designed для single-line EOF output)» обещает чистый bare-path, а `yt-frames --timestamps T1,T2,T3` фактически выдаёт N строк формата `Wrote: ` (с префиксом). + +**Проблема:** наивный «take last line as path» парс ломается на префиксе. Single-line EOF контракт работает для `yt-transcript` и `yt-frames` с одним таймкодом, но не для multi-timestamp. + +**Status:** done +**Closed by:** `claude-skills@b2c1a21` — option (b) chosen: fix spec, не код. Причина: `yt_tools/frames.py:12-13` docstring явно декларирует per-line `Wrote: ` дизайн «so callers can pipe/scrape without parsing summary», `tests/test_cli_smoke.py:81` уже enforce'ит `assert all(line.startswith("Wrote: "))`. Code-side change ломал бы намеренный piping-friendly контракт. SKILL.md Steps секция (L61) теперь разделяет stdout-контракт на per-CLI: `yt-transcript`/`yt-watch` — bare single-line path; `yt-frames` — N строк `Wrote: `, strip префикс «Wrote: » чтобы получить путь. Skill PATCH 0.2.1→0.2.2 (wording, без behavior change). +**Next action:** (none — kept until merged) +**Branch:** n/a + + + +--- + +## 🟢 [using-yt-tools-warning-mojibake] — yt-dlp warnings на Windows console рендерятся с mojibake: `Video unavailable “ using minimal metadata` → `Video unavailable � using minimal metadata`. + +**Корень:** stderr encoding mismatch (cp1251 default Windows console vs UTF-8 source). + +**Status:** done +**Closed by:** `OpeItcLoc03/common@fc400b7` — программная починка (первый вариант). Helper `force_utf8_streams()` в `yt_tools/core.py` делает `sys.stdout.reconfigure(encoding='utf-8', errors='replace')` + same for stderr; AttributeError/OSError swallowed для wrapped streams (pytest capsys, file redirects). Вызывается в `main()` всех трёх CLI: `yt-transcript`, `yt-frames`, `yt-watch`. pytest 77/77 🟢 (helper не ломает capsys). yt-tools bump 0.1.4→0.1.5 PATCH (одним коммитом с empty-cache-dir-on-failure). +**Next action:** (none — kept until merged) +**Branch:** n/a + + + +--- + +## 🟡 [skill-readmes] — write English README.md for every skill + translate root README +**Status:** paused +**Where I stopped:** infra-skill cluster done — READMEs for `project-bootstrap`, `setup-wiki`, `setup-tasks`, `using-wiki`, `using-tasks`; root README translated; cross-links between all five skills wired up +**Next action:** continue with the next batch — suggest the caveman cluster (`caveman`, `caveman-commit`, `caveman-review`, `caveman-help`, `caveman-compress`, `compress`) since they form a coherent group; or jump to `active-platform`, `find-skills`, `setup-context7`, `using-context7`, `using-markitdown` if the caveman cluster needs deduplication first (see `compress-dedup` task) +**Branch:** master + +--- + +## 🟢 [bootstrap-skill-deps-check] — refactor project-bootstrap Step 5.6 into a generic skill-dependencies check (replaces per-skill `Step 5.X` mirror shape) +**Status:** done +**Where I stopped:** `skills/project-bootstrap/SKILL.md` Step 5.6 переписан с single-skill `superpowers`-only detector на generic `trigger → fulfiller` table walker (9-row inline map: 8 skills + 1 plugin; `kind` flag drives install-command emission); README Workflow Step 5.6 description обновлён под новую форму; `version:` 1.6.0 → 1.7.0 (MINOR — adds capability, absorbs prior detector cleanly); `dist/project-bootstrap.skill` rebuilt + installed; `~/.claude/skills/project-bootstrap/SKILL.md` shows `version: 1.7.0`; design page `.wiki/concepts/bootstrap-skill-deps-check.md` written (rationale: why generic over per-skill mirrors, skill/plugin kind distinction, MCP-server caveat, source-of-truth invariant between SKILL map ↔ `assets/CLAUDE.md.template`); `.wiki/index.md` + `.wiki/log.md` updated. Subsumes `[bootstrap-recommend-projects-meta]` by absorption. +**Next action:** (none — kept until merged) +**Branch:** master + +--- + +## 🟢 [bootstrap-recommend-projects-meta] — Step 5.7 in project-bootstrap: recommend `setup-projects-meta` if MCP tools missing +**Status:** done +**Where I stopped:** closed by absorption 2026-05-05 — `[bootstrap-skill-deps-check]` shipped a generic Step 5.6 that walks the whole canonical trigger list (`using-projects-meta` is one of 9 rows in the inline `trigger → fulfiller` map). The per-skill mirror this task envisioned was rejected as a copy-paste explosion shape; the generic walker handles the same case + the analogous gap for every other canonical trigger. +**Next action:** (none — kept until merged) +**Branch:** master + +--- + +## 🟢 [compress-dedup] — resolve compress vs caveman-compress duplication +**Status:** done +**Where I stopped:** `skills/compress/` deleted as a byte-identical dupe of `skills/caveman-compress/` (SHA256 match across all 7 scripts/ files; SKILL.md diff was `name:` + Process step 2 only; README + SECURITY only in caveman-compress). Ported the better Process-step wording from compress (`cd ` instead of brittle `cd caveman-compress`) into `skills/caveman-compress/SKILL.md`. Added `version: 1.0.0` to caveman-compress frontmatter (first versioned release; aligns with skill-versioning concept). Removed: `skills/compress/`, `dist/compress.skill`, `~/.claude/skills/compress/` (manual prune — install.sh has no prune step). Rebuilt `dist/caveman-compress.skill` + reinstalled to `~/.claude/skills/caveman-compress/` (verified `version: 1.0.0` + new Process step on disk). Harness skill listing confirms `compress` is gone, only `caveman-compress` remains. `.wiki/concepts/compress-dedup.md` written (rationale + rejected alternatives: alias-stub has no harness mechanism, "keep both" wastes listing budget, "delete caveman-compress" loses README + SECURITY); `.wiki/index.md` + `.wiki/log.md` updated. +**Next action:** (none — kept until merged) +**Branch:** master + +--- + +## ⚪ [install-ps1] — paired install.sh + install.ps1 (cross-platform parity, with --prune) +**Status:** ready +**Where I stopped:** Verify 2026-05-07 (factory-context audit): (a) ✅ `scripts/install.ps1` существует — кросс-платформа есть; (b) ❌ `--prune` flag отсутствует в обоих скриптах; (c) ❌ doc `.wiki/concepts/install-cross-platform.md` не написан. 1/3 acceptance — не закрывать. + +Пересечение с `[claude-skills-update-skill]`: оба про install/build/prune trinity. Имеет смысл сделать вместе — `--prune` логику добавить в существующие `install.{ps1,sh}` параллельно с новыми `update.{ps1,sh}` (которые будут жить в том же `scripts/`). Doc `install-cross-platform.md` тогда покрывает всю четвёрку. +**Next action:** (a) уже есть. Остаётся: (b) добавить `--prune` flag в `scripts/install.ps1` и `scripts/install.sh` — для каждого имени в `~/.claude/skills//`, отсутствующего в `skills//`, удалить (lesson из `[compress-dedup]`). (c) написать `.wiki/concepts/install-cross-platform.md` — документировать parity-инвариант (PS+bash сpan, identical behavior, paired flags, error semantics). Опционально: совместить с `[claude-skills-update-skill]` (см. её next_action). +**Branch:** (not started) + +--- + +## ⚪ [archive-roundtrip-test] — smoke-test for .skill archive shape +**Status:** ready +**Where I stopped:** (not started) — caught the PowerShell-Compress-Archive backslash bug manually; a smoke-test would catch the next regression automatically +**Next action:** add a script that builds `dist/.skill`, unzips into a temp dir, and `diff -r` against `skills//`. Fail on any difference. Run from CI or pre-commit if we add one +**Branch:** (not started) + +--- + +## 🟡 [active-platform-eval] — eval-driven tuning of active-platform (description + body), absorbs `[active-platform-tuning]` +**Status:** paused +**Where I stopped:** design doc complete (`.wiki/concepts/active-platform-eval-design.md`, ~150 lines); per-task file complete (`.tasks/active-platform-eval.md`); STATUS.md collapsed the two original ⚪ tasks into this one block. Pre-flight verified: `claude` CLI on PATH at `C:\nvm4w\nodejs\claude.ps1` (Claude Code 2.1.128); `run_loop.py` present at `~/.claude/plugins/cache/claude-plugins-official/skill-creator/unknown/skills/skill-creator/scripts/run_loop.py`. Stopped right before eval-set authoring at user request — paused for asynchronous follow-up. No code touched, no tooling launched, no `.tasks/active-platform-eval/` workspace dir created yet. +**Next action:** resume by answering Q2 from the chat ("eval set — write 20 queries solo from current SKILL.md + concept-page open questions, or run skill-creator's HTML review template first for user-driven edits before kickoff?"), then (a) build `.tasks/active-platform-eval/eval-set.json`, (b) snapshot skill to `.tasks/active-platform-eval/skill-snapshot/`, (c) launch run_loop.py in background, (d) parallel manual body sweep, (e) apply changes + bump to 1.1.0 + rebuild + reinstall + final report + commit. +**Branch:** master + +--- + +## ⚪ [skills-grouping-revisit] — revisit flat vs grouped skills/ layout if count grows past ~30 +**Status:** ready +**Where I stopped:** (not started) — current 14 skills fit fine in flat `skills/`; threshold for revisiting is ~30 +**Next action:** when triggered (skill count crosses threshold), evaluate variant B (grouped by family) vs variant C (flat + manifest) from the original brainstorm in `.wiki/concepts/repo-layout.md` +**Branch:** (not started) + +--- + +## 🟢 [setup-interns-fix-paths] — fix `/.common/...` cwd-relative paths in `setup-interns` SKILL.md (mirror of done `using-projects-meta-fix-paths`) +**Status:** done +**Where I stopped:** Shipped в `54ba5ca`. All `/.common/...` replaced with `~/projects/.common/...` across Phase 0–8, rollback, cross-platform notes, and common mistakes. Mirrors setup-projects-meta absolute-path convention. Bumped version 0.2.0 → 0.3.0 (MINOR — includes clone-fallback capability from sibling task). Rebuilt `dist/setup-interns.skill` + installed to `~/.claude/skills/setup-interns/`. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + + +## 🟢 [refresh-project-bootstrap] — Run the `project-bootstrap` skill in this repo to refresh its layout to the canonical state (git, .gitignore, README.md, .wiki/, .tasks/, CLAUDE.md). The skill handles both greenfield bootstrap and refresh of existing repos. +**Status:** done +**Where I stopped:** (not started) +**Next action:** Invoke the `project-bootstrap` skill at the repo root, confirm each file write, push to main. +**Branch:** n/a + + + +--- + +## 🟢 [using-projects-meta-fix-paths] — fix stale `~/.local/projects-meta-mcp/` paths in `setup-projects-meta` SKILL.md +**Status:** done +**Where I stopped:** `setup-projects-meta/SKILL.md` lines 152–154 (Phase 5 platform-path table) переписаны с `~/.local/projects-meta-mcp` на `~/projects/.common/lib/projects-meta-mcp` (Windows/Linux/macOS варианты); `version:` bump 1.0.0 → 1.0.1 (PATCH, doc consistency); rebuild `dist/setup-projects-meta.skill` + install в `~/.claude/skills/setup-projects-meta/` подтверждены (frontmatter `version: 1.0.1` в установленной копии); `using-projects-meta/{SKILL,README}.md` уже были чистые из прошлых коммитов; line 224 (`Path forms (~/.local/..., ~/.config/..., ~/projects/...) are identical on all three.`) намеренно оставлена как generic POSIX-path syntax aside, не projects-meta install reference +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [interns-repo-read-skill-updates] — Skill-side updates for the `repo_read` intern (design at claude-skills/.wiki/concepts/interns-repo-read-design.md). Two coordinated edits: (1) `using-interns/SKILL.md` routing section — add hints on when to use `repo_read` vs `bulk_text_read`, MINOR bump; (2) `setup-interns/SKILL.md` — add Node-in-PATH check + optional `npx repomix@latest --version` pre-warm step, MINOR bump. Blocked by `common#interns-repo-read-impl` only for end-to-end test; skill edits themselves can ship in parallel. + +**Status:** done +**Where I stopped:** both skills edited (0.1.0→0.2.0), rebuilt + installed, versions verified on disk +**Next action:** (none — kept until merged) +**Branch:** n/a + + +--- + +## 🟢 [project-creation-lifecycle-skill] — Extend `project-bootstrap` (or add a new `create-project` skill) to cover the **full new-project lifecycle**, not just upgrading an existing folder. + +**Gap surfaced 2026-05-06** при промоушене брейнсторма `factory-bootstrap`: репо `git.kzntsv.site/OpeItcLoc03/factory` был создан ad-hoc (git init + gitea repo create + push), без бутстрапа `.tasks/STATUS.md` и `.wiki/index.md`. Из-за этого `projects-meta-mcp:sync-runner.ts:69` отбрасывал репо как «не проект», и `knowledge_ingest target=factory` / `tasks_create target=factory` валились с «project not in cache». Промоушн встал, пришлось вручную сидить layout через прямые `Write` + push. Это чинится один раз в скиле, а не каждый раз руками. + +**Что должен делать новый flow** (один обход): +1. Создать локальную папку `~/projects//` (валидация: latin, kebab-case, не существует). +2. `git init` + первоначальный `.gitignore` + минимальный `README.md`. +3. Создать гитеа-репо в org/user через Gitea API (auth из `~/.config/projects-mcp/auth.toml`), set remote, push initial commit. +4. Делегировать в `setup-wiki` + `setup-tasks` для канонического layout (или inline-fallback с записью в bootstrap-manifest). +5. `node ~/projects/.common/lib/projects-meta-mcp/dist/sync.js` — re-sync кэша, чтобы новый репо стал виден projects-meta. +6. Опционально: `tasks_create target=_meta slug=-onboarded` — чтобы новый проект засветился на кросс-проектном борде с момента создания. + +**Два варианта дизайна — выбрать в скил-creator:** +- **(a) Extend `project-bootstrap`** — добавить flag `--create` (или авто-detection «папки нет / git remote нет»), который запускает шаги 1–3 и 5–6. Существующий path для «уже существующая папка → upgrade» сохраняется. Плюс: один скил для всего lifecycle. Минус: SKILL.md распухает. +- **(b) Новый скил `create-project`** — отдельный entry-point, в конце дёргает `project-bootstrap` для шагов 4 (layout). Плюс: single-responsibility. Минус: ещё один скил в реестре, юзеру выбирать какой звать. + +Склоняюсь к (a) — `project-bootstrap` уже знает про оба режима init/upgrade (см. SKILL.md Step 0), четвёртый под-режим «greenfield + remote create» естественно туда ложится. Но это решение уровня skill-creator. + +**Acceptance criteria:** +- Новый flow прогоняется на пустой папке: `~/projects/.test-greenfield/` → один вызов скила → конец: репо в gitea, layout в `.wiki/`/`.tasks/`, виден в `mcp__projects-meta__meta_status`. +- Существующий upgrade-mode не сломан (regression-test на `.factory/` после этого фикса — re-run должен быть no-op). +- README скила обновлён. + +**Status:** done +**Where I stopped:** Shipped в `23431c5 feat(project-bootstrap): add greenfield-full mode with remote create`. Variant (a) — extend `project-bootstrap`. SKILL.md v1.8.0 → 1.9.0 (mode `greenfield-full` + `add-remote` + `upgrade`); Step 1.5 (remote create через Gitea API + auth.toml + push initial); Step 8 (projects-meta sync через `dist/sync.js`). README обновлён под три режима. ⚠️ Acceptance criteria smoke-test «один вызов на `.test-greenfield/`» **не прогонялся** — surfaced в `using-tasks-close-coverage-gate`. Закрыто по shipped-code-баребоне; regression-test трек отдельно. +**Next action:** (none — kept until merged); regression-smoke-test на пустой папке трекать через `using-tasks-close-coverage-gate` +**Branch:** n/a + + +--- + +## 🟢 [extend-project-discipline-brainstorm-workspaces] — Расширить `project-discipline` (`C:\Users\vitya\.claude\skills\project-discipline\SKILL.md`) явным обращением к **transit-zone / brainstorm-workspaces** типа `~/projects/.meeting-room/`. + +**Gap surfaced 2026-05-05:** Rule 1 скила («project conventions override skill defaults») написан для проектов с `.wiki/` / `.tasks/`. Не покрывает кейс, когда workspace **сам по себе** — discussion-zone, и его README/CLAUDE.md явно говорит «здесь no `.tasks/`, transit zone, всё уезжает в глобал». В той сессии я неправильно применил «transit-zone autopilot» — потащил artifact из брейнсторма в `~/projects/.wiki/concepts/` без явной директивы пользователя. Корректировка: artifacts уезжают в `.brainstorm/.md` (in-progress) ИЛИ в глобал-вики (mature, user-directed) — никогда автопилотом по аналогии с Rule 1. + +**Предлагаемая поправка к SKILL.md:** +1. Default destination для in-progress brainstorm artifacts: `.brainstorm/.md` (или чем README workspace'а это объявил). +2. Default destination для mature, cross-cutting outputs: `~/projects/.wiki/concepts/-design.md` через `mcp__projects-meta__knowledge_ingest` — **только** когда user явно это направил. +3. Агент **не должен** auto-promote brainstorm-артефакты в глобал-вики по аналогии с Rule 1; convergence-moment решение принадлежит user'у. + +**Reference:** `.meeting-room/CLAUDE.md` уже описывает «hard rules» этого workspace'а (правила §1–§4) — это конкретный пример того, что project-discipline должен явно поддерживать. + +**Acceptance criteria:** +- В SKILL.md `project-discipline` появилась секция «Transit-zone / brainstorm workspaces» с тремя пунктами выше. +- Регрессия: на тестовом meeting-room сессии (как 2026-05-05) агент не вытаскивает brainstorm в `.wiki/concepts/` без user-команды. + +**Status:** done +**Where I stopped:** Shipped в `215afdd feat(project-discipline): add Rule 5 — transit-zone workspaces`. SKILL.md v0.1.0 → 0.1.1 (PATCH; описано в commit как добавление, де-факто новая секция — но user-decision на PATCH). Rule 5 содержит три обязательных пункта (`.brainstorm/.md` для in-progress, global wiki только по user-команде, no auto-promote по аналогии с Rule 1). `.meeting-room/CLAUDE.md` служит каноничным примером (упомянут в Example блоке Rule 5). +**Next action:** (none — kept until merged) +**Branch:** n/a + + +--- + +## 🟢 [recommend-dont-menu-skill] — Создать новый скил `recommend-dont-menu` (имя обсуждаемо в skill-creator) — codifies user-style override: в design-conversations, брейнстормах, code review агент даёт **одну аргументированную рекомендацию**, а не меню вариантов A/B/C/D. + +**Why skill, not CLAUDE.md:** правило сейчас живёт в `~/.claude/CLAUDE.md` на одной машине (создан 2026-05-06). Per-machine, per-agent — на новом ноуте его не будет, Gemini/Copilot его не прочитают. Скил в claude-skills клонится на все машины как L1 component factory-manifest'а; trigger-line в CLAUDE.md template наследуется всеми проектами через project-bootstrap. + +**Содержимое скила (SKILL.md body — готово, копируется из удалённого memory entry):** + +> Default mode для design-questions: «Я рекомендую X, потому что Y₁, Y₂. Trade-off: Z. Возражения?» +> +> Альтернативы упоминать **только если они реально близки** или несут важный trade-off, который user должен взвесить — тогда коротко: «Если важно W — лучше X', но добавляет сложность; иначе X». +> +> Не перечислять варианты ради видимости вариативности. Меню тормозит когда один вариант очевидно лучше — вынуждает читать заведомо проигрышные опции и задвигает позицию агента за обтекаемое перечисление вместо ответственной рекомендации. +> +> Это override стандарта `superpowers:brainstorming`, где multiple-choice указан как preferred. User instructions > skill defaults. + +**Frontmatter (черновик):** +```yaml +--- +name: recommend-dont-menu +description: Use during design discussions, brainstorming, architecture reviews, or any "what should we do" question — give one argued recommendation with explicit trade-offs, not a multiple-choice menu. Override of superpowers:brainstorming default. +version: 0.1.0 +--- +``` + +**Triggers (когда скил активируется):** +- Любые design / architecture / "что выбрать" вопросы. +- Активные брейнстормы (где `superpowers:brainstorming` тоже бы подцепилось — этот скил **переопределяет** его стиль). +- Code review с альтернативами. +- В `.meeting-room/` — постоянно (в её workspace contract это default для совещаний). + +**Cross-agent applicability:** скил — про стиль ответа, не про tool calls, поэтому работает на любом агенте без mappings (в отличие от `using-superpowers`, которому нужны `references/copilot-tools.md` и т.д.). В description явно отметить «works on any agent — pure response-style rule». + +**Integration tasks:** +1. **Trigger-line в `project-bootstrap` CLAUDE.md template.** Добавить строку в canonical set (`assets/CLAUDE.md.template`, см. `project-bootstrap/SKILL.md` Step 5). Кандидаты: `prefer single recommendations`, `recommend, don't menu`, `argued recommendations`. Решает skill-creator. После — в Step 5.6 (skill dependencies check) добавить новую строку в trigger→fulfiller table. +2. **Свернуть `~/.claude/CLAUDE.md`** на машинах, где скил установлен, до одной trigger-строки. Текущее содержимое (1 параграф правила) переносится в SKILL.md body, в global CLAUDE.md остаётся только триггер. +3. **Cross-agent prop:** trigger-line должен попасть в `~/.gemini/GEMINI.md` и `~/.copilot/AGENTS.md` — следствие, отдельная подзадача (либо часть `setup-*` для соответствующих агентов, либо часть будущего factory L1 manifest'а). + +**Acceptance criteria:** +- `~/.claude/skills/recommend-dont-menu/SKILL.md` существует, проходит skill-validation. +- Trigger-line в `project-bootstrap/assets/CLAUDE.md.template` + соответствующий ряд в Step 5.6 trigger→fulfiller table. +- Регрессионный smoke-test: в test-сессии после установки скила я (агент) на «как лучше — X или Y?» отвечаю «рекомендую X, потому что Z. Возражения?» вместо меню. +- В `using-superpowers/SKILL.md` или `superpowers:brainstorming` упомянут override (либо через priority-секцию, которая уже есть: «User instructions > Superpowers skills > Default»). + +**Reference:** правило выкристаллизовалось в брейнсторме interns 2026-05-05, проверено многократно в `.meeting-room/` сессиях. Source content до удаления memory лежал в `feedback_brainstorm_recommend_dont_menu.md` (memory была удалена 2026-05-06 после переноса в `~/.claude/CLAUDE.md`, который сам теперь временный). + +**Status:** done +**Where I stopped:** Shipped в `011a8b4 feat(recommend-dont-menu): add skill + integrate into project-bootstrap [v0.1.0 / v1.9.0]`. Skill at `skills/recommend-dont-menu/SKILL.md` v0.1.0; trigger-line `recommend, don't menu` в `project-bootstrap/assets/CLAUDE.md.template:12`; row в Step 5.6 trigger→fulfiller table at `project-bootstrap/SKILL.md:447`; override упомянут в SKILL.md секция "## Override". `project-bootstrap` bumped 1.8.0 → 1.9.0 (MINOR; одновременно с greenfield-full mode из `[project-creation-lifecycle-skill]` — single bump cover both features). Heading emoji приведён к `done`-канону 2026-05-06 (был ⚪ при `Status: 🟢 Done` — рассинхрон, surfaced `using-tasks-close-coverage-gate`). +**Next action:** (none — kept until merged) +**Branch:** n/a + + +--- + +## 🟢 [hermes-converter-mvp] — Build conversion infrastructure для Hermes-rollout: создать `hermes/mapping.yaml` (схема: per-skill `mode: auto|manual|skip`, `category`, `replace-rules`, `skip-list` + `reason`), написать `scripts/build-hermes.{sh,py}` (читает `skills//SKILL.md` или `hermes/skills//` для manual, применяет mapping, пишет `dist-hermes///`, генерит `dist-hermes/SKIPPED.md` с per-skip reason'ами). Прогнать через 4 universal: `pulling-before-work`, `active-platform`, `project-discipline`, `using-markitdown`. Закоммитить `dist-hermes/` для этих 4 в репу. Дизайн: `.wiki/concepts/hermes-skills-rollout-design.md`. Pre-encode security уроки (extraheader-pattern, POSIX-absolute paths, version-bump per Rule 3) на этапе шаблонов конвертера. + +**Status:** done +**Where I stopped:** Shipped в `6b36b31 feat(hermes): MVP converter + 4 universal skills converted`. (1) `hermes/mapping.yaml` — 4-mode schema (`auto`/`manual`/`skip`/`pending`); 22 skills mapped explicitly; build fails on unmapped (verified on synthetic `fake-skill` → exit 1). (2) `scripts/build-hermes.py` — Python (PyYAML 6.0.3); replace-rules ordered string-substitution on SKILL.md only; manual mode copies `hermes/skills//` verbatim; SKIPPED.md auto-generated с pending intended-mode preserved. (3) 4 universal через converter в `dist-hermes/`: `pulling-before-work` + `project-discipline` (with READMEs) → `software-development/`; `active-platform` (replace-rules `**Windows + PowerShell.**` → `**Linux + bash.**` + reasoning sentence) → `software-development/`; `using-markitdown` → `productivity/`. (4) `dist-hermes/` committed (8 файлов: 4 SKILL.md + 2 README.md + SKIPPED.md). (5) Security infra: replace-rules + manual mode ready; concrete extraheader / POSIX / version-bump templates land в `hermes-flavour-mcp-setups` (per design separation `.wiki/concepts/hermes-skills-rollout-design.md` § Связанные таски). README.md updated (новая секция `### Build for Hermes` + Layout). Per-task file `.tasks/hermes-converter-mvp.md`. **Smoke:** idempotent re-run no diff; replace-rules verified via grep on `dist-hermes/.../active-platform/SKILL.md`; strict-mapping fail-fast verified. +**Next action:** (none — kept until merged); unblocks `[hermes-flavour-mcp-setups]`, `[hermes-installer-skill]`, `[hermes-mvp-coverage]` +**Branch:** master + + +--- + +## 🟢 [hermes-flavour-mcp-setups] — Переписать `setup-projects-meta` и `setup-context7` в Hermes-flavour: вместо клона/билда — yaml-edit `~/.hermes/config.yaml > mcp_servers.` (stdio command pointing к `~/projects/.common/lib/projects-meta-mcp/dist/server.js`, env-vars из `~/.config/projects-mcp/auth.toml`), затем `/reload-mcp`. Обе версии лежат как `mode: manual` в `hermes/skills/setup-projects-meta-hermes/SKILL.md` и `hermes/skills/setup-context7-hermes/SKILL.md` (конвертер копирует as-is, без преобразований). Pre-check: бинарь existing в `~/projects/.common/lib/projects-meta-mcp/`, `auth.toml` existing в `~/.config/projects-mcp/`. Применить extraheader-pattern сразу на git-clone fallbacks (если pre-check провалится). Дизайн: `.wiki/concepts/hermes-skills-rollout-design.md`. Зависит от `hermes-converter-mvp` (нужен mapping.yaml schema понимающий `mode: manual`). + +**Status:** done +**Where I stopped:** Shipped в `5990b06 feat(hermes): flavour-mcp-setups + installer-skill`. `hermes/skills/setup-projects-meta/SKILL.md` + `hermes/skills/setup-context7/SKILL.md` написаны (mode: manual, yaml-edit паттерн); mapping.yaml: оба pending → manual; `dist-hermes/mcp/setup-projects-meta/` + `dist-hermes/mcp/setup-context7/` populated; extraheader-pattern на git-clone fallback включён. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [hermes-installer-skill] — Написать `dist-hermes/meta/claude-skills-installer/SKILL.md` — recursive bootstrap installer для Hermes-стороны. SKILL'у на триггер «установи скилы из claude-skills» / «обнови claude-skills»: итерирует по `dist-hermes///` (всем кроме `meta/`), для каждого вызывает `skill_manage(action='create', category=, name=, content=, assets=)`. Учитывать `dist-hermes/SKIPPED.md` — не пытаться установить пропущенные. Документировать **bootstrap-процедуру** в `claude-skills/README.md` (Linux/Hermes раздел): один раз вручную `skill_manage(action='create', from=)`, далее команда «обнови claude-skills» работает сама. Recursive: installer обновляется вместе со всем остальным через `git pull && trigger update`. Дизайн: `.wiki/concepts/hermes-skills-rollout-design.md`. + +**Status:** done +**Where I stopped:** Shipped в `5990b06 feat(hermes): flavour-mcp-setups + installer-skill`. `dist-hermes/meta/claude-skills-installer/SKILL.md` написан (recursive bootstrap, iterates dist-hermes/ excluding meta/, calls skill_manage); README.md Hermes quick-start секция добавлена. +**Next action:** (none — kept until merged); smoke-test на фабричной Linux box ещё не прогонялся +**Branch:** master + + +--- + +## 🟢 [hermes-mvp-coverage] — Расширить `hermes/mapping.yaml` и пропустить через converter оставшиеся 9 MVP-скилов: `setup-tasks`, `using-tasks`, `setup-wiki`, `using-wiki`, `using-projects-meta`, `using-context7`, `project-bootstrap` (адаптируется последним — orchestrator). Замечания: (a) `using-wiki`/`setup-wiki` поверх Hermes built-in `research/llm-wiki` — наша schema (`.wiki/CLAUDE.md`, `entities/persons/`, `packages/`, `raw/research,transcripts`) сохраняется через override-precedence; (b) `project-bootstrap` Hermes-flavour убирает CLAUDE.md trigger-lines (Hermes auto-discover), оставляет git/gitignore/README/setup-wiki/setup-tasks orchestration. End-to-end smoke-test на фабричной Linux-машине: `git clone claude-skills` на чистую box → `skill_manage` installer → trigger «установи всё» → `hermes skills list` показывает все 13 в правильных категориях → `mcp__projects_meta__*` тулы доступны после `/reload-mcp`. Дизайн: `.wiki/concepts/hermes-skills-rollout-design.md`. + +**Status:** done +**Where I stopped:** Shipped в `a003b80 feat(hermes): mvp-coverage — 9 skills converted`. mapping.yaml: 7 pending → auto. dist-hermes/ populated: software-development (project-bootstrap), productivity (recommend-dont-menu, setup-tasks, using-tasks), research (setup-wiki, using-wiki), mcp (using-context7, using-projects-meta). pending count now 0. active-platform replace-rule verified (Windows+PowerShell → Linux+bash in body). +**Next action:** (none — kept until merged); unblocks `hermes-converter-ci` + +--- + +## ⚪ [hermes-converter-ci] — [deferred — после ручной валидации MVP] CI hook (Gitea-pipeline или GitHub-Action если зеркалим): на push to master запустить `scripts/build-hermes.py`, сравнить diff `dist-hermes/`, авто-коммит если изменения (или PR-шаблон). Цель — чтобы `dist-hermes/` всегда матчил `skills/`+`hermes/mapping.yaml`+`hermes/skills/` без ручного запуска build. Не блокирует MVP — первая итерация делается ручным запуском конвертера. Дизайн: `.wiki/concepts/hermes-skills-rollout-design.md`. + +**Status:** ready +**Where I stopped:** unblocked 2026-05-07 — `hermes-mvp-coverage` shipped в `a003b80`; MVP живой на фабрике. +**Next action:** Выбрать платформу CI (Gitea Actions vs внешняя), написать workflow, прогнать тестовый push. +**Branch:** master + + +--- + +## 🟢 [using-tasks-close-coverage-gate] — Расширяет `using-tasks` SKILL **двумя коррелированными правилами для decision-points** (изначально таска была только про close-coverage; scope-priority добавлен 2026-05-06 после session-start ревью отчёта claude-skills агента). + +**Часть A — Pre-close coverage gate** (исходный scope). `using-tasks` должен явно требовать coverage-проверку acceptance-criteria тестами **перед** вызовом `tasks_close`. Surfaced 2026-05-06 в код-ревью factory-bootstrap fallout: 3 из 4 common-фиксов закрыты по «150/150 / 151/151 tests pass» (existing suite), но новые behaviour не покрыты — нет теста на `cached = null` invalidation, нет теста на `AggregateStatusEnum` validation error, partial test на archived-filter. Acceptance criteria требовали regression-тестов — пропущены. Также: после `feat:`/`fix:` коммита skill должен подсказывать «эта работа закрывает таску ``?» — иначе код shipped (`215afdd`, `23431c5` в claude-skills) при stale ⚪ ready статусе (`extend-project-discipline-brainstorm-workspaces`, `project-creation-lifecycle-skill`). + +**Часть B — Scope priority at recommendation time** (added 2026-05-06). При session-start (или любой триггер «что делать дальше / куда копаем»), рекомендации должны идти **в порядке**: сначала ranked-список из cwd-проекта (🔴 active → 🟡 paused → ⚪ ready), потом — **одна footnote-строка** «есть 🔴 в других проектах: , см. `tasks_aggregate`», если релевантно. Cross-project — информация, не driver рекомендации. Surfaced когда другой агент в claude-skills cwd рапортовал «Срочные — 3 🔴 в других проектах (stostayer.new, modules-db, crsc.web)» — выводя cross-project в первую строку, хотя у юзера cwd был claude-skills и интересовали локальные таски. `using-projects-meta` уже декларирует local-first для **чтений** — нужно распространить на **recommendation phase**. + +Парный фикс к `common#tasks-close-normalize-body` (там tooling, тут policy/skill). После закрытия: новые hermes-таски попадут в починенный close-flow + рекомендации фокусируются в cwd-проекте. + +**Status:** done +**Where I stopped:** Shipped в `b0d2d51 feat(using-tasks): pre-close coverage gate + local-first recommendations [v1.1.0]`. Часть A: `### Task completion` step 1 — pre-close coverage check со списком acceptance criteria и грозой gap'а; новая секция `### Post-commit task closure prompt` — на `feat:`/`fix:` коммитах prompt про closure (skips `chore:`/`meta:`). Часть B: новая секция `### Recommendations / "what's next" trigger` — local cwd-board first, cross-project как одна footnote-строка; explicit cross-project trigger flips order. README.md mirrored. Version bump 1.0.0 → 1.1.0 (MINOR, **не PATCH** как было в next_action — Rule 3 grades adding 2 new operation types as capability addition). Build+install подтверждены, `~/.claude/skills/using-tasks/SKILL.md` shows `version: 1.1.0`. **Smoke-tests:** (1) coverage-check триггернулся на самой этой close-операции (acceptance criteria verified inline above); (2) local-first рекомендации сработали раньше в этой сессии — на «discipline pre-reqs первыми» отчёт фокусировался на claude-skills, не на cross-project 🔴. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [tdd-criteria-skill-write] — runtime artefact for the TDD-criteria policy. Design rationale (the «why» behind every rule + the anti-vandalism leading argument + the test-immutability second-order defence) lives in `.wiki/concepts/tdd-criteria-design.md` (already promoted via `meeting-room-promote-brainstorm` 2026-05-07; NOTE: design doc was promoted at 2026-05-07T04:00 with 3 anti-loophole rules; **rule 4 (test-immutability) was added in this task description below at 2026-05-07T05:00 after user surfaced the symmetric vandalism risk; the impl session must mirror rule 4 into the design doc as well — see «Design doc amendment» section below**). + +This task writes the **runtime SKILL.md** + amends the design doc to include rule 4. + +**Frontmatter (YAML):** +- `name: tdd-criteria` +- `version: 0.1.0` +- `description: >` (multi-line) — must include trigger phrases the agent recognises: "TDD", "test-driven", "следуй TDD", "use TDD", "should I write tests", "skip tdd", "[skip-tdd: ...]", "[test-modify: ...]", and the bare topic name `tdd-criteria`. Also state cross-agent applicability and reference the design page. + +**Body sections (use `project-discipline` and `recommend-dont-menu` as structural templates):** +1. **`# tdd-criteria`** — one-line tag-line. +2. **`## When this runs`** — trigger phrases (session-start trigger via `follow tdd-criteria` line in CLAUDE.md, plus on-demand triggers); explicit «applies before any code touches a *.ts/*.js/*.py file the agent didn't author this session». +3. **`## Default mode`** — one sentence: «TDD by default. Skip only if one of four bright-line carve-outs matches and is marked in commit subject.» +4. **`## Decision algorithm (8 questions, top-down)`** — copy the algorithm block verbatim from design doc. +5. **`## Ironclad rules (TDD obligatory)`** — 4 rules, each ≤3 lines: trigger property + what test type. No rationale (rationale = design doc). +6. **`## Permissive carve-outs (skip + marker required)`** — 4 categories, each ≤2 lines: trigger + marker. +7. **`## Anti-loophole`** — **4 rules** (was 3 before the 2026-05-07 amendment): + - Rule 1: skip-without-category invalid + - Rule 2: spike-survivor (backfill-tests task on merge) + - Rule 3: friction is the point (don't relax before ≥2 weeks) + - **Rule 4 (NEW): tests are append-only by default.** Modifying assertion / deleting test / disabling (`it.skip`/`xit`/`@skip`/`@Disabled`) requires: + - **Marker in commit subject:** `[test-modify: : was ; is ; reason: ]` where `` and `` are the **literal assertion expressions** (not paraphrased). + - **Separate commit from impl changes:** a commit must not modify both `*.test.*` and `src/*` files (or project-equivalents). `git log --grep '\[test-modify'` must give a clean test-only audit trail. + - **Why literal `was/is`:** an agent forced to write the literal assertion publishes exactly what they're rewriting. Reasons like «updated to match new behaviour» hide vandalism — agents will use them whenever allowed. + - **Bright-line check** for an optional pre-commit hook (see follow-up task `tdd-criteria-precommit-hook`): diff includes removed `expect(...)` / changed assertion args / added `.skip`/`xit`/`@skip` / deleted test definition AND commit subject has no matching `[test-modify: ...]` → block. AND files include both test-pattern and impl-pattern → block (require split). + - Composite-task pattern (decompose by artefact) is example, not rule. +8. **`## Cross-agent applicability`** — pure policy, no Claude tool refs, Hermes-mappable as `mode: auto`. +9. **`## Out of scope`** — does not enforce via git hooks (separate optional task `tdd-criteria-precommit-hook`); does not modify project CLAUDE.md (that's `project-bootstrap`'s job); does not run tests. +10. **`## Why this exists`** — one paragraph: «Tests make behaviour an invariant; without them code is an artefact silent-deletable by agents. AND: the test itself must be defended too (rule 4) — otherwise the contract collapses back into an artefact when the agent rewrites the failing test. Full rationale at `.wiki/concepts/tdd-criteria-design.md`.» + +**Constraints:** +- No `Read/Edit/Glob/Bash` references in body — keep it agent-agnostic. +- No code blocks with shell commands — pure policy doc. +- Length target: ≤220 lines (project-discipline ~140, this is denser due to rule 4). +- Frontmatter version starts at `0.1.0` (per Rule 3 of `project-discipline`: first edit of new versioned artefact = add 0.1.0, not bump). + +**Design doc amendment (do in same impl session):** + +After SKILL.md is written, also amend `~/projects/claude-skills/.wiki/concepts/tdd-criteria-design.md` to add rule 4 to the «Anti-loophole» section AND extend «The argument behind TDD-default» with a sub-section «The contract is only as strong as the contract itself». Full text below — paste verbatim, no rewriting: + +--- + +**ADD to «The argument behind TDD-default» section, after the existing paragraphs:** + +```markdown +### The contract is only as strong as the contract itself + +But there's a **second-order vandalism mode** that the bare contract argument doesn't cover: the agent doesn't delete the code, it rewrites the **test**. Test fails → agent changes the expected value, adds `.skip`, or deletes the test → test now passes → success reported. + +If the contract artefact (the test) is rewritable by the same agent that's failing to satisfy it, the invariant collapses back into an artefact. The defence requires **two layers**: + +1. **Code is defended by tests.** Ironclad rules 1-4 below. +2. **Tests are defended by process discipline.** Anti-loophole rule 4 below — append-only by default, modifications require literal-`was/is` marker in commit subject, test changes are separate commits from impl changes. + +Both layers are needed. Either alone leaves a path-of-least-resistance route to «success». +``` + +**ADD to «Anti-loophole» section (after rule 3):** + +```markdown +4. **Tests are append-only by default** (added 2026-05-07). New tests: free. **Modifying** an existing assertion, **deleting** a test, or **disabling** it (`it.skip`, `xit`, `@pytest.mark.skip`, `@Disabled`, etc.) requires both: + + **a)** A marker in commit subject: + `​`` + [test-modify: : was ; is ; reason: ] + `​`` + Where `` and `` are the **literal assertion expressions** before and after, not paraphrased. Example: + `​`` + [test-modify: validates email format: was expect(isValid("a@b")).toBe(true); is expect(isValid("a@b.com")).toBe(true); reason: tightened spec to require TLD] + `​`` + + **b)** Test changes go in a **separate commit** from any impl changes. A single commit must not modify both `*.test.*` and `src/*` files (or their project-equivalents). This forces an audit-able split — `git log --grep '\[test-modify'` shows every test rewrite cleanly. + + **Why literal `was/is`, not free-form reason:** an agent forced to write the literal assertion publishes exactly what they're rewriting. If `42` was the correct expectation and they changed it to `43` to make a buggy fix pass, the literal `was 42; is 43` line in `git log` identifies the culprit. A reason like «updated to match new behaviour» hides everything — agents will use it whenever it is allowed. + + **Bright-line check** (for an optional pre-commit hook, see follow-up task `tdd-criteria-precommit-hook`): + - `git diff --cached` includes a removed `expect(...)` / `assert(...)` / `assertThat(...)` line, OR + - changes the arguments of an existing assertion call, OR + - adds `.skip`, `xit`, `@skip`, `@Disabled`, etc. annotation, OR + - deletes a test file or `it(...)` / `test(...)` / `def test_*` definition + + AND the commit subject does not contain `[test-modify: ...]` matching the format above → block. + + AND `git diff --cached --name-only` includes both test-pattern and impl-pattern files → block (require split). + +5. **Don't apply rule 4 retroactively** to tests written before the rule was adopted. The rule applies to test changes made after the project's CLAUDE.md picks up `follow tdd-criteria`. Existing test bodies aren't grandfathered into requiring `was/is` for a one-time rewrite. +``` + +**ADD to «What's excluded as not bright-line»:** +- ~~«Tests should not be modified casually»~~ — paraphrasable, agents will modify casually and call it «refactor». Replaced by Anti-loophole rule 4 with literal-evidence requirement. + +**ADD to «Trade-offs» section:** +- **The literal-`was/is` requirement is verbose** for a renamed test or trivial typo fix. The verbosity is the point — an agent that genuinely fixed a typo writes the same assertion twice with one character changed; an agent that rewrote a failing test writes obviously different assertions. Reading `git log --grep '\[test-modify'` shows the difference at a glance. + +**UPDATE frontmatter** to add `amended: "2026-05-07: added test-immutability defence (Anti-loophole rule 4) after user noted symmetric vandalism risk on tests"`. + +--- + +**Commit pattern for impl session:** +- Commit 1: `skills/tdd-criteria/SKILL.md` (new file). Subject: `feat(skills): tdd-criteria skill v0.1.0 [TDD-default + 4 carve-outs + 4 anti-loophole rules incl. test-immutability]`. +- Commit 2: `.wiki/concepts/tdd-criteria-design.md` (amend). Subject: `docs(tdd-criteria): rule 4 — test-immutability defence (was X; is Y marker)`. Note: this is wiki, not test code, so `[test-modify]` rule doesn't apply to this commit — rule 4 governs *test* file changes, not wiki rationale changes. + +**Frontmatter (YAML):** +- `name: tdd-criteria` +- `version: 0.1.0` +- `description: >` (multi-line) — must include trigger phrases the agent recognises: "TDD", "test-driven", "следуй TDD", "use TDD", "should I write tests", "skip tdd", "[skip-tdd: ...]", and the bare topic name `tdd-criteria`. Also state cross-agent applicability and reference the design page. + +**Body sections (use `project-discipline` and `recommend-dont-menu` as structural templates):** +1. **`# tdd-criteria`** — one-line tag-line. +2. **`## When this runs`** — trigger phrases (session-start trigger via `follow tdd-criteria` line in CLAUDE.md, plus on-demand triggers); explicit «applies before any code touches a *.ts/*.js/*.py file the agent didn't author this session». +3. **`## Default mode`** — one sentence: «TDD by default. Skip only if one of four bright-line carve-outs matches and is marked in commit subject.» +4. **`## Decision algorithm (8 questions, top-down)`** — copy the algorithm block verbatim from design doc. +5. **`## Ironclad rules (TDD obligatory)`** — 4 rules, each ≤3 lines: trigger property + what test type. No rationale (rationale = design doc). +6. **`## Permissive carve-outs (skip + marker required)`** — 4 categories, each ≤2 lines: trigger + marker. +7. **`## Anti-loophole`** — 3 bullets: skip-without-category invalid; spike-survivor rule; composite-task = decompose by artefact. +8. **`## Cross-agent applicability`** — pure policy, no Claude tool refs, Hermes-mappable as `mode: auto`. +9. **`## Out of scope`** — does not enforce via git hooks (separate optional task `tdd-criteria-precommit-hook`); does not modify project CLAUDE.md (that's `project-bootstrap`'s job); does not run tests. +10. **`## Why this exists`** — one paragraph: «Tests make behaviour an invariant; without them code is an artefact silent-deletable by agents. Full rationale at `.wiki/concepts/tdd-criteria-design.md`.» + +**Constraints:** +- No `Read/Edit/Glob/Bash` references in body — keep it agent-agnostic. +- No code blocks with shell commands — pure policy doc. +- Length target: ≤200 lines (project-discipline is ~140 — similar density). +- Frontmatter version starts at `0.1.0` (per Rule 3 of `project-discipline`: first edit of new versioned artefact = add 0.1.0, not bump). + +**Status:** done +**Where I stopped:** Shipped в `954f8ba feat(skills): tdd-criteria skill v0.1.0`. SKILL.md 86 lines, 4 ironclad + 4 permissive + 4 anti-loophole (incl. rule 4 test-immutability), 0 Claude-tool refs. Design doc amended в `2ba6981` — rule 4 + subsection + trade-off + excluded formulation added. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [tdd-criteria-hermes-mapping] — Add entry for `tdd-criteria` skill to `~/projects/claude-skills/hermes/mapping.yaml`. Required step — `build-hermes.py` fails on unmapped skills. + +**Entry (place alphabetically among `auto`-mode skills, after `project-discipline`):** + +```yaml + tdd-criteria: + mode: auto + category: software-development +``` + +**No `replace-rules`** — the skill is pure policy (no Claude-tool refs like `Read`/`Edit`/`Bash`/`Glob` in SKILL.md body). Verified by `tdd-criteria-skill-write` task constraints. + +**Verification step:** run `python ~/projects/claude-skills/scripts/build-hermes.py` after edit — output must include `tdd-criteria` in the converted list under `dist-hermes/software-development/tdd-criteria/`. If build fails on `Unmapped skill: tdd-criteria` the entry didn't take; if it fails with replace-rule errors, the SKILL.md inadvertently has Claude-tool refs (loop back to `tdd-criteria-skill-write` to clean). + +**Out of scope:** no Hermes-side install / activation (Hermes side handled by `hermes-mvp-coverage` task family separately). This task only ensures the converter knows about the skill. + +**Status:** done +**Where I stopped:** Shipped в `7d308ff feat(hermes): add tdd-criteria mapping (auto, software-development) + rebuild dist-hermes`. Build verified: `dist-hermes/software-development/tdd-criteria/SKILL.md` created. No replace-rules needed (pure policy skill). +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [tdd-criteria-build-install] — Build `dist/tdd-criteria.skill` archive, install to `~/.claude/skills/tdd-criteria/`, commit both `dist/` and `dist-hermes/` artefacts. Closes the rollout loop — after this task the skill is live for both Claude (next session) and Hermes (next factory deploy). + +**Steps:** +1. `bash ~/projects/claude-skills/scripts/install.sh tdd-criteria` — copies `skills/tdd-criteria/` → `~/.claude/skills/tdd-criteria/` (replaces if exists). Verifies skill is loadable in next Claude session. +2. `bash ~/projects/claude-skills/scripts/build.sh tdd-criteria` — zips `skills/tdd-criteria/` → `dist/tdd-criteria.skill`. Cross-platform: bash on Linux/macOS, delegates to PowerShell on Windows without `zip`. +3. `python ~/projects/claude-skills/scripts/build-hermes.py` — regenerates `dist-hermes/` (already done in `tdd-criteria-hermes-mapping`'s verification step, but re-run for clean state before commit). +4. Commit: `dist/tdd-criteria.skill` + `dist-hermes/software-development/tdd-criteria/` (whole tree). +5. Push (subject to project-discipline Rule 4 — ask user if no auto-push grant). + +**Verification:** +- `ls ~/.claude/skills/tdd-criteria/SKILL.md` exists. +- `ls ~/projects/claude-skills/dist/tdd-criteria.skill` exists, size > 0. +- `ls ~/projects/claude-skills/dist-hermes/software-development/tdd-criteria/SKILL.md` exists. +- Optional: in a fresh Claude session, ask «what skills do you have for TDD?» — `tdd-criteria` should surface. + +**Out of scope:** activation in specific projects (separate task `tdd-criteria-rollout-projects` if user wants to add `follow tdd-criteria` trigger to selected `CLAUDE.md`s). Skill is reachable via description-based pull regardless. + +**Status:** done +**Where I stopped:** Shipped в `62a54c9 build(tdd-criteria): add dist archive`. `install.sh` → `~/.claude/skills/tdd-criteria/`; `build.sh` → `dist/tdd-criteria.skill` (2862 bytes); `build-hermes.py` → `dist-hermes/software-development/tdd-criteria/SKILL.md`. All three artefacts verified on disk. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [tdd-criteria-review] — Code-review checkpoint для брейнсторма `tdd-criteria` (промоушен 2026-05-07). + +**Status:** done +**Where I stopped:** Review complete. 7-point checklist passed. 4 findings applied directly as v0.2.0 (trigger-loophole fix, composite-tasks + refactoring sections, file-extension expansion, wrapper line-count clarification, spike-survivor .tasks/ fallback, foreign-schema fix). 3 minor items deferred as wontfix (retroactivity marker — negligible risk; spike-survivor already has fallback; refactoring is now documented). SKILL.md 0.1.0→0.2.0, design doc amended, rebuilt + reinstalled + hermes rebuilt. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## ⚪ [tdd-criteria-precommit-hook] — Optional pre-commit hook script that automates the bright-line checks from `tdd-criteria` Anti-loophole rules 1 and 4. Project owner opts in per-repo by symlinking / copying to `.git/hooks/pre-commit` (or via `husky` / `lefthook` integration if the project uses them). + +**Two checks** (both bright-line, both fail-closed): + +**Check 1 — `[skip-tdd: ]` validation.** If `git diff --cached --name-only` includes a `*.ts` / `*.js` / `*.py` / similar code file (excluding tests and Permissive-zoned paths like `*.css` / `*.env*` / `*.md` / `*.yaml`), require either: +- A `*.test.*` / `*.spec.*` / `test_*.py` / similar file present in the same diff, OR +- The commit subject (read from `$1` arg, line 1 of `$1` = msg path) matches `\[skip-tdd: (visual|spike|oneshot|wrapper)\]`. + +If neither holds → block with message: +``` +TDD policy violation: code change without test or skip marker. +Add a test, or include [skip-tdd: ] in commit subject. +See claude-skills/.wiki/concepts/tdd-criteria-design.md for which category applies. +``` + +**Check 2 — Test-modification audit (`[test-modify]` rule 4).** Detect test-modifying changes in `git diff --cached`: +- Removed line matching `^-\s*(expect|assert|assertThat|chai\.)\(` (assertion deletion) +- Added/removed lines that change argument values inside `expect(...)` / `assert(...)` calls +- Added `.skip` / `\.xit\b` / `@pytest\.mark\.skip` / `@Disabled` / `@Ignore` annotations +- Deleted `it(...)` / `test(...)` / `def test_*` definitions (matches `^-\s*(it|test|describe)\(` or `^-def test_`) + +If any matched → require commit subject matches `\[test-modify: [^:]+: was .+; is .+; reason: .+\]`. Block otherwise with message: +``` +Test-modification without [test-modify: ...] marker. +Required format: [test-modify: : was ; is ; reason: ] +The was/is must be the LITERAL assertion expressions, not paraphrased. +See tdd-criteria Anti-loophole rule 4. +``` + +ALSO: if `git diff --cached --name-only` includes both a test-pattern file AND an impl-pattern file → block: +``` +Test changes must be in a separate commit from impl changes (tdd-criteria rule 4b). +Run: git reset HEAD && git commit (test-only) && git add && git commit (impl-only). +``` + +**Implementation:** +- Bash script (single file). Cross-platform: works on Linux/macOS and on Windows under git-bash (which Claude/Hermes both already run on). +- Path: `~/projects/claude-skills/scripts/tdd-criteria-precommit-hook.sh`. Plus a Windows wrapper `.ps1` that delegates if needed. +- Tested on a real repo before commit (`books` is a good candidate — it has Jest tests + `*.test.js` convention). +- Documented in `claude-skills/.wiki/concepts/tdd-criteria-design.md` «See also» section (already linked). + +**Activation pattern (per-repo):** +```bash +# Symlink or copy the hook +ln -sf ~/projects/claude-skills/scripts/tdd-criteria-precommit-hook.sh \ + .git/hooks/pre-commit +chmod +x .git/hooks/pre-commit +``` + +Or via `lefthook.yml` / `.husky/pre-commit` if the project already uses one of those. + +**Out of scope:** +- Mutation testing (Stryker, mutmut) — separate heavyweight infra, not this hook. +- Coverage gates — different mechanism, different cost/benefit. +- Visual regression infra (Percy, Chromatic) — Permissive-5 acknowledges this is heavyweight; not bundled. +- Auto-fixing the violation — hook only blocks; fix is human's job. + +**Why this is optional, not required by the SKILL.md:** + +The skill is **policy** that lives in claude-skills and gets pulled into agent context per project. The hook is **enforcement** that needs per-project setup. Some projects opt out of pre-commit hooks entirely (e.g. `karu` if it's pure CSS — no code surface to enforce). Forcing the hook into the skill would couple policy to tooling. + +**Bypass:** + +Pre-commit hooks have `--no-verify`. Per `project-discipline` Rule 4 («never skip hooks unless user explicitly asks») agents must not use `--no-verify` — but humans can in emergencies. Each `--no-verify` use should be self-flagged in the commit body («bypassed pre-commit because: ...»). Not enforceable by the hook itself; this is a higher-level audit. + +**Status:** ready +**Where I stopped:** (not started) +**Next action:** Решить, нужен ли hook (он опционален) — если да, написать `~/projects/claude-skills/scripts/tdd-criteria-precommit-hook.sh` по спеке в description, протестировать на `books` (Jest-конвенция `*.test.js`), задокументировать activation pattern. Если нет — закрыть как `wontfix` с пометкой что fence чисто социальная (commit subject visible в `git log`). +**Branch:** master + + +--- + +## 🟢 [bootstrap-add-tdd-trigger] — Добавить `tdd-criteria` в раскатку `project-bootstrap`: канонический триггер в шаблоне `CLAUDE.md` + запись в Step 5.6 (skill-deps check). Сейчас у `tdd-criteria` v0.1.0 есть Hermes-mapping (commit 7d308ff — авто-классификатор по домену software-development), но текстового триггера в раскатываемом `CLAUDE.md` нет — старые/новые проекты явного крючка для агента не получают, срабатывание только через классификацию. Паттерн повторяет коммиты d0a450c (`follow project discipline`, v1.5.0) и dcad950 (`pull remote before work`, v1.4.0). Версия `project-bootstrap` 1.9.0 → 1.10.0 (MINOR — новый канонический триггер). + +**Status:** done +**Where I stopped:** Shipped в `e566df4 feat(project-bootstrap): add tdd-criteria to canonical triggers [v1.10.0]`. Trigger-line `follow TDD criteria` добавлен в `assets/CLAUDE.md.template` + Step 5.6 trigger→fulfiller table. Version 1.9.0 → 1.10.0. +**Next action:** (none — kept until merged) +**Branch:** n/a + + +--- + +## 🟢 [bootstrap-upgrade-canonical-triggers] — Прогнать `project-bootstrap` v1.10.0 в upgrade-режиме на этом проекте. Цель — дописать в `CLAUDE.md` 2 недостающих канонических триггера: `follow tdd-criteria`, `recommend, don't menu`. Уже присутствуют: `pull remote before work`, `follow project discipline`, `delegate to interns when allowed`, `we're on Windows`. Аудит 2026-05-07 через `bulk_text_read`. Контекст — координатор `[bootstrap-rollout-canonical-triggers]` в `_meta` (commit dd933fd). Пилот на `books` подтвердил идемпотентность (158fc95). + +**Status:** done +**Where I stopped:** Shipped в `ac0fa57 chore(bootstrap): upgrade CLAUDE.md — add tdd-criteria, recommend-dont-menu triggers`. 2 файла изменены: CLAUDE.md (+2 triggers), manifest 1.2.0→1.10.1. Verify подтверждён — `git diff --stat` показывал 2 файла. Unblock готов — можно продолжить mass-unblock 5-string группы. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [claude-skills-update-skill] — Orchestrator-скил `update-claude-skills` — Claude-Code-сторона аналог `hermes-installer-skill`. Триггеры: «обнови claude-skills», «sync claude-skills», «update claude-skills», «обнови всё». На Windows + Claude Code (а также Linux/Mac) прогоняет в одну команду полный uplift существующей установки: + +**Status:** done +**Where I stopped:** Shipped в `627a183 feat(update-claude-skills): add update skill + scripts [v0.1.0]`. Bugfix в `0a8d8ac` — PS 5.1 Unicode crash (em dashes + box-drawing в ANSI-CP1251) + NullArray на Select-String miss. Scripts `scripts/update.{ps1,sh}` + SKILL.md v0.1.0 (wrapper, delegates to active-platform). Smoke verified: idempotent no-op on current machine. Hermes mapping: `mode: skip` (Claude-Code-only). Dist archive built + installed. +**Next action:** (none — kept until merged). Follow-ups: (1) `.factory/factory.yaml` post_install → `update.sh` — отдельный PR в factory; (2) `--prune` flag — tracked in `[install-ps1]`; (3) wiki doc `claude-skills-update-flow.md` — optional, can merge with `install-cross-platform.md`. +**Branch:** master + + +--- + +## ⚪ [bootstrap-fix-tdd-recommend-template] — Fix bug в `project-bootstrap@1.10.0`: CLAUDE.md template-литерал (lines 339-342 в `skills/project-bootstrap/SKILL.md`) содержит только 4 канонических триггера, отсутствуют `follow tdd-criteria` и `recommend, don't menu` — те, что v1.10.0 заявляла добавить. Соответствующие prose-paragraph (lines 351-385) тоже отсутствуют для этих двух. Step 5.6 (skill-deps map, lines 444-449) при этом полный — все 6 строк правильно мапятся. + +Симптом: upgrade-mode на репе, где отсутствуют `tdd-criteria` и `recommend-dont-menu` (например claude-skills, cancel-music-webstore, meeting-room, projects-meta-mcp), детектит «already canon» (потому что 4 template-строки уже на месте), не вставляет недостающие 2. + +Воспроизведено 2026-05-07 на `claude-skills`: `upgrade project` отрапортовал «CLAUDE.md — Already canon. All template triggers present; extras (follow tdd-criteria, recommend, don't menu) preserved» — но в реальности эти 2 строки отсутствуют (`Select-String` пусто, `bootstrap-manifest.md` остался на 1.2.0). + +Корень: bootstrap-add-tdd-trigger task (closed `7bde0cd`) была реализована **частично** — version bump + Step 5.6 update сделан, template-литерал и prose не тронуты. Скил остался в неконсистентном состоянии. + +**Status:** done +**Where I stopped:** Shipped в `aac9088 fix(project-bootstrap): v1.10.1`. Template + prose updated, 2 canonical triggers added. Upgrade tested on claude-skills — `ac0fa57` confirms 2-file change (CLAUDE.md +2, manifest 1.2.0→1.10.1). +**Next action:** (none — kept until merged) +**Branch:** master +**Next action:** 1) В `skills/project-bootstrap/SKILL.md`, lines 339-342 — добавить две строки в template-литерал в правильном порядке (соответствует канон-порядку из `books@5ca16d9`): + +``` +pull remote before work +follow project discipline +follow tdd-criteria ← NEW, после discipline +delegate to interns when allowed +recommend, don't menu ← NEW, после interns +we're on Windows +``` + +2) Добавить две prose-paragraph после существующих: + - **`follow tdd-criteria`** — после `follow project discipline` paragraph (~line 366). Текст по образцу соседних: что активирует, что делает, install hint, ссылка на `.wiki/concepts/tdd-criteria-design.md`. + - **`recommend, don't menu`** — после `delegate to interns when allowed` paragraph (~line 379). Текст: override `superpowers:brainstorming`, одна рекомендация вместо меню, install hint. + +3) Bump version frontmatter: `version: 1.10.0` → `1.10.1` (PATCH — bug fix, completing what 1.10.0 заявляла, но не доставила). + +4) Build + install: + ```powershell + pwsh ./scripts/build.ps1 project-bootstrap + pwsh ./scripts/install.ps1 project-bootstrap + ``` + +5) Verify: `head -5 ~/.claude/skills/project-bootstrap/SKILL.md` показывает `version: 1.10.1`. + +6) Commit: `fix(project-bootstrap): v1.10.1 — add missing tdd-criteria + recommend-dont-menu to CLAUDE.md template + prose` с body, объясняющим что 1.10.0 был частичным. + +7) **Test:** в этой же сессии (после reload-skill через rebuild) — `upgrade project` в `~/projects/claude-skills/`. Должен теперь увидеть 2 missing строки и предложить вставку. Прогнать. Получить 2-файловый коммит (CLAUDE.md +2 + bootstrap-manifest.md 1.2.0 → 1.10.1). Закрыть `[bootstrap-upgrade-canonical-triggers]` в этой же репе. + +8) `tasks_close` этой fix-таски + параллельно — после успешного теста — подтвердить в `.meeting-room` сессии, чтобы я мог продолжить mass-unblock 5-string группы и step 3 cleanup. +**Branch:** n/a + + +--- + +## ⚪ [tasks-board-cleanup-2026-05] — Архивная чистка `.tasks/STATUS.md`: переместить все 🟢 done-блоки в `.tasks/.archive/done-2026-05.md`, оставить в STATUS.md только header + status legend + 🔴/🟡/⚪/🔵 блоки. Цель — разгрузить файл от исторического шума без потери data (git history + явный архивный файл для текстового поиска). + +Предпосылка: на 2026-05-07 в `claude-skills/.tasks/STATUS.md` накопилось ~18+ 🟢 done-блоков (rolled out за 2026-04-25 — 2026-05-07: tdd-criteria-rollout, hermes-rollout, bootstrap-related, project-creation-lifecycle, recommend-dont-menu и т.д.). Convention в шапке файла говорит «🟢 Done — kept until merged», но проект работает на master-only (нет ветвления для merge), поэтому convention превратилась в «kept forever». Файл рос до ~600 строк / 26K токенов — Read-инструменты упираются в лимит. + +После чистки STATUS.md остаётся только активный board (🔴/🟡/⚪/🔵 + closed `[bootstrap-upgrade-canonical-triggers]` если уже отработал). Архивный файл `.tasks/.archive/done-2026-05.md` сохраняет полный текст всех перемещённых блоков для grep-поиска и historical context. + +**Status:** ready +**Where I stopped:** Создана 2026-05-07. После rollout v1.10.0/1.10.1 на STATUS.md накопилось много свежих 🟢 (bootstrap-add-tdd-trigger, refresh-project-bootstrap closed-as-superseded, bootstrap-fix-tdd-recommend-template, bootstrap-upgrade-canonical-triggers если уже отработал). Хороший момент для batch-archive. +**Next action:** 1) Прочитать `.tasks/STATUS.md` целиком (Read in chunks if needed). 2) Идентифицировать все блоки `## 🟢 [...]` — разделители `---`. 3) Создать `.tasks/.archive/` директорию (если нет). 4) Записать `.tasks/.archive/done-2026-05.md` с шапкой: + +```markdown +# Archived — Done batch 2026-05 + +Перемещено из `.tasks/STATUS.md` 2026-05-07 в рамках board-cleanup. Полный список 🟢 done-тасок, шипанутых в апреле-мае 2026. + +Полный source — git history `.tasks/STATUS.md` до commit X. + +--- +``` + +И подряд все 🟢-блоки в исходном виде. + +5) Edit `.tasks/STATUS.md`: убрать все 🟢-блоки + соседние `---` разделители. Header + status legend + 🔴/🟡/⚪/🔵 блоки оставить. 6) Verify: `grep '^## ' .tasks/STATUS.md | wc -l` — было ~28+, должно остаться число активных (🔴+🟡+⚪+🔵, ожидание ~10-12). 7) Commit `meta(tasks): archive done batch 2026-05 → .tasks/.archive/done-2026-05.md` с body, описывающим какие группы перемещены (tdd-criteria, hermes, bootstrap, etc.) и итоговый count. 8) `tasks_close`. + +NB: эта таска сама уйдёт в следующий cleanup batch (2026-06 или подобный), так что не пытаться её закрыть+архивировать в одном коммите. +**Branch:** n/a + + +--- + +## 🟢 [setup-interns-clone-fallback] — Bug в `setup-interns`: когда `~/projects/.common/lib/interns-mcp/` отсутствует, скил НЕ ensure-ит `.common`-репу из gitea, а предлагает «initialize from scratch» (создать stub-сервер). Stub-сервер не связан с production-кодом → разрыв со всем флотом. + +Воспроизведено 2026-05-07 на свежей Windows-машине (`D:\projects\stostayer.new`): юзер сказал «настрой interns» → агент: «`.common/lib/interns-mcp/` not found, хочешь инициализировать?». Правильный ответ — ensure `.common` репа клонирована/свежая, **не initialize**. + +**Architecture clarification:** `interns-mcp` — это поддиректория `.common/lib/interns-mcp/` внутри **одной общей репы `common`** (Gitea: `OpeItcLoc03/common`). Не отдельная репа. Все MCP-серверы (`projects-meta-mcp`, `interns-mcp`, и будущие) живут субдиректориями в `.common/lib/`. Это decision из таски `[migrate-to-common-lib]` (закрыта, line 127-134 в `projects-meta-mcp/.tasks/STATUS.md`). + +Pattern для починки в `setup-interns/SKILL.md`: +- Phase 1 (source detection): если `~/projects/.common/lib/interns-mcp/` отсутствует, проверить существование `~/projects/.common/` как git-репы. +- Если `.common/` есть как git-репа, но `lib/interns-mcp/` отсутствует — `git pull --ff-only` в `.common` (стылый клон, нужны свежие subdirs). +- Если `.common/` отсутствует целиком — `git clone {gitea}/common.git ~/projects/.common/` (с extraheader-auth pattern для PAT-fallback). +- После ensure source — продолжить нормальный flow (pip install -e + secrets + register). + +Аналогичный fix потенциально нужен и в `setup-projects-meta` — оно (вроде) уже умеет клонировать, но мог быть тот же gap. Стоит проверить параллельно. (Раньше предполагалось, что setup-projects-meta это покрывает — нужно подтвердить чтением SKILL.md.) + +**Связь с `[setup-interns-fix-paths]` (⚪ ready, line 81):** там фикс cwd-relative → absolute paths. Этот фикс делать после или вместе. + +**Bonus discovery:** `.factory/factory.yaml` lines 41-46 объявляет `interns-mcp` как **отдельный component с url `{git_host}/{git_org}/interns-mcp`** — это **тоже неверно** (репа interns-mcp на gitea не существует, только subdir в `.common`). Стоит завести отдельную fix-таску в `.factory` (не в этой репе) на корректную манифест-схему — либо source как path внутри другой репы, либо component definition пересмотреть. + +**Status:** done +**Where I stopped:** Shipped в `54ba5ca` + `90d066b`. v0.3.0: replaced "stop — initialize first" with clone-fallback from `OpeItcLoc03/common` monorepo (NOT a separate `interns-mcp` repo — it's a subdirectory). Phase 1 checks `~/projects/.common/.git` existence; Phase 4 does `git pull --ff-only` (stale clone) or `git clone .../common.git` (fresh). Path fix (cwd-relative → absolute `~/projects/.common/...`) applied simultaneously. Common mistakes + Out of scope updated for monorepo. Rebuilt + installed. +**Next action:** 1) Делать вместе с `[setup-interns-fix-paths]` (path-fix предшествует или идёт parallel). 2) В `skills/setup-interns/SKILL.md` найти Phase 1 (source detection / bailout). 3) Заменить bailout на ensure-`.common`-pattern: проверить `~/projects/.common/`, если git-репа есть — `git pull --ff-only`, если нет — `git clone {gitea}/common.git ~/projects/.common/`. 4) Auth-fallback: при clone провале — print инструкцию по gitea-PAT (как в `.factory/bootstrap.ps1` lines 132-141). 5) Verify после clone/pull: `~/projects/.common/lib/interns-mcp/pyproject.toml` существует. 6) **Update SKILL frontmatter `description:`** — текущая формулировка «clones the repo to `~/projects/.common/lib/interns-mcp/`» двусмысленна (читается как «клонирует отдельную interns-mcp репу»). Заменить на: «ensures `~/projects/.common/` is cloned (single Gitea `common` repo, `interns-mcp` lives as `lib/interns-mcp/` subdir), pulls if stale, then `pip install -e` and configures». Та же поправка нужна для `setup-projects-meta` description (см. шаг 9). 7) Bump version: 0.2.0 → 0.3.0 (MINOR — новая capability + clarified description). 8) Build + install: `pwsh ./scripts/build.ps1 setup-interns ; pwsh ./scripts/install.ps1 setup-interns`. 9) **Параллельно:** прочитать `setup-projects-meta/SKILL.md` Phase 1 + frontmatter description, проверить идентичный pattern + clarity. Если разрыв — починить вместе. 10) Smoke-test: удалить `.common/` локально → `настрой interns` → должно clone .common + install + register. 11) `tasks_close`. **NB:** одной операцией смежно может быть закрытие `[setup-interns-fix-paths]`. **Связь:** `[factory-yaml-mcp-subdir-schema]` в `factory` — фикс той же ошибки в L1 manifest. Желательно skil-fix и manifest-fix выкатить в один день, чтобы не было трещины между описаниями. +**Branch:** master + +Воспроизведено 2026-05-07 на свежей Windows-машине (`D:\projects\stostayer.new`): юзер сказал «настрой interns» → агент: «`.common/lib/interns-mcp/` not found in `D:\projects\stostayer.new`. Хочешь, чтоб я инициализировал?». Правильный ответ — clone из gitea, не initialize. + +Pattern для починки уже отработан в `setup-projects-meta`: при отсутствии source клонирует из gitea (см. его SKILL.md, та же фаза), затем продолжает pip install + secrets + register. `setup-interns` эту feature не получил при первой реализации. + +**Связь с `[setup-interns-fix-paths]` (⚪ ready, line 81 в STATUS.md):** там фикс cwd-relative → absolute `~/projects/.common/...`. Этот фикс **делать после** или **вместе с** ним. Иначе clone пойдёт в неправильный относительный путь. + +**Status:** ready +**Where I stopped:** Created 2026-05-07 после incident на stostayer.new. Setup-projects-meta уже имеет clone-fallback (отрабатывал на field-test 2026-05-06 на новом ноуте), setup-interns эту feature не получил при первой реализации (`0eb7dd1 feat(interns-skills-mvp): ship setup-interns + using-interns v0.1.0`). +**Next action:** 1) Делать вместе с `[setup-interns-fix-paths]` (path-fix предшествует или идёт parallel). 2) В `skills/setup-interns/SKILL.md` найти Phase 1 (source detection / bailout). 3) Заменить bailout на ensure-`.common`-pattern: проверить `~/projects/.common/`, если git-репа есть — `git pull --ff-only`, если нет — `git clone {gitea}/common.git ~/projects/.common/`. 4) Auth-fallback: при clone провале — print инструкцию по gitea-PAT (как в `.factory/bootstrap.ps1` lines 132-141). 5) Verify после clone/pull: `~/projects/.common/lib/interns-mcp/pyproject.toml` существует. 6) **Update SKILL frontmatter `description:`** — текущая формулировка «clones the repo to `~/projects/.common/lib/interns-mcp/`» двусмысленна (читается как «клонирует отдельную interns-mcp репу»). Заменить на: «ensures `~/projects/.common/` is cloned (single Gitea `common` repo, `interns-mcp` lives as `lib/interns-mcp/` subdir), pulls if stale, then `pip install -e` and configures». Та же поправка нужна для `setup-projects-meta` description (см. шаг 9). 7) Bump version: 0.2.0 → 0.3.0 (MINOR — новая capability + clarified description). 8) Build + install: `pwsh ./scripts/build.ps1 setup-interns ; pwsh ./scripts/install.ps1 setup-interns`. 9) **Параллельно:** прочитать `setup-projects-meta/SKILL.md` Phase 1 + frontmatter description, проверить идентичный pattern + clarity. Если разрыв — починить вместе. 10) Smoke-test: удалить `.common/` локально → `настрой interns` → должно clone .common + install + register. 11) `tasks_close`. **NB:** одной операцией смежно может быть закрытие `[setup-interns-fix-paths]`. **Связь:** `[factory-yaml-mcp-subdir-schema]` в `factory` — фикс той же ошибки в L1 manifest. Желательно skil-fix и manifest-fix выкатить в один день, чтобы не было трещины между описаниями. +**Branch:** n/a + + +--- + +## 🟢 [update-using-projects-meta-qualified-names] — Обновить SKILL.md скила `using-projects-meta`: примеры `target_project` переписать на qualified формат (`/` вместо bare-имени). Cross-reference на concept `projects-meta-multi-owner` в common wiki. Часть миграции multi-owner. + +**Status:** done +**Where I stopped:** SKILL.md already reflects multi-owner schema (qualified target_project examples, agenda literal, bare-name rejection in common-mistakes table) +**Next action:** (none — kept until merged) +**Blocker:** multi-owner-tools-mutate +**Branch:** n/a + + + + +--- + +## 🟢 [update-setup-projects-meta-auth-toml] — Обновить SKILL.md скила `setup-projects-meta`: новая структура auth.toml — `gitea_owners = [...]` массив, `agenda_tasks_repo` qualified имя. Backwards-compat note: старый `gitea_user = "X"` без `gitea_owners` читается как `gitea_owners = ["X"]`. Часть миграции multi-owner (см. concept `projects-meta-multi-owner` в common wiki). + +**Status:** done +**Where I stopped:** SKILL.md already reflects multi-owner schema (gitea_owners array, agenda_tasks_repo qualified, backwards-compat note) +**Next action:** (none — kept until merged) +**Blocker:** multi-owner-config +**Branch:** n/a + + + + +--- + +## 🟢 [meta-isolation-bootstrap-skill-update] — Обновить `project-bootstrap` skill чтобы для своих новых проектов он создавал локальный `.gitignore` с `!`-инверсиями мета-путей. Без обновления любой новый bootstrap'нутый проект ломается сразу: `.tasks/`, `.wiki/`, `.claude/` создаются, но git их не видит из-за глобального `core.excludesFile`, и в первый коммит они не попадают. + +**Источник:** `.workshop/.wiki/concepts/meta-out-of-repo.md` — секции «Слой 2» и «Новые проекты». + +**Что добавить в скил `project-bootstrap`:** + +При создании / upgrade своего нового проекта (greenfield mode), после `git init` и до первого `git add`, создать или дополнить локальный `.gitignore` блоком: + +``` +# AI обвеска — слой 2: переопределяем глобальный ~/.config/git/ignore +# для своих репо (см. global wiki concept meta-out-of-repo) +!.claude/ +!.tasks/ +!.wiki/ +!.brainstorm/ +!.archive/ +!.mcp/ +!.mcp.json +!MEMORY.md +``` + +**Кейсы:** +- Greenfield новый свой проект → класть блок безусловно. +- Upgrade существующего своего проекта (репо уже есть) → проверить, есть ли блок; если нет — append; если есть — skip. +- Bootstrap внутри форка чужого репо → **не класть** (если такой режим вообще нужен — обсудить отдельно). По concept'у: «Слой 2 только для своих». + +**Версия:** bump MINOR по правилу `project-discipline` Rule 3 (новая фича, обратно совместимо). Записать в commit message. + +**Сопутствующие скилы для проверки:** `setup-tasks`, `setup-wiki`, `setup-projects-meta` — тоже могут трогать `.gitignore` или ожидать что мета-пути видны git'у. Если они сейчас работают «и так» — апдейт в них не нужен; если падают на untracked мета-путях — починить вместе с этим. + +**Acceptance:** после bootstrap'а тестового greenfield-проекта в `/tmp/` (или `%TEMP%\`): +1. `.gitignore` содержит блок инверсий +2. `touch .tasks/_smoke.md && git status` показывает файл как untracked (а не скрытым) +3. Первый коммит включает `.tasks/`, `.wiki/`, `.claude/` файлы созданные скилом + +**Не делать:** не клади инверсии безусловно, без проверки что цель — свой репо. В чужих форках это потенциально создаст расхождение с upstream'ом (новый файл в `.gitignore`). Если bootstrap отрабатывает только для своих новых проектов — этот edge-case не релевантен; уточнить в коде скила. + +**Status:** done +**Where I stopped:** v1.11.0 shipped + installed. Template carries meta-isolation block; SKILL.md Step 1 documents greenfield (template) + upgrade-case marker-append. Smoke test on `%TEMP%\test-bootstrap-meta-iso` passed all 3 acceptance + negative control + idempotency. Wiki: `concepts/project-bootstrap-meta-isolation.md` + index + log entries. +**Next action:** (closed) +**Branch:** master + + + +--- + +## 🟢 [using-synology-ops-install] — Install `using-synology-ops` skill (skeleton) into the local Claude Code skill registry so it activates in new sessions. + +The SKILL.md was committed locally to `~/projects/claude-skills/skills/using-synology-ops/SKILL.md` by the promotion run on 2026-05-12 (commit fb4ef65), but the file lives in the repo only — Claude Code reads skills from `~/.claude/skills/`. Without install the skill is invisible. + +**Status:** done +**Where I stopped:** (done 2026-05-12) — installed via `pwsh scripts/install.ps1 -Names using-synology-ops` → `C:\Users\vitya\.claude\skills\using-synology-ops\SKILL.md`. SHA256 dst==src (`DD0CE0F8…CB7D50`). Skill registry refresh не понадобился — `using-synology-ops` появился в available-skills list текущей сессии сразу после copy. /reload-plugins не запускался. +**Next action:** (none — kept until merged) +**Branch:** n/a + + + +--- + +## 🟢 [using-synology-ops-hermes-mapping] — Register `using-synology-ops` in `~/projects/claude-skills/hermes/mapping.yaml` so the hermes-agent (chat-agent for non-CC environments) knows how to map intent to this skill. + +Mode: `auto` — skill is purely behavioral (trigger-and-guidance for an MCP server), does not modify local environment or require permission grants. + +**Status:** done +**Where I stopped:** (done 2026-05-12) — mapping.yaml entry добавлен (mode:auto, category:mcp, без replace-rules — skill behavior одинаков на CC и Hermes). `build-hermes.py` → 25 skills processed (auto 14, manual 2, skip 9, pending 0). `dist-hermes/mcp/using-synology-ops/SKILL.md` присутствует, SHA256 совпадает с источником (passthrough без правил). NB: rebuild также вытащил unrelated LF→CRLF diff'ы у `using-projects-meta` + `project-bootstrap` dist'ов — это побочный эффект build-hermes.py под Windows (Python пишет system-newline). Эти diff'ы НЕ закоммичены этой таской — отдельная задача на `.gitattributes` (`*.md text eol=lf`). +**Next action:** (none — kept until merged) +**Branch:** n/a + + + +--- + +## 🟢 [using-synology-ops-test-trigger] — Behavioral smoke-test for `using-synology-ops` trigger phrases. Verify the skill activates on its declared triggers AND does NOT activate on close-but-not-NAS phrases (false-positive check). + +Sub-task: after first real-world usage, tune triggers if false-pos / false-neg observed (e.g. skill misfires on "modulair-rag deploy scheduled tomorrow" — non-incident mention should be skipped). + +**Status:** done +**Where I stopped:** (closed 2026-05-12 — 10/10 PASS after 1 tuning pass; see close-note below) + +**Static-pass findings:** +- `description` length = **948 chars** (под 1024 hard-limit harness, чуть над 900 safe). Свободного запаса на новые триггеры ≈75 chars. +- Bare `synology-ops` отсутствует в NAS-domain words (есть только `Synology` и container `synology-ops-mcp`). EN pos #1 «synology-ops show me the modulair stack» полагается на stem-match — поведение зависит от LLM. +- Дискриминатор «when NAS-context» для incident-phrases — неявный. NEG #3 «restart-loop in books-pipeline» может false-fire если LLM ключится на `restart-loop` без проверки хоста. +- Skip-rule «non-incident container mentions» — формально есть для NEG #1, эффективность видна только в behavioral тесте. + +**Behavioral fire-test (юзер, в свежей сессии после `/exit` + new session):** + +POSITIVE (skill ДОЛЖЕН активироваться): +- RU: `посмотри что с modulair-pipeline` +- RU: `опс мсп проверь стек` +- RU: `контейнер lightrag-modulair падает` +- RU: `что у нас на NAS с tier1-converter` +- EN: `synology-ops show me the modulair stack` ← stem-match risk +- EN: `what's wrong with tier1-converter` +- EN: `check restart-loop on modulair-mcp` + +NEGATIVE (skill НЕ должен активироваться): +- `modulair-rag deploy scheduled tomorrow` ← skip-rule vs container trigger +- `open Portainer GUI and check logs` +- `restart-loop in books-pipeline` ← NAS-context disambiguator + +Tuning-кандидаты по результатам fire-test'а (фиксить только если соответствующий тест fail'нулся): +- Если EN pos #1 fail (false-neg) — добавить bare `synology-ops` в NAS-domain words. +- Если NEG #3 false-fire — расширить skip-list: «non-NAS hosts (vps-books, etc.)». +- Если NEG #1 false-fire — усилить skip-rule «non-incident container mentions» (примеры в скобках). + +После каждой правки `description`: `pwsh scripts/install.ps1 -Names using-synology-ops` → новая чистая сессия → повторить fail'нутый тест. + +Что хочу видеть в close-note: 10/10 expected results, или «N тестов fail, починены через X правок в description, версия 0.1.0 → 0.1.1 / 0.2.0». + +**Next action:** (none — kept until merged) +**Branch:** n/a + + + +--- + +## ⚪ [using-synology-ops-review] — Skill-review checkpoint для using-synology-ops (промоушен 2026-05-12). + +**Источник дизайна:** .workshop/.archive/2026-05-12-using-synology-ops-skill.md (process trace дискуссии Q1–Q6) + .wiki/concepts/synology-ops-mcp-design.md в shared projects-wiki (canonical design самого MCP-сервера). +**Импл-таски:** using-synology-ops-install, using-synology-ops-hermes-mapping, using-synology-ops-test-trigger. + +**Кто делает:** **не имплементер.** Другая сессия / другой день / другой агент. Identity-not-location: ревьюер работает в любой папке, где есть доступ к файлам (см. .workshop/.wiki/concepts/workshop-architecture.md §5.1 если есть). + +**Поведенческий smoke-test (это и есть acceptance):** +- Скил активируется в чистой сессии на каждой триггер-фразе из `description` (русский И английский варианты — список в test-trigger таске). +- Скил **не** активируется на 2-3 близких но не своих фразах из соседних доменов (false-positive check; список в test-trigger таске). +- Каждый шаг секции `Steps` отрабатывает на тестовом буфере без ошибок (NB: после второго прохода body, см. ниже). +- `Failure modes` уводят в abort, не в частичный успех с грязным состоянием. +- `What NOT to do` соответствует реальности — нет дыры между правилом и реализацией. + +Findings — обычные follow-up tasks (`using-synology-ops--fix` или подобное) через `tasks_create` в `claude-skills`. + +**Закрытие:** только когда все findings зафайлены ИЛИ ревьюер подтвердил «нет findings» в close-note. + +**NB по семверу:** `version: 0.1.0` записан промоутером. Дальнейшие инкременты — ответственность владельца `claude-skills/`, **не** этого скила и не ревьюера. Если ревью требует правок — правит владелец, бампит он же. + +**NB по body:** на момент создания таски body SKILL.md — пустой каркас (When to use / Inputs / Steps / Failure modes / Side effects / What NOT to do все пустые). Второй проход «доведём using-synology-ops» дописывает их глазами из archive + concepts/synology-ops-mcp-design.md. Если этот проход не сделан до review — close-note: "skill каркас, body пустой, перенесите review", это не findings. + +**Status:** ready +**Where I stopped:** (not started — baseline-таски все 🟢 на 2026-05-12, version теперь 0.1.1, body всё ещё <пусто> stub) +**Next action:** Ревьюер: прогнать поведенческий smoke-test в чистой main-сессии (subagent fire-test уже сделан имплементером — см. close-note test-trigger; нужен gold-standard прогон). Body пустой → по NB-правилу: "skill каркас, body пустой, перенесите review" или закрыть с findings про body-gap. Findings → follow-up tasks через `tasks_create`. +**Branch:** n/a + + +--- + +## 🟢 [using-vds-ops-install] — Установить using-vds-ops в Claude Code. После локального коммита `49f6532` в `OpeItcLoc03/claude-skills` скил лежит в tree, но не задеплоен (нет push, install.sh не запущен, /reload-plugins не сделан). Acceptance: в новой чистой сессии скил активируется на VDS-триггер-фразе из description. + +**Источник дизайна:** `OpeItcLoc03/vds-ops-mcp/.wiki/concepts/vds-ops-mcp-design.md` §6 «Скилл using-vds-ops». + +**Status:** done +**Where I stopped:** Закрыто 2026-05-21. Commit 49f6532 уже был запушен. `bash scripts/install.sh using-vds-ops` → установлен в `~/.claude/skills/using-vds-ops/`. Скилл виден в available-skills list. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [using-vds-ops-hermes-mapping] — Добавить запись для `using-vds-ops` в `~/projects/claude-skills/hermes/mapping.yaml`. + +**Mode = `pending`** (НЕ `auto`). Обоснование (из spec workshop-promote-brainstorm §7): «mode=`pending` если скил трогает инструменты или окружение». Этот скил вызывает `mcp__vds-ops__*` tools (read-only, но всё равно tools), → требует отдельного аудита перед auto-routing. + +**Источник дизайна:** `OpeItcLoc03/vds-ops-mcp/.wiki/concepts/vds-ops-mcp-design.md` §6. +**Прецедент:** проверить как сделана запись для sibling `using-synology-ops` в том же `hermes/mapping.yaml` — повторить структуру 1:1 с заменой триггеров. + +**Status:** done +**Where I stopped:** Закрыто 2026-05-21. Добавлен блок `using-vds-ops` в `hermes/mapping.yaml` с `mode: pending`. Build-hermes.py подтверждён (27 skills: 14 auto / 2 manual / 9 skip / 2 pending). Commit `d84a0d3` feat(hermes): add using-vds-ops mode=pending. +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [using-vds-ops-test-trigger] — Behavioral trigger smoke-test для `using-vds-ops`. Проверить что description-контракт активации работает корректно: positive на VDS-фразах, negative на близких НЕ-VDS фразах (false-positive check), + ambiguity resolution на `traefik` (есть и на NAS, и на VDS). + +**Источник триггеров:** `~/projects/claude-skills/skills/using-vds-ops/SKILL.md` (description field, commit `49f6532`). +**Прецедент-test-set:** аналогичная проверка для `using-synology-ops` (если есть фиксированный набор — взять оттуда; иначе сформулировать здесь). + +**Status:** done +**Where I stopped:** Закрыто 2026-05-21. Behavioral smoke-test проведён в новой сессии. 7/7 PASSED: +- Positive (1-3): all activated using-vds-ops ✅ +- Negative (4-5): using-synology-ops for NAS, none for no-context ✅ +- Ambiguity (6-7): registry → VDS (correct), traefik → disambiguation asked ✅ + +**Next action:** (none — kept until merged) +**Branch:** master + + +--- + +## 🟢 [using-vds-ops-review] — Skill-review umbrella checkpoint для `using-vds-ops` (промоушн 2026-05-21 из `OpeItcLoc03/vds-ops-mcp/.wiki/concepts/vds-ops-mcp-design.md` §6). + +**Текущее состояние скила:** скелет — только шапка + 6 пустых секций (When to use / Inputs / Steps / Failure modes / Side effects / What NOT to do). Commit `49f6532` в `OpeItcLoc03/claude-skills`. **Body fill-in — отдельный последующий проход**, не часть этого ревью. + +**Импл-таски (blocker'ы — должны быть 🟢):** +- `using-vds-ops-install` — задеплоен и активируется в новой сессии +- `using-vds-ops-hermes-mapping` — добавлен с mode=pending +- `using-vds-ops-test-trigger` — triggers smoke-test проведён + +**Кто делает: не имплементер.** Другая сессия / другой день / другой агент с чистым контекстом. Identity-not-location: ревьюер может быть в любой папке (этот ревью — про скил в `~/projects/claude-skills/`, не про доменный проект). + +**Поведенческий smoke-test (= acceptance) — на текущем skeleton-state:** +- Activation: скил активируется на каждой VDS-фразе из description (русский + английский). +- False-positive: НЕ активируется на близких но НЕ-VDS фразах (см. test-trigger таску). +- Ambiguity: `traefik` без host-контекста — disambiguation handled, не silent activation. +- Hermes-mapping: `mode: pending` явно прописан (не accidentally auto). +- Description sanity: триггеры в SKILL.md совпадают с тем что в hermes/mapping.yaml (нет drift). + +Findings — отдельные follow-up tasks (`using-vds-ops--fix` или подобное) через `tasks_create` в `OpeItcLoc03/claude-skills`. + +**Закрытие:** все findings зафайлены ИЛИ ревьюер подтвердил «нет findings» в close-note. + +**NB по семверу:** `version: 0.1.0` — промоутер. Дальнейшие инкременты — владелец `claude-skills/` (не этот скил, не ревьюер). Если ревью требует правок скила — правит владелец, бампит он же. + +**NB:** body-секции пустые by design. Ревью НЕ требует их заполнения. Body fill-in — отдельная таска / отдельный последующий проход «доведём using-vds-ops» (см. workshop-promote-brainstorm spec §6 «Проход второй»). + +**Status:** done +**Where I stopped:** Re-closed 2026-05-21 fresh-eyes pass via general-purpose subagent (clean context, no impl-priming). Verdict: ✅ PASS — no findings on 5 acceptance dimensions: +- (1) Activation coverage ✅ — RU+EN triggers cover 3 positive smoke phrases +- (2) False-positive ✅ — explicit "Skip for ..." carve-out (NAS modulair-* / write-ops / no-incident) +- (3) Ambiguity ✅ — explicit `traefik` disambiguation clause (stronger than NAS sibling) +- (4) Hermes mapping ✅ — `mode: pending`, intended mirrors `using-synology-ops` +- (5) Description ↔ mapping drift ✅ — no drift + +2 informational notes вне 5 dimensions (filed as ⚪ siblings, не блокеры): +- description length 1473 chars vs ≤900 char limit recorded in MEMORY — empirically works, requires investigation → `using-vds-ops-description-length-investigate` +- sibling `using-synology-ops` lacks explicit disambiguation clause (VDS skill stronger here) → `using-synology-ops-disambiguation-uplift` + +**Next action:** (none — kept until merged) +**Branch:** master + + + + +--- + +## ⚪ [using-vds-ops-description-length-investigate] — Investigate description length vs MEMORY ≤900 char limit. Drift between memory feedback и реальным harness behavior? + +**Observed (2026-05-21, fresh-eyes review of using-vds-ops):** description field в `skills/using-vds-ops/SKILL.md` = **1473 chars**. MEMORY entry `feedback_skill_description_length_limit.md` декларирует «hard limit ~1024 chars; превышение → harness тихо отбрасывает description и подставляет H1; держать ≤900 chars». При этом 7/7 behavioral smoke-test PASSED, и hermes/mapping.yaml корректно обрабатывает скил с этим description — empirically работает. + +**Гипотезы:** +- (a) Harness limit увеличился / снят — memory note устарел. Проверить current behavior в `available-skills` listing: full description vs H1-fallback. +- (b) Limit зависит от downstream consumer (Claude vs Gemini vs Copilot). VDS skill пока тестировался только на CC — может Gemini его дропнет. +- (c) MEMORY note был paranoid — реальный limit выше, но best-practice держать ≤900 всё равно (compression discipline). + +**Acceptance:** +- Замер реального текущего harness limit (написать тестовый skill с известной длиной description, инкрементировать, смотреть когда listing fallback'нет). +- Обновить или удалить MEMORY entry `feedback_skill_description_length_limit.md` в зависимости от результата. +- Если limit реально низкий — открыть `using-vds-ops-description-compress` follow-up (compress 1473 → ≤900 без потери triggers). + +**Status:** ready +**Where I stopped:** (not started) +**Next action:** написать пустой skill `skills/.test-description-length/SKILL.md` с description progressively dlinning, прогонять `/reload-plugins`, смотреть харнес listing. Document findings → update memory. +**Branch:** (not started) + + +--- + +## ⚪ [using-synology-ops-disambiguation-uplift] — Add explicit disambiguation clause to `using-synology-ops` description (sibling parity with `using-vds-ops`). + +**Observed (2026-05-21, fresh-eyes review of using-vds-ops):** VDS skill description содержит explicit clause `Disambiguation: traefik exists on both VDS and NAS — ask «на VDS или на NAS?» before tool call when ambiguous.` NAS skill (`using-synology-ops`) такого clause не имеет, хотя проблема симметричная — `traefik` есть на обоих хостах. + +**Acceptance:** +- `skills/using-synology-ops/SKILL.md` description обновлён: добавить mirror-clause «Disambiguation: `traefik` exists on both NAS and VDS — ask "на NAS или на VDS?" before tool call when ambiguous.» +- bump version (PATCH — wording / clarity без поведенческого изменения). +- `scripts/install.ps1 -Names using-synology-ops` пройден. +- Behavioral smoke-test на двух новых сессиях: «traefik unhealthy» в чистой сессии должна вызвать disambiguation, не silent activation одного скила. + +**Status:** ready +**Where I stopped:** (not started) +**Next action:** edit SKILL.md description + version bump + reinstall + smoke. Заодно посмотреть нет ли других shared names (`portainer`? `postgres` — где он живёт?). +**Branch:** (not started) + + +--- + +## 🟢 [interns-grep-audit-skill-updates] — Routing-добавка в `using-interns/SKILL.md` под новый детерминированный интерн `grep_audit`. Параллельная задача к `[interns-grep-audit-impl]` в `.common` — skill-edit сам по себе шипится независимо, e2e check после имплементации. + +**Design:** `.wiki/concepts/interns-grep-audit-design.md` §«Layer 3 — skill update». + +**Scope:** +1. В `using-interns/SKILL.md` секция routing-подсказок — добавить 3 строки: + - `grep_audit` — детерминированный аудит N×M, без LLM-вызова, zero cost, zero hallucination boundary. Использовать для матриц contains/not-contains (CLAUDE.md / SKILL.md / frontmatter audits). + - `bulk_text_read` vs `grep_audit` — Q&A с LLM vs детерминированный contains. Семантический матч — это `bulk_text_read` с вопросом, не `grep_audit`. + - Always-ask paths применяются единообразно (server открывает файл даже без LLM-вызова). +2. Bump `using-interns` version MINOR (0.X.Y → 0.(X+1).0 — новая capability в routing-таблице). +3. Rebuild + install через `scripts/install.ps1 -Names using-interns` (см. existing pattern в STATUS.md закрытых тасок). +4. Verify `version: 0.(X+1).0` в `~/.claude/skills/using-interns/SKILL.md` после reinstall. + +**Hermes mirror:** на 2026-05-22 `hermes/skills/using-interns-hermes/` не существует (проверено). Если за время промоушена hermes-зеркало для `using-interns` будет создано — обновить routing симметрично; иначе skip. + +**Acceptance:** +- SKILL.md содержит 3 строки routing про `grep_audit`. +- version bumped MINOR, рамку semver Rule 3 из `project-discipline` соблюдена. +- dist + install pass. + +**Reviewer:** см. `[interns-grep-audit-review]` umbrella. + +**Status:** done +**Where I stopped:** done — routing block landed, skill installed at v0.3.0 +**Closed by:** SKILL.md got 3 routing-table rows per design page §«Layer 3» (grep_audit deterministic claim, `bulk_text_read` vs `grep_audit` boundary, always-ask uniform reminder); plus consistency adds — Overview catalog table row, Overview prose note «first LLM-free intern in the catalog», Tool quick reference row with `paths`/`patterns`/`output`/`case_sensitive` signature. Version bumped 0.2.2 → 0.3.0 (MINOR — new routing capability). Rebuild + install via `scripts\install.ps1 -Names using-interns` (Windows PS); installed copy at `~/.claude/skills/using-interns/SKILL.md` shows `version: 0.3.0` and 6× `grep_audit` mentions (1 catalog + 3 routing + 1 quick-ref + 1 prose). Hermes mirror absent (`hermes/skills/using-interns*` empty per Glob) — skip per task scope. +**Next action:** (none — kept until merged); unblocks `[interns-grep-audit-review]` (other blocker `[interns-grep-audit-impl]` lives in `OpeItcLoc03/.common`, status TBD). +**Branch:** master + + + +--- + +## 🟢 [interns-grep-audit-review] — Code-review checkpoint для брейнсторма `interns-grep-audit` (промоушен 2026-05-22 из `.workshop/.brainstorm/interns.md` — partial-extract, living-catalog продолжает жить). + +**Спецификация:** `.wiki/concepts/interns-grep-audit-design.md`. +**Process trace:** `~/projects/.workshop/.archive/2026-05-22-grep-audit-extract.md`. +**Импл-таски:** `interns-grep-audit-impl` (в `OpeItcLoc03/.common` 🟢), `interns-grep-audit-skill-updates` (в `OpeItcLoc03/claude-skills` 🟢). + +**Кто делает:** **не имплементер.** Следующая сессия в этом проекте (другая модель / другой день / другой агент) поднимает таску с чистым контекстом. «Я только что это написал» bias = главный риск. + +**Чек-лист ревью:** + +1. **Спецификация vs shipped-код.** + - Сигнатура `grep_audit(paths, patterns, output, case_sensitive)` совпадает с design-page §«Сигнатура». + - `output="table"` рендерит ✅/❌/⚠️ как в дизайне. + - `output="json"` shape матчит `{"rows": [{path, matches: {: bool|null}}]}`. + - При `FileNotFoundError`/`PermissionError`/`IsADirectoryError` — partial-result с `null`/`⚠️`, не abort всего вызова. + - Always-ask matcher применяется (`safety.check_paths`) — single source of truth с другими интернами. + +2. **TDD discipline.** + - `git log --reverse` тестов и имплементации — тесты должны быть закоммичены ДО имплементации (или в том же коммите с явной маркировкой «red phase»). + - Каждый тест из acceptance-листа в impl-task реально написан и pass. + - Coverage не «прогонка через ветку», а assert на observable behavior. + +3. **Base class adaptation.** + - `endpoint=null` действительно skipпает LLM-client init без exceptions при registry-load. + - Это generic-механизм, не one-off hack для `grep_audit`. Будущий детерминированный интерн поднимется тем же путём. + +4. **Skill routing.** + - `using-interns/SKILL.md` содержит 3 строки про `grep_audit` (deterministic claim, vs `bulk_text_read` boundary, always-ask reminder). + - version bumped MINOR. + - dist installed и verified. + +5. **Boundary check (Script-First Rule).** + - В реализации НЕТ LLM-вызова. Ни условного, ни fallback-режима. Если в коде встретился `self.client.complete(...)` — это finding, нарушение Decision #2. + +**Findings → follow-up tasks** через `tasks_create` (`interns-grep-audit--fix` или подобное). + +**Закрытие:** только когда все findings зафайлены ИЛИ ревьюер подтвердил «нет findings» в close-note. + +**NB:** workshop-promote-brainstorm v1.x mandates pointers-task для domain-промоушена. В этом промоушене pointers skipped с обоснованием «design ляжет в claude-skills (тот же репо, где скилы и using-interns), impl — в .common; cross-repo gap мелкий, одна ссылка `**Design:** path` в description каждой импл-таски достаточна». Ревьюер должен подтвердить что impl-агент в `.common` нашёл дизайн через ссылку без угадывания. + +**Status:** done +**Where I stopped:** done — review PASS, all 5 checklist items passed, no blocking findings. Minor note: impl adds `OSError` beyond design spec's three exceptions (reasonable defensive addition). +**Closed by:** Fresh-eyes review 2026-05-22. Spec-vs-code: signature matches, table/json outputs correct, partial-result on errors intact, always-ask gate present. TDD: single atomic commit with tests (237 lines) + impl (104 lines), 15 acceptance tests all present and assert on observable behavior. Base class: `endpoint=None` works generically, not one-off. Skill routing: 3 rows in using-interns, MINOR bump 0.2.2→0.3.0 verified. Script-First Rule: grep_audit.py is pure `re` + `Path.read_text()`, zero LLM calls. +**Branch:** n/a + + + +--- diff --git a/tests/fixtures/real-projects-meta-mcp-STATUS.md b/tests/fixtures/real-projects-meta-mcp-STATUS.md new file mode 100644 index 0000000..751167f --- /dev/null +++ b/tests/fixtures/real-projects-meta-mcp-STATUS.md @@ -0,0 +1,147 @@ +# Task Board +_Updated: 2026-05-05_ + + + + + + + + +## 🟢 [mvp1-meta-pseudo-project] — локальный `_meta` pseudo-project (DONE) + +**Status:** done +**Where I stopped:** имплементировано на main: `~/projects/.wiki/` (Karpathy-канон) + `~/projects/.tasks/STATUS.md` созданы; `config.ts` добавил `metaTasksDir`; `sync-runner.ts` инжектит `_meta` через optional `readMetaTasks` dep; `meta.ts` отдаёт `meta_present`/`meta_tasks_active`/`meta_tasks_total`/`meta_tasks_dir`. 63 теста зелёные, e2e на реальной Gitea отдаёт 7 проектов включая `_meta`. Commit `0d6e05d`. +**Next action:** перезапустить Claude Code чтобы новая сессия видела новые поля `meta.status`; затем — MVP-2. +**Branch:** main + +--- + +## 🟢 [mvp2-projects-tasks-repo] — Gitea-репо `projects-tasks` как источник `_meta` (DONE) + +**Status:** done +**Where I stopped:** имплементировано на main: `AuthConfig.metaTasksRepo` (default `projects-tasks`, опционально override через auth.toml); `runSync` сначала пытается тянуть `//.tasks/STATUS.md`, при отсутствии — fallback на локальный `~/projects/.tasks/STATUS.md`; meta-репо исключается из обычного списка проектов; `meta.status` отдаёт `meta_source` (`gitea`/`local`/`none`) и `meta_branch`. 72 теста зелёные. Commit `d1ac107`. +**Next action:** для cross-machine — создать Gitea-репо `projects-tasks` с `.tasks/STATUS.md`; вторая машина после `node dist/sync.js` увидит мета-доску. Сейчас `meta_source: 'local'` потому что репо нет. +**Branch:** main + +--- + +## 🟢 [mvp3-write-api] — backend interface + `tasks_create` / `tasks_update` / `tasks_close` (DONE) + +**Status:** done +**Where I stopped:** имплементировано на main: `Backend` interface (`src/lib/backend.ts`); `gitea.ts` рефакторен в `makeGiteaBackend` с `getFileWithSha` + `commitFile` (Contents API, base64, sha-lock); `status-md-writer.ts` (format/append/close/update + identity footer); 3 write-тула с hard confirm-gate и dry-run preview; routing-hint в read-тулах. `server.ts` грузит auth опционально. 107 тестов зелёные. Commit `a3aa8b8`. +**Next action:** smoke против реальной Gitea — создать `projects-tasks` репо, запустить sync, дёрнуть `tasks.create` для `_meta` в новой Claude-сессии. +**Branch:** main + +--- + +## 🟢 [mvp4-knowledge-ingest] — `knowledge_ingest` / `knowledge_promote` через backend (DONE) + +**Status:** done +**Where I stopped:** имплементировано на main: `wiki-writer.ts` (formatPage / insertIndexEntry / appendLogEntry / freshIndexMd / freshLogMd); `resolveTarget` вынесен в `lib/resolve-target.ts`; `AuthConfig.metaWikiRepo` (default `projects-wiki`); 2 новых тула с hard confirm-gate, тремя последовательными коммитами (page + index + log), partial-failure пробрасывается. 125 тестов зелёные. Commit `5b9e380`. +**Next action:** **Рекурсивный профит unlocked** — `tool-description-routing-hint` теперь решается через `knowledge.ingest({target_project: "_meta", type: "concepts", slug: "tasks-routing-rule", body: "..."})`. Gitea-репо `projects-wiki` создан 2026-04-29 (пустой — нужен seed-коммит перед первым ingest). +**Branch:** main + +--- + +## ⚪ [knowledge-getfrom-meta-alias] — `_meta` алиас в `knowledge.get_from` + +**Status:** ready +**Where I stopped:** обнаружено при verify [tool-description-routing-hint] 2026-05-01. `ingest` принимает `_meta` (через `resolveTarget`), `get_from` — нет (находит проект через прямой `cache.projects.find`). Асимметрия read/write API. +**Next action:** прокинуть `metaWikiRepo` в `get_from` хендлер; заменить inline find на `resolveTarget(opts.cacheFile, input.project, opts.metaWikiRepo)` по образцу `ingest:384`. Тривиальный fix (~10 строк + tests). +**Branch:** main + +--- + +## ⚪ [knowledge-search-reindex] — invalidate FTS-кэш после ingest/promote + +**Status:** ready +**Where I stopped:** `knowledge.search` мемоизирует `cached: WikiPage[]` навсегда (knowledge.ts:133-138). После `ingest` страница лежит на диске + в Gitea, но in-memory FTS-индекс не обновляется — поиск пуст до рестарта MCP. `meta_status.wiki_pages_count` при этом обновляется (другой code path). +**Next action:** простой путь — invalidate `cached = null` в конце `ingest`/`promote` confirm-пути; архитектурный — `chokidar` watcher на `wikiRoot` для self-healing на внешний `git pull`. Решить scope, потом писать. +**Branch:** main + +--- + +## 🟢 [tool-description-routing-hint] — расширить через knowledge.ingest (DONE) + +**Status:** done +**Where I stopped:** `knowledge.ingest` дёрнут 2026-05-01 в `_meta/concepts/tasks-routing-rule` (3 коммита в `projects-wiki@main`: `d98d62c` + `be3fefe` + `5b4fe1b`). Верификация через `knowledge_get_from(projects-wiki, concepts/tasks-routing-rule)` — страница доступна. Хардкод routing-hint в `tasks.ts` (commit `a3aa8b8`, MVP-3) остаётся как safety net. Бас-механизм MVP-4 подтверждён. +**Next action:** (none — task closed). Discoveries during verify заведены отдельными тасками: `_meta`-алиас в `knowledge_get_from`, реиндекс search после ingest. +**Branch:** main + + +--- + +## 🟢 [wiki-path-mismatch] — knowledge.* read/write по одному пути (DONE) + +**Status:** done +**Where I stopped:** fix `sharedWikiClone → ~/projects/projects-wiki/.wiki` смержен в main 2026-04-29 (PR #1 → merge commit `d4396b0`, fix commit `621a69f`). Локальная ветка fix/wiki-path-mismatch удалена. 125 тестов зелёные, build чистый. +**Next action:** (none — закрыто). Для применения fix в текущей Claude-сессии — рестарт (dist/server.js перечитать). После рестарта `knowledge.get packages/projects-meta-mcp` должен вернуть содержимое. +**Branch:** main + + + +--- + +## 🟢 [refresh-project-bootstrap] — Refresh repo layout via `project-bootstrap@1.5.0` (DONE) + +**Status:** done +**Where I stopped:** upgrade-mode reconciliation finished 2026-05-01: CLAUDE.md gained `pull remote before work` + `follow project discipline` triggers; `.wiki/concepts/bootstrap-manifest.md` bumped 1.2.0 → 1.5.0 with `project-discipline@0.1.0`; per-task file created. `.wiki/` and `.tasks/` already canon — no structural changes. +**Next action:** (none — task closed). Push deferred to next batched push along with `wiki-path-mismatch`. +**Branch:** main + + + +--- + +## 🟡 [cross-platform-support] — Поддержка Linux и macOS + +**Status:** paused +**Where I stopped:** запушены все изменения (commits 3fb3c02 + 0597322). README.md имеет OS-specific инструкции, sync.ts упрощён, tests pass (149/16), build clean. Остаток: тестирование на реальных Linux/macOS машинах или CI. +**Next action:** запушить изменения; проверить на Linux/macOS (или через CI); обновить per-task файл с decision log. +**Branch:** main + +--- + +## 🟢 [migrate-to-common-lib] — Backlog: migrate `projects-meta-mcp` source from `~/projects/projects-meta-mcp/` into `.common/lib/projects-meta-mcp/` for layout uniformity with `interns-mcp` and any future MCP servers. Preserves git history; updates `~/.claude.json` MCP command path; updates `setup-projects-meta` install location. Layout uniformity across `lib/` was raised in interns design as a follow-up to the new `.common/lib/interns-mcp/` location. + +**Status:** done +**Where I stopped:** discovered 2026-05-05 via `~/.claude.json` inspection — `mcpServers.projects-meta.command` already points to `~/projects/.common/lib/projects-meta-mcp/dist/server.js`, and `dist/` is built there. `node dist/sync.js` from that location returns `synced 13 projects, 0 errors`. Migration happened informally outside this task's tracking. +**Next action:** (none — closed). Follow-ups: (1) `[using-projects-meta-fix-paths]` in `claude-skills` updates the skill's stale `~/.local/projects-meta-mcp/` references; (2) decide separately whether `~/projects/projects-meta-mcp/` is a leftover working copy to remove, or remains as an active dev clone. +**Branch:** n/a + + + +--- + +## ⚪ [bootstrap-upgrade-canonical-triggers] — Прогнать `project-bootstrap` v1.10.0 в upgrade-режиме на этом проекте. Цель — дописать в `CLAUDE.md` 3 недостающих канонических триггера: `follow tdd-criteria`, `delegate to interns when allowed`, `recommend, don't menu`. Уже присутствуют: `pull remote before work`, `follow project discipline`, `we're on Windows`. Аудит 2026-05-07 через `bulk_text_read`. Контекст — координатор `[bootstrap-rollout-canonical-triggers]` в `_meta` (commit dd933fd). Пилот на `books` подтвердил идемпотентность (158fc95). NB: репа под `OpeItcLoc03` но в кэше projects-meta отсутствует (sync пропустил как archived) — таска создана прямой записью в `.tasks/STATUS.md`. + +**Status:** ready +**Where I stopped:** Unblocked 2026-05-07 после успешного прогона `pilonuxt` (commit `e5d45f9`: полный bootstrap — CLAUDE.md + `.wiki/concepts/bootstrap-manifest.md`, скил отработал корректно). +**Next action:** 1) `upgrade project` в `~/projects/projects-meta-mcp/`. **Запустить скил `project-bootstrap` v1.10.0** — не делать ручной Edit (incident на `pilonuxt` 2026-05-07). 2) Подтвердить план: 2 файла — `CLAUDE.md` (+3: `follow tdd-criteria`, `delegate to interns when allowed`, `recommend, don't menu`) + `.wiki/concepts/bootstrap-manifest.md` (manifest 1.x → 1.10.0). 3) Прогнать. 4) **Verify:** `git diff --stat` показывает 2 файла. Если 1 — переоткрыть. Reference: `books@5ca16d9`. 5) Коммит `chore(bootstrap): upgrade CLAUDE.md — add tdd-criteria, interns, recommend-dont-menu triggers` + body со скилами. 6) Повторный — no-op. 7) Закрыть вручную в `.tasks/STATUS.md` (cache-skip). +**Branch:** n/a + + + +--- diff --git a/tests/gitea.test.ts b/tests/gitea.test.ts new file mode 100644 index 0000000..8793041 --- /dev/null +++ b/tests/gitea.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, test, vi } from 'vitest'; +import { createGiteaClient } from '../src/gitea.ts'; + +type MockFetch = ReturnType; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + }); +} + +function makeClient(mockFetch: MockFetch) { + return createGiteaClient({ + baseUrl: 'https://git.kzntsv.site', + token: 'test-token', + fetch: mockFetch as unknown as typeof fetch, + }); +} + +describe('GiteaClient.getFile', () => { + test('hits /api/v1/repos///contents/ with Authorization header', async () => { + const mockFetch: MockFetch = vi.fn().mockResolvedValue( + jsonResponse({ + content: Buffer.from('hello world', 'utf8').toString('base64'), + encoding: 'base64', + }), + ); + const client = makeClient(mockFetch); + + await client.getFile('OpeItcLoc03', 'board-viewer', '.tasks/STATUS.md'); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const [url, init] = mockFetch.mock.calls[0]!; + expect(url).toBe( + 'https://git.kzntsv.site/api/v1/repos/OpeItcLoc03/board-viewer/contents/.tasks/STATUS.md', + ); + expect((init as RequestInit).headers).toMatchObject({ + Authorization: 'token test-token', + }); + }); + + test('decodes base64 content and returns text', async () => { + const original = 'STATUS.md body\nwith newline.'; + const mockFetch: MockFetch = vi.fn().mockResolvedValue( + jsonResponse({ + content: Buffer.from(original, 'utf8').toString('base64'), + encoding: 'base64', + }), + ); + const client = makeClient(mockFetch); + + const result = await client.getFile('owner', 'repo', 'file.md'); + + expect(result).toBe(original); + }); + + test('returns null on 404 (file absent)', async () => { + const mockFetch: MockFetch = vi.fn().mockResolvedValue( + new Response('Not Found', { status: 404 }), + ); + const client = makeClient(mockFetch); + + const result = await client.getFile('owner', 'repo', 'missing.md'); + + expect(result).toBeNull(); + }); + + test('throws on non-404 HTTP error (e.g. 500)', async () => { + const mockFetch: MockFetch = vi.fn().mockResolvedValue( + new Response('Server error', { status: 500 }), + ); + const client = makeClient(mockFetch); + + await expect(client.getFile('owner', 'repo', 'file.md')).rejects.toThrow(/500/); + }); +}); + +describe('GiteaClient.getLatestCommitIso', () => { + test('returns commit date ISO string when commit exists', async () => { + const mockFetch: MockFetch = vi.fn().mockResolvedValue( + jsonResponse([ + { commit: { author: { date: '2026-05-22T08:10:00Z' } } }, + ]), + ); + const client = makeClient(mockFetch); + + const iso = await client.getLatestCommitIso('owner', 'repo', '.tasks/foo.md'); + + expect(iso).toBe('2026-05-22T08:10:00Z'); + const [url] = mockFetch.mock.calls[0]!; + expect(url).toMatch( + /\/api\/v1\/repos\/owner\/repo\/commits\?path=\.tasks%2Ffoo\.md&limit=1/, + ); + }); + + test('returns null when no commits touch the path', async () => { + const mockFetch: MockFetch = vi.fn().mockResolvedValue(jsonResponse([])); + const client = makeClient(mockFetch); + + expect(await client.getLatestCommitIso('o', 'r', 'p.md')).toBeNull(); + }); +}); + +describe('GiteaClient.rawUrl', () => { + test('builds the raw URL for a file on the default branch', () => { + const client = makeClient(vi.fn()); + + const url = client.rawUrl('OpeItcLoc03', 'board-viewer', '.tasks/STATUS.md'); + + expect(url).toBe( + 'https://git.kzntsv.site/OpeItcLoc03/board-viewer/raw/branch/master/.tasks/STATUS.md', + ); + }); +}); diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 33a6f0c..d7c2b51 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -33,6 +33,22 @@ describe('parseStatus', () => { expect(parseStatus('# Task Board\n_Updated: 2026-05-22_\n\nNo tasks yet.\n')).toEqual([]); }); + test.each([ + 'real-books-STATUS.md', + 'real-claude-skills-STATUS.md', + 'real-projects-meta-mcp-STATUS.md', + ])('parses real STATUS.md fixture %s without crashing and yields ≥1 block', (name) => { + const blocks = parseStatus(fixture(name)); + + expect(blocks.length).toBeGreaterThan(0); + for (const b of blocks) { + expect(b.slug).toMatch(/^[a-z0-9][a-z0-9-]*$/); + expect(b.status).toMatch(/^(open|in_progress|paused|blocked|done)$/); + expect(b.status_emoji).toMatch(/^(⚪|🔴|🟡|🔵|🟢)$/u); + expect(b.title.length).toBeGreaterThan(0); + } + }); + test('ignores blocks whose H2 has no known status emoji', () => { const text = '## 🚀 [some-task] — title\n**Status:** custom\n**Branch:** master\n---\n'; expect(parseStatus(text)).toEqual([]); diff --git a/tests/reader.test.ts b/tests/reader.test.ts new file mode 100644 index 0000000..2e47f76 --- /dev/null +++ b/tests/reader.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, test, vi } from 'vitest'; +import type { GiteaClient } from '../src/gitea.ts'; +import { readBoard } from '../src/reader.ts'; + +function fakeClient(overrides: Partial = {}): GiteaClient { + return { + getFile: vi.fn().mockResolvedValue(null), + getLatestCommitIso: vi.fn().mockResolvedValue(null), + rawUrl: vi.fn().mockReturnValue('http://example/raw/x'), + ...overrides, + }; +} + +const STATUS_MD_ONE = `# Task Board +_Updated: 2026-05-22_ + +## 🔴 [task-a] — first task +**Status:** active +**Where I stopped:** doing A. +**Next action:** continue A. +**Branch:** master + +--- +`; + +describe('readBoard', () => { + test('merges parser output with project metadata and Gitea last-commit + raw URL', async () => { + const client = fakeClient({ + getFile: vi.fn().mockResolvedValue(STATUS_MD_ONE), + getLatestCommitIso: vi.fn().mockResolvedValue('2026-05-22T09:00:00Z'), + rawUrl: vi.fn().mockReturnValue( + 'https://git.kzntsv.site/OpeItcLoc03/board-viewer/raw/branch/master/.tasks/task-a.md', + ), + }); + + const records = await readBoard(client, [{ owner: 'OpeItcLoc03', repo: 'board-viewer' }]); + + expect(records).toHaveLength(1); + expect(records[0]).toEqual({ + slug: 'task-a', + project: 'board-viewer', + project_owner: 'OpeItcLoc03', + status: 'in_progress', + status_emoji: '🔴', + title: 'first task', + where_stopped: 'doing A.', + next_action: 'continue A.', + blocker: null, + branch: 'master', + last_commit_iso: '2026-05-22T09:00:00Z', + raw_url: + 'https://git.kzntsv.site/OpeItcLoc03/board-viewer/raw/branch/master/.tasks/task-a.md', + }); + }); + + test('skips repo silently when STATUS.md is missing (returns no records for that repo)', async () => { + const client = fakeClient({ + getFile: vi.fn().mockResolvedValue(null), // 404 + }); + + const records = await readBoard(client, [{ owner: 'o', repo: 'no-tasks' }]); + + expect(records).toEqual([]); + }); + + test('aggregates blocks across multiple repos', async () => { + const repoFiles: Record = { + 'repo-a': STATUS_MD_ONE, + 'repo-b': STATUS_MD_ONE.replace('task-a', 'task-b').replace('first task', 'second task'), + }; + const client = fakeClient({ + getFile: vi.fn(async (_owner: string, repo: string) => repoFiles[repo] ?? null), + getLatestCommitIso: vi.fn().mockResolvedValue('2026-05-22T09:00:00Z'), + }); + + const records = await readBoard(client, [ + { owner: 'o', repo: 'repo-a' }, + { owner: 'o', repo: 'repo-b' }, + ]); + + expect(records.map((r) => `${r.project}:${r.slug}`)).toEqual([ + 'repo-a:task-a', + 'repo-b:task-b', + ]); + }); + + test('builds raw_url per-task file using .md path under .tasks/', async () => { + const rawUrl = vi.fn((_o: string, _r: string, path: string) => `RAW:${path}`); + const client = fakeClient({ + getFile: vi.fn().mockResolvedValue(STATUS_MD_ONE), + getLatestCommitIso: vi.fn().mockResolvedValue(null), + rawUrl, + }); + + const records = await readBoard(client, [{ owner: 'o', repo: 'r' }]); + + expect(records[0]!.raw_url).toBe('RAW:.tasks/task-a.md'); + expect(rawUrl).toHaveBeenCalledWith('o', 'r', '.tasks/task-a.md'); + }); + + test('queries last_commit_iso for the per-task file path', async () => { + const getLatestCommitIso = vi.fn().mockResolvedValue('2026-05-22T10:00:00Z'); + const client = fakeClient({ + getFile: vi.fn().mockResolvedValue(STATUS_MD_ONE), + getLatestCommitIso, + }); + + await readBoard(client, [{ owner: 'o', repo: 'r' }]); + + expect(getLatestCommitIso).toHaveBeenCalledWith('o', 'r', '.tasks/task-a.md'); + }); +});