Merge branch 'feat/brain-retro-2026-05-26' into main

Brain-retro #5 artifacts + session-length warning + batch-reviewer tool.

Includes commits:
  659f2b07 feat(brain-retro): retro #5 — first reviewer pass (184/202)
  ea9430d8 feat(observer): session-length warning in STATUS.md (candidate B)

Adds: tools/brain-retro-batch-reviewer.mjs (new), retro note, sanity Q&A,
computeSessionLengthBlock in status-md-generator + 7 tests. 184 episodes
in docs/observer/episodes-2026-05.jsonl now have review.* fields.
This commit is contained in:
Дмитрий
2026-05-26 11:43:15 +03:00
10 changed files with 712 additions and 196 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"2026-05": {
"WIN_USER_PATH": 57,
"WIN_USER_PATH": 72,
"IPV4": 1,
"RU_PHONE": 1
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"last_read_at": "2026-05-24T13:27:14.691Z",
"read_count_last_period": 2,
"last_read_at": "2026-05-26T05:07:20.692Z",
"read_count_last_period": 3,
"period_start": "2026-05-19T00:00:00+03:00"
}
+1 -1
View File
@@ -1,4 +1,4 @@
{
"last_run_at": null,
"episodes_since_last": 0
"episodes_since_last": 202
}
+18 -14
View File
@@ -1,6 +1,6 @@
# Brain Status (auto-generated)
Last updated: 2026-05-25T14:59:12.388Z
Last updated: 2026-05-26T07:52:20.201Z
| Контролёр | Состояние | Детали |
|---|---|---|
@@ -8,14 +8,14 @@ Last updated: 2026-05-25T14:59:12.388Z
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago |
| C4 Сигнальный статус | ✅ | This file (self-reference) |
| C5 Observer-coverage | ⚠️ | 414 episode(s) this month · Stop-hook + post-commit OK · 21 missed activation(s) — see /brain-retro |
| C5 Observer-coverage | ⚠️ | 464 episode(s) this month · Stop-hook + post-commit OK · 21 missed activation(s) — see /brain-retro |
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
## Метрики (информационные, не алерты)
- Observer evidence: 414 episodes this month, 0 observer_error markers, 59 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 275
- Last /brain-retro: 1 day(s) ago
- Observer evidence: 464 episodes this month, 0 observer_error markers, 74 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 325
- Last /brain-retro: 0 day(s) ago
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 21. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
## Метрики дисциплины
@@ -24,17 +24,17 @@ Baseline дисциплины роутера (этап 2 router discipline overh
| Тип задачи | Эпизодов | % с триггер-матчем | % через скил |
|---|---|---|---|
| analysis | 19 | 42.1% | 21.1% |
| monitoring | 16 | 0.0% | 0.0% |
| monitoring | 22 | 0.0% | 0.0% |
| analysis | 20 | 40.0% | 20.0% |
| feature | 14 | 14.3% | 0.0% |
| planning | 11 | 18.2% | 18.2% |
| bugfix | 11 | 36.4% | 45.5% |
| planning | 10 | 20.0% | 20.0% |
| cleanup | 4 | 0.0% | 0.0% |
| refactor | 1 | 0.0% | 0.0% |
| cleanup | 1 | 0.0% | 0.0% |
Router step distribution: 1: 166, 2: 143, 3: 54, 5: 46
Router step distribution: 1: 187, 2: 170, 3: 54, 5: 48
Boundaries applied (ADR / границы): 64 of 409 эпизодов (15.6%).
Boundaries applied (ADR / границы): 65 of 459 эпизодов (14.2%).
## Активные многоэтапные проекты
@@ -44,6 +44,10 @@ Boundaries applied (ADR / границы): 64 of 409 эпизодов (15.6%).
- Этап 3 (принуждение — хук на routing) — Phase A+B (классификатор + 3 хука: router-prehook/tool-gate/stop-gate в `.claude/settings.json`) ✅ + влит в main 2026-05-24. Гейт работает в режиме **`warn-only`** (только stderr-предупреждения, никакой блокировки). Bug-fix `bec69aa5`: `deriveRouterStep` в `tools/discipline-metrics.mjs` — шаг роутера теперь выводится из наблюдаемых признаков (был захардкоженной константой 1). **Follow-up 3 fixes 2026-05-24** (после ANTHROPIC_API_KEY + рестарта CC выявлены при инспекции state): (a) UTF-8 stdin helper `tools/router-stdin-helper.mjs` через `StringDecoder` + подключение к 3 хукам (русский в state-файл и Anthropic API без mojibake); (b) `tools/observer-state-enricher.mjs` — pure helper для чтения `router-state-<session>.json`; (c) `parseTranscript` обогащение `primary_rationale` 4 полями (`recommended_node` override + `recommended_chain` + `chain_progress` + `chain_completed`). 538 tools-тестов GREEN. Plan: `docs/superpowers/plans/2026-05-24-router-stage3-three-fixes.md`. CHECKPOINT B: дать warn-only накопить реальные наблюдения с **починенным** сторожем (план говорит «минимум 24 часа»), затем Task 9 — переключение в `enforce` + 2 новых метрики (domain-hit-rate / chain-completion). Plan: `docs/superpowers/plans/2026-05-24-router-overhaul-stage-3-enforcement.md`.
- Этап 4 (уборка устаревших правил, deprecation `observer-classification-map.json` → удаление) — не начат.
## Длинные сессии
Ни одной сессии с >50 ходов сегодня (UTC). ✅
## Стоимость месяца
| Компонент | Токены (in/out) | USD |
@@ -61,13 +65,13 @@ Boundaries applied (ADR / границы): 64 of 409 эпизодов (15.6%).
## Авто-ретроспектива
Last self-retrospect: never
Episodes since last run: 0 / threshold: 10
Last self-retrospect: never ⚠️ (202 эпизодов с последнего запуска, порог 10)
Episodes since last run: 202 / threshold: 10
## Reviewer: субагент vs fallback
0 эпизодов проверено из 414.
0 эпизодов проверено из 464.
## Алерт-индикаторы
File diff suppressed because one or more lines are too long
@@ -0,0 +1,227 @@
# Brain-retro #5 — first non-empty reviewer pass
**Дата:** 2026-05-26 (~08:20 MSK).
**Период:** 2026-05-24T13:18Z .. 2026-05-26T05:09Z (~40 часов, **202 эпизода**).
**Аналитик:** `node tools/brain-retro-analyzer.mjs docs/observer/episodes-2026-05.jsonl` + `tools/brain-retro-batch-reviewer.mjs` (новый — see candidate B).
**Уровень анализа:** полный (analyzer + reviewer + sanity).
**Отношение к предыдущему ретро:** надстройка над [2026-05-24-brain-retro.md](2026-05-24-brain-retro.md) (cutoff 2026-05-24T13:18Z).
> `episodeCount=202`, `reviewed=184` (91%), `errors=18` (8.9% API/parse), `observerErrorCount=0`. **Первый ненулевой reviewer-pass** в истории brain-governance (предыдущие 4 retro имели 0 reviewed).
---
## Period & context
40 часов после retro #4 — относительно тихий период (Биллинг v2 Спец C Phase 1 был выкачен ~25.05 вечер, supplier-webhook reliability Phase 1+2+3 ушёл на боевой 26.05 ночь). Главное событие — **наблюдаемая работа наблюдателя**: за этот период я (через текущую сессию) обнаружил баг самооценки (полный путь см. в коммите `752d80af` на `fix/self-assessment-prompt-source`) и впервые прогнал reviewer на 184 эпизодах.
---
## Macro метрики
| метрика | retro #4 (28h) | retro #5 (40h) | дельта |
|---|---|---|---|
| эпизоды | 116 | 202 | +86 (плотнее) |
| path_type regulated | 19.0% | **4.5%** (9/200) | **14.5 п.п. ⚠️** |
| skill-инвокации | 22 (19%) | 10 (5%) | 14 п.п. |
| missed activations | 9 | 21 (по STATUS.md — на весь файл, period N/A) | — |
| observer_error | 0 | 0 | стабильно |
| reviewed (впервые!) | 0 | **184** | +184 |
| reviewer rework rate | n/a | **11.4%** (21/184) | baseline |
**Главное:** дисциплина роутинга **резко упала** vs retro #4 (regulated 19% → 4.5%, skill-инвокаций 19% → 5%). Скорее всего — текущая длинная сессия debug+brain-retro (~125 моих ходов) превышает короткие промежутки между sanity-чекпоинтами. Эффект «длинной сессии без перезапуска».
---
## Path-type distribution
| path_type | count | % |
|---|---|---|
| improvised | 191 | 95.5% |
| regulated | 9 | 4.5% |
---
## Reviewer outcome distribution (184 reviewed)
| outcome_reviewed | count | % |
|---|---|---|
| soft_success | 118 | 64.1% |
| success | 45 | 24.5% |
| **rework** | **21** | **11.4%** |
| blocked | 0 | — |
`success + soft_success = 88.6%` — большинство задач закрыто, но **11.4% rework** — материальный сигнал.
---
## Reviewer node_quality (184 reviewed)
| node_quality | count | % |
|---|---|---|
| disputable | 108 | 58.7% |
| **correct** | **56** | **30.4%** |
| **wrong_node** | **17** | **9.2%** |
| overkill | 2 | 1.1% |
| underkill | 1 | 0.5% |
**Только 30% решений «correct»** по оценке Opus 4.7. **9% wrong_node** — где явно нужно было идти не direct'ом. Остальные 59% — «disputable» (приемлемо, но не очевидно лучшее).
### Топ нод, которые я игнорировал (alternative_better из wrong_node)
| узел | раз пропущен | назначение |
|---|---|---|
| **#19 superpowers (writing-plans, brainstorming, TDD…)** | **5** | планирование, plan-driven работа |
| **#18 Pest** | **3** | тесты (когда писал код без тестов) |
| #33 claude-md-management | 2 | правка CLAUDE.md (я делал direct Edit) |
| #25 Semgrep | 2 | анализ кода / SAST |
| #30 Frontend Design | 1 | UI design decision |
| #66 laravel-backend-patterns | 1 | backend convention check |
| #62 billing-audit | 1 | биллинг-инвариант |
| #31 UPM | 1 | UI material lookup |
| #11 Pint | 1 | code style fix |
---
## Reviewer chain_quality
| chain_quality | count |
|---|---|
| n/a | 177 |
| missing_step | 6 |
| extra_step | 1 |
`missing_step=6` — chain'ы рекомендовались, но шаг пропускался.
---
## Reviewer agent_self_assessment_accuracy
| accuracy | count | % |
|---|---|---|
| **no_self_assessment** | **172** | **93.5%** ⚠️ |
| accurate | 8 | 4.3% |
| over_confident | 2 | 1.1% |
| under_confident | 2 | 1.1% |
**Подтверждение бага (а)** — 93.5% эпизодов period'а вообще не имели валидной самооценки (либо pending, либо "Пустой запрос пользователя" — оба варианта reviewer трактует как `no_self_assessment`). Этот retro **подтверждает** ценность фикса в коммите `752d80af`. После landing'а фикса следующий retro покажет реальную динамику accuracy/over/under.
---
## Reviewer error_root_cause
| cause | count |
|---|---|
| n/a | 147 |
| **wrong_skill** | **19** |
| external_failure | 12 |
| wrong_chain_order | 4 |
| wrong_tool | 2 |
`wrong_skill=19` пересекается с `wrong_node=17` — стабильный сигнал «надо было звать другой узел».
`external_failure=12` — сетевые/lock/race (включая параллельные сессии и API hangs).
---
## Sanity-check results
См. [docs/observer/sanity-checks/2026-05-26.json](../sanity-checks/2026-05-26.json).
1. «Что наблюдатель должен был засечь, но не засёк?» → **Не вспомню**.
2. «Случались моменты, когда я выбрал direct, хотя нужен был навык?» → **Не вспомню**.
Reviewer количественно ответил за заказчика: **17 явных wrong_node + 6 missing_step = 23 эпизода** где навык/цепочка были рекомендованы и пропущены. Это «не вспомню» ≠ «не было» — наблюдатель видит то, что не видит память заказчика.
---
## Reviewer errors (не покрыто этой ретрой)
18 эпизодов получили `null` от API (timeout / parse_error / non-2xx). Будут переподняты в следующем retro.
---
## Causal chains
Топ файлов в periode (analyzer factorMatrix не вытащил chains для batch view — глянул вручную):
| файл | эпизодов | контекст |
|---|---|---|
| `docs/observer/episodes-2026-05.jsonl` | ~20 | моё текущее debugging самооценок (эта сессия) |
| `tools/observer-stop-hook.mjs` | 5+ | фикс самооценки (commit 752d80af) |
| `memory/MEMORY.md` | ~10 | memory-sync after big-day events |
| `ПИЛОТ.md` | ~6 | обновления после прод-деплоев |
**Цепочка эта-сессии** (debug→fix→commit→push→retro) — представлена 8-10 эпизодами на текущих 125 turn'ах.
---
## Candidates for owner review
### A. Add `tools/brain-retro-batch-reviewer.mjs` to repo
**Rationale:** этот retro первый, у которого reviewer-pass нашёл реальные сигналы (rework=11.4%, wrong_node=17). Канонический путь procedure (Task() spawn per episode) непригоден для batch'а на 200 эпизодах — 200 subagent'ов в одной сессии невозможно. Я написал `tools/brain-retro-batch-reviewer.mjs` (direct API через ProxyAPI, 5 concurrency, в-place мутация JSONL). Драйвер общий, не ad-hoc.
**Suggested edit:** добавить файл в репо как первый-class инструмент (`tools/brain-retro-batch-reviewer.mjs`), описать в `.claude/skills/brain-retro/SKILL.md` шаг 5b как «canonical for >50 episodes». Стоимость одного прогона ~$10 (Opus 4.7 × 200 × ~0.05).
**Rejection-option:** не добавлять в репо, оставить как локальный one-off. Тогда следующий retro переоткроет ту же проблему.
### B. Дисциплина роутинга в длинных сессиях
**Rationale:** regulated rate **упал 19.0% → 4.5%** за 40 часов. Главная причина — моя текущая сессия (~125 turn'ов) обрабатывает много меток без перезапуска, и при длинном контексте я склоняюсь к direct. Reviewer подтверждает: 17 wrong_node + 6 missing_step случаев почти все в текущей сессии.
**Suggested edit:** **не править нормативку** — это сигнал для оператора, не для правила. Кандидат для рассмотрения: автоматический «session-length warning» в STATUS.md (например, при >50 turn'ах одной сессии в день — флаг на ослабление дисциплины). Можно реализовать в `tools/status-md-generator.mjs` без правки спека.
**Rejection-option:** ничего не делать — длинные сессии нечасты и сами по себе не плохи.
### C. Enforcement of recommended_node when classifier suggests one
**Rationale:** в `wrong_node=17` случаях classifier ЯВНО рекомендовал узел (`primary_rationale.recommended_node` populated), а я пошёл direct. Это не «классификатор не справился» — это «я не послушался уже-готовой рекомендации». Stage 3 router-overhaul пока в warn-only; для случая «recommended_node !== null && node_chosen === 'direct'» — лучший кандидат на первый enforce.
**Suggested edit:** в `tools/router-tool-gate.mjs` (PreToolUse) добавить отдельный enforce-mode когда `recommended_node` явный из classifier. Пока остальные сценарии warn-only — этот один блокирует. Это уже в дорожной карте Stage 4 — приоритезировать.
**Rejection-option:** ждать полного Stage 4 (батч enforce всех сигналов). Сейчас не пилить отдельно.
### D. Confirm fix (а) — повторить retro через 7 дней
**Rationale:** в этой ретре 93.5% эпизодов «no_self_assessment». Фикс самооценки сел в `752d80af` (ветка `fix/self-assessment-prompt-source` на origin, не в main). После merge в main и накопления нового периода — следующий retro должен показать **резкое снижение** no_self_assessment + появление реальных accurate/over/under распределений.
**Suggested edit:** не правка — а контрольное событие. Календарно через ~7 дней (2026-06-02) запустить retro #6 с явной целью «verify self-assessment fix works in production».
**Rejection-option:** доверять unit-тестам, не делать спец-retro. Тогда никто не увидит если фикс не работает на проде.
---
## Behavioral rule check (Pravila §16.4)
- «Не использован ≠ проблема» — соблюдено. Reviewer flagged **17 wrong_node** — это реальные missed activations с явной recommended_node (`profile task present`). Не помечал generic unused-by-design как «zombie».
- Reviewer честно говорит `disputable` где не уверен (108 случаев) — не настаивает на «правильном» решении когда не очевидно.
---
## Cost report (estimated, без cost-daily.json)
| Component | Calls | Tokens (est.) | USD (est.) |
|---|---|---|---|
| Classifier (Sonnet 4.6) | 3 | ~3K in + ~3K out | ~$0.05 |
| Self-assessment (Sonnet 4.6) | ~33 (broken) | ~10K in + ~10K out | ~$0.20 |
| **Reviewer batch (Opus 4.7)** | **184** | **~140K in + ~90K out** | **~$8.85** |
| **Итого ретра #5** | | | **~$9.10** |
NB: cost-daily.json не существует на этой машине. Сумма — оценочная по ProxyAPI ценам.
---
## Self-retrospect trigger status
`docs/observer/.self-retrospect-counter.json``last_run_at: null`, `episodes_since_last: 0`.
После ретры #5 bump'ну на +202. Threshold 50 (по spec §4.8 default; в текущем `.self-retrospect-counter.json` поле `threshold` отсутствует — норма из спека). Counter превысит порог уже сейчас → **propose: запустить `/self-retrospect`** (opt-in).
---
## Что НЕ меняется этим retro
- НЕ редактирую `tools/observer-classification-map.json`, `docs/registry/nodes.yaml`, `tools/.node-dormancy.json`, нормативку, code (кроме `tools/observer-stop-hook.mjs` который уже в коммите `752d80af` отдельной ветке).
- НЕ переключаю router-gate из warn-only в enforce (это кандидат C, требует решения).
- НЕ пишу в `episodes-*.jsonl` через ручную правку — только через batch-reviewer (`review.*` + `outcome_reviewed` + `outcome_reviewed_source` поля).
- НЕ trigger'у auto-memory.
- STATUS.md перегенерируется через `node tools/status-md-generator.mjs` (шаг 8a процедуры).
@@ -0,0 +1,15 @@
{
"schema_version": 1,
"date": "2026-05-26",
"retro_period": "2026-05-24T13:18:00Z..now",
"questions": [
{
"q": "Что наблюдатель должен был засечь за период (24.05-26.05), но не засёк?",
"a": "Не вспомню"
},
{
"q": "Случались моменты, когда я выбрал direct, хотя нужен был навык?",
"a": "Не вспомню"
}
]
}
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Brain-retro batch reviewer (one-off, not part of canonical procedure).
*
* Reads docs/observer/episodes-YYYY-MM.jsonl, filters episodes in period and
* without outcome_reviewed, samples N (or all), calls reviewViaDirectApi on
* each (Opus 4.7 via ProxyAPI), and writes review.* fields + outcome_reviewed
* + outcome_reviewed_source = "direct_api_batch" back into the JSONL file
* (in-place line replacement, preserves forward-only forward fields).
*
* Usage:
* node tools/brain-retro-batch-reviewer.mjs <jsonl-path> <cutoff-iso> [limit] [concurrency]
*
* Example:
* node tools/brain-retro-batch-reviewer.mjs docs/observer/episodes-2026-05.jsonl 2026-05-24T13:18:00Z 30 5
*/
import { readFileSync, writeFileSync } from 'fs';
import { reviewViaDirectApi } from './brain-retro-opus-reviewer.mjs';
const [, , filePath, cutoff, limitStr = '30', concStr = '5'] = process.argv;
if (!filePath || !cutoff) {
console.error('usage: <jsonl-path> <cutoff-iso> [limit=30] [concurrency=5]');
process.exit(1);
}
const limit = parseInt(limitStr, 10);
const concurrency = parseInt(concStr, 10);
const raw = readFileSync(filePath, 'utf-8');
const lines = raw.split('\n');
const lineCount = lines.length;
const targets = []; // { idx, episode }
for (let i = 0; i < lineCount; i++) {
const line = lines[i];
if (!line.trim()) continue;
let ep;
try { ep = JSON.parse(line); } catch { continue; }
if (ep.observer_error) continue;
if (!ep.timestamps?.started_at) continue;
if (ep.timestamps.started_at < cutoff) continue;
if (ep.outcome_reviewed) continue;
targets.push({ idx: i, episode: ep });
}
const total = targets.length;
const slice = targets.slice(0, limit);
console.error(`[batch-reviewer] total in period unreviewed: ${total}, processing first ${slice.length} with concurrency ${concurrency}`);
let done = 0;
let errors = 0;
const startTs = Date.now();
async function reviewOne({ idx, episode }) {
try {
const review = await reviewViaDirectApi(episode);
if (review && !review.reviewer_error) {
episode.review = review;
episode.outcome_reviewed = review.outcome_reviewed ?? null;
episode.outcome_reviewed_source = 'direct_api_batch';
lines[idx] = JSON.stringify(episode);
done++;
} else {
errors++;
console.error(`[batch-reviewer] ${idx}: null/error from API`);
}
} catch (e) {
errors++;
console.error(`[batch-reviewer] ${idx}: ${e.message}`);
}
}
async function runBatched() {
for (let i = 0; i < slice.length; i += concurrency) {
const batch = slice.slice(i, i + concurrency);
await Promise.all(batch.map(reviewOne));
const elapsed = ((Date.now() - startTs) / 1000).toFixed(1);
console.error(`[batch-reviewer] progress ${done + errors}/${slice.length} (${elapsed}s)`);
}
}
await runBatched();
// Write file back. Note: we re-serialize EVERY line we mutated, but other lines
// are kept verbatim (no re-serialization that could alter ordering/escaping).
writeFileSync(filePath, lines.join('\n'), 'utf-8');
const elapsed = ((Date.now() - startTs) / 1000).toFixed(1);
console.error(`[batch-reviewer] done: ${done} reviewed, ${errors} errors, ${elapsed}s wall-clock`);
process.exit(0);
+65 -2
View File
@@ -118,6 +118,67 @@ Last self-retrospect: never
}
}
/**
* Brain-retro #5 candidate B (2026-05-26): session-length warning.
*
* Long sessions correlate with discipline drift — reviewer pass on retro #5
* showed regulated rate dropped 19% → 4.5% during a long session.
*
* Algorithm: group episodes by task_id (session id), compute MAX
* session_turn per session over the current calendar day (UTC), surface
* sessions with turn count >= threshold.
*
* Pure — takes episodes array, returns markdown string. No I/O.
*/
export function computeSessionLengthBlock(episodes, opts = {}) {
const threshold = opts.threshold ?? 50;
const now = opts.now ? new Date(opts.now) : new Date();
const todayUtc = now.toISOString().slice(0, 10);
if (!Array.isArray(episodes) || episodes.length === 0) {
return `## Длинные сессии\n\n(нет данных)`;
}
const sessions = new Map();
for (const e of episodes) {
if (!e || !e.task_id || !e.timestamps?.started_at) continue;
if (e.timestamps.started_at.slice(0, 10) !== todayUtc) continue;
const turn = Number(e.environment?.session_turn);
if (!Number.isFinite(turn)) continue;
const id = e.task_id;
const cur = sessions.get(id) || { maxTurn: 0, lastSeen: '', regulated: 0, total: 0 };
if (turn > cur.maxTurn) cur.maxTurn = turn;
if (e.timestamps.started_at > cur.lastSeen) cur.lastSeen = e.timestamps.started_at;
cur.total++;
if (e.path_type === 'regulated') cur.regulated++;
sessions.set(id, cur);
}
const longOnes = [...sessions.entries()]
.filter(([, v]) => v.maxTurn >= threshold)
.sort((a, b) => b[1].maxTurn - a[1].maxTurn);
if (longOnes.length === 0) {
return `## Длинные сессии\n\nНи одной сессии с >${threshold} ходов сегодня (UTC). ✅`;
}
const rows = longOnes.map(([id, v]) => {
const regPct = v.total > 0 ? ((v.regulated / v.total) * 100).toFixed(0) : '—';
const shortId = id.slice(0, 8);
return `| \`${shortId}\` | ${v.maxTurn} | ${regPct}% | ${v.lastSeen} |`;
}).join('\n');
return `## Длинные сессии
⚠️ Сегодня (${todayUtc} UTC) есть сессии с ≥${threshold} ходов — корреляция с падением дисциплины роутинга (retro #5 candidate B).
| session_id | макс. ход | % regulated | последний эпизод |
|---|---|---|---|
${rows}
Long sessions correlate with discipline drift. Если % regulated просел в текущей сессии — рассмотри перезапуск.`;
}
export function computeReviewerBlock(episodes) {
const reviewed = episodes.filter(ep => ep.review?.reviewed_at !== null && ep.review?.reviewed_at !== undefined);
const total = episodes.length;
@@ -213,7 +274,7 @@ Last updated: ${now}
- Legacy v1 episodes (not in factor analysis): ${observer.v1Episodes || 0}
- Last /brain-retro: ${retroLine}
- Использование узлов: см. \`/brain-retro\` (раз в спринт). missed_activations: ${missed.totalMissed}. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory \`feedback_brain_unused_tools_not_problem\` — outside-repo memory store).
${disciplineBlock}${projectsBlock}${inputs.costBlock ? `\n${inputs.costBlock}\n` : ''}${inputs.anomalyBlock ? `\n${inputs.anomalyBlock}\n` : ''}${inputs.selfRetrospectBlock ? `\n${inputs.selfRetrospectBlock}\n` : ''}${inputs.reviewerBlock ? `\n${inputs.reviewerBlock}\n` : ''}
${disciplineBlock}${projectsBlock}${inputs.sessionLengthBlock ? `\n${inputs.sessionLengthBlock}\n` : ''}${inputs.costBlock ? `\n${inputs.costBlock}\n` : ''}${inputs.anomalyBlock ? `\n${inputs.anomalyBlock}\n` : ''}${inputs.selfRetrospectBlock ? `\n${inputs.selfRetrospectBlock}\n` : ''}${inputs.reviewerBlock ? `\n${inputs.reviewerBlock}\n` : ''}
## Алерт-индикаторы
✅ — норма ・ ⚠️ — внимание ・ 🔴 — действие требуется ・ ⚪ — не запускалось
@@ -343,15 +404,17 @@ if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/status-md-
};
const eps = loadCurrentMonthEpisodes();
let costBlock = null, anomalyBlock = null, selfRetrospectBlock = null, reviewerBlock = null;
let costBlock = null, anomalyBlock = null, selfRetrospectBlock = null, reviewerBlock = null, sessionLengthBlock = null;
try { costBlock = computeCostBlock(eps, PRICING); } catch (err) { console.warn('[status-md-generator] costBlock skipped:', err.message); costBlock = '(нет данных)'; }
try { anomalyBlock = computeAnomalyBlock(eps); } catch (err) { console.warn('[status-md-generator] anomalyBlock skipped:', err.message); anomalyBlock = '(нет данных)'; }
try { selfRetrospectBlock = computeSelfRetrospectBlock(join('docs', 'observer', '.self-retrospect-counter.json')); } catch (err) { console.warn('[status-md-generator] selfRetrospectBlock skipped:', err.message); selfRetrospectBlock = '(нет данных)'; }
try { reviewerBlock = computeReviewerBlock(eps); } catch (err) { console.warn('[status-md-generator] reviewerBlock skipped:', err.message); reviewerBlock = '(нет данных)'; }
try { sessionLengthBlock = computeSessionLengthBlock(eps); } catch (err) { console.warn('[status-md-generator] sessionLengthBlock skipped:', err.message); sessionLengthBlock = '(нет данных)'; }
inputs.costBlock = costBlock;
inputs.anomalyBlock = anomalyBlock;
inputs.selfRetrospectBlock = selfRetrospectBlock;
inputs.reviewerBlock = reviewerBlock;
inputs.sessionLengthBlock = sessionLengthBlock;
const md = renderStatus(inputs);
writeFileSync('docs/observer/STATUS.md', md);
+68 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { renderStatus, computeCostBlock, computeAnomalyBlock, computeSelfRetrospectBlock, computeReviewerBlock } from './status-md-generator.mjs';
import { renderStatus, computeCostBlock, computeAnomalyBlock, computeSelfRetrospectBlock, computeReviewerBlock, computeSessionLengthBlock } from './status-md-generator.mjs';
const baseInputs = (overrides = {}) => ({
now: '2026-05-19T10:00:00+03:00',
@@ -312,3 +312,70 @@ describe('renderStatus — 4 new optional blocks integration', () => {
expect(md).not.toContain('## Reviewer: субагент vs fallback');
});
});
// -----------------------------------------------------------------------------
// computeSessionLengthBlock — brain-retro #5 candidate B (2026-05-26)
// Long sessions correlate with discipline drift; surface a warning when any
// session today (UTC) has ≥50 turns.
// -----------------------------------------------------------------------------
describe('computeSessionLengthBlock', () => {
const day = '2026-05-26';
const ep = (turn, opts = {}) => ({
task_id: opts.id ?? 'sess-1',
timestamps: { started_at: `${opts.day ?? day}T01:00:0${turn % 10}Z`, ended_at: `${opts.day ?? day}T01:00:0${turn % 10}Z` },
environment: { session_turn: turn },
path_type: opts.regulated ? 'regulated' : 'improvised',
});
it('returns "no data" placeholder when episodes empty', () => {
expect(computeSessionLengthBlock([])).toContain('(нет данных)');
});
it('returns OK (✅) when no session reaches threshold', () => {
const out = computeSessionLengthBlock([ep(1), ep(2), ep(10)], { now: `${day}T05:00:00Z` });
expect(out).toContain('✅');
expect(out).toContain('Ни одной сессии');
});
it('flags a session that crossed threshold', () => {
const eps = Array.from({ length: 55 }, (_, i) => ep(i + 1));
const out = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z` });
expect(out).toContain('⚠️');
expect(out).toContain('`sess-1');
expect(out).toContain('55'); // max turn
});
it('respects custom threshold', () => {
const eps = Array.from({ length: 15 }, (_, i) => ep(i + 1));
const flagged = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z`, threshold: 10 });
const notFlagged = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z`, threshold: 20 });
expect(flagged).toContain('⚠️');
expect(notFlagged).toContain('✅');
});
it('ignores episodes from other UTC days', () => {
const eps = Array.from({ length: 55 }, (_, i) => ep(i + 1, { day: '2026-05-25' }));
const out = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z` });
expect(out).toContain('✅'); // yesterday's session not counted
});
it('computes regulated % per long session', () => {
const eps = Array.from({ length: 50 }, (_, i) => ep(i + 1, { regulated: i < 10 }));
const out = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z`, threshold: 40 });
expect(out).toContain('⚠️');
expect(out).toContain('20%'); // 10 regulated out of 50 = 20%
});
it('handles missing session_turn / task_id gracefully', () => {
const eps = [
{ task_id: 'x', timestamps: { started_at: `${day}T01:00:00Z` } }, // no session_turn
{ timestamps: { started_at: `${day}T01:00:00Z` }, environment: { session_turn: 60 } }, // no task_id
ep(70, { id: 'real' }),
];
const out = computeSessionLengthBlock(eps, { now: `${day}T05:00:00Z` });
expect(out).toContain('⚠️');
expect(out).toContain('`real');
expect(out).toContain('70');
});
});