12f88f32c1
Phase 3 Task 19 partial — coverage announcement §4.9 deferred to a
separate commit (touches Pravila §17, requires §15.2 pre-flight sync).
- tools/brain-retro-sanity-generator.mjs (NEW, pure):
generateCandidateQuestions(episodes) returns ≤5 sanity questions
derived from per-classification volume (>10 episodes per task type
triggers a themed question: bugfix/feature/planning/refactor/security/
marketing) plus 2 meta questions about missed activations / direct
bypass. Reads task_type from classifier_output (v4) with fallback
to primary_rationale.task_classification (v2/v3). Spec §4.7.
- tools/brain-retro-sanity-generator.test.mjs (NEW): 6 tests
(bugfix >10 / feature >10 / max 5 / empty / legacy v2/v3 / strings).
- .claude/skills/brain-retro/SKILL.md:
+ description rewritten — "раз в 1-2 недели OR sanity-check threshold"
(cadence change per spec §4.7).
+ procedure +steps 5a (sanity questions via AskUserQuestion +
PII filter + sanity-checks/YYYY-MM-DD.json), 5b (reviewer-agent
Task() spawn + fallback to brain-retro-opus-reviewer.mjs), 9
(self-retrospect threshold check), 10 (cost report from
~/.claude/runtime/cost-daily.json), 11 (richer summary).
- .claude/skills/self-retrospect/SKILL.md (NEW) — stub skill;
full procedure wired in Task 20 (analyzer + STATUS.md surface the
threshold).
- docs/observer/.self-retrospect-counter.json (NEW): initial state
{last_run_at: null, episodes_since_last: 0}.
- docs/observer/sanity-checks/.gitkeep (NEW): directory placeholder
for sanity-answers JSON files.
Tests: 608 passed / 0 failed (+15 from Task 19 + prior). 4 pre-existing
file fails unchanged. Coverage announcement §4.9 (economy-mode.py +
Pravila §17 subsection + feedback memory + coverage-annotation-mode
flag) — deferred: touches Pravila which is in the §15.2 8-file SoT
list and needs pre-flight `git fetch origin && git log HEAD..origin/main`
before edit; flagging as Phase 3 follow-up commit.
89 lines
3.7 KiB
JavaScript
89 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* brain-retro sanity-check candidate generator (Phase 3 Task 19, spec §4.7).
|
|
*
|
|
* Pure deterministic — read-only, no fs, no LLM. Given the episodes of a
|
|
* /brain-retro period, emit up to 5 candidate sanity-check questions for the
|
|
* controller (главный Claude) to choose 3-4 from. Questions are asked via
|
|
* AskUserQuestion; comments pass through observer-pii-filter before being
|
|
* persisted to docs/observer/sanity-checks/YYYY-MM-DD.json.
|
|
*
|
|
* Threshold: a per-classification question fires when the corresponding
|
|
* volume crosses 10 episodes in the period (per spec §4.7).
|
|
*
|
|
* All questions are in Russian to match the controller-user dialogue.
|
|
*/
|
|
|
|
const MAX_QUESTIONS = 5;
|
|
|
|
const VOLUME_THRESHOLD = 10;
|
|
|
|
function classification(ep) {
|
|
if (!ep) return null;
|
|
return ep?.classifier_output?.task_type
|
|
?? ep?.primary_rationale?.task_classification
|
|
?? null;
|
|
}
|
|
|
|
const VOLUME_QUESTIONS = [
|
|
{
|
|
cls: 'bugfix',
|
|
q: 'За период было много багов. Что мешает увереннее их отдебагать с первой попытки — недостаток воспроизведения, недостаток observability, или нехватка времени на гипотезы?',
|
|
},
|
|
{
|
|
cls: 'feature',
|
|
q: 'За период было много новых фич. Где сейчас бутылочное горлышко — спецификация, code review, тесты, выкат?',
|
|
},
|
|
{
|
|
cls: 'planning',
|
|
q: 'За период было много задач на планирование. Это сигнал что план каждой задачи становится сложнее, или что задачи приходят без подготовленного скоупа?',
|
|
},
|
|
{
|
|
cls: 'refactor',
|
|
q: 'За период было много рефакторов. Они шли парами с фичами/багами, или это отдельные кампании? Какие самые болезненные участки кода остались?',
|
|
},
|
|
{
|
|
cls: 'security',
|
|
q: 'За период было много security-задач. Это плановые сканы перед выкатом, или реакция на находки? Где сейчас самый высокий риск?',
|
|
},
|
|
{
|
|
cls: 'marketing',
|
|
q: 'За период было много маркетинговых задач. Кампании окупились по KPI, или работа идёт без замера? Что хотим оптимизировать в следующий период?',
|
|
},
|
|
];
|
|
|
|
const META_QUESTIONS = [
|
|
'Что наблюдатель должен был засечь за период, но не засёк? Назови один конкретный кейс если есть.',
|
|
'За период случались моменты когда контроллер выбрал direct, хотя нужен был навык? Один пример достаточно.',
|
|
];
|
|
|
|
export function generateCandidateQuestions(episodes) {
|
|
const eps = Array.isArray(episodes) ? episodes : [];
|
|
|
|
const counts = new Map();
|
|
for (const ep of eps) {
|
|
const c = classification(ep);
|
|
if (!c) continue;
|
|
counts.set(c, (counts.get(c) || 0) + 1);
|
|
}
|
|
|
|
const ranked = [...counts.entries()]
|
|
.filter(([_, n]) => n > VOLUME_THRESHOLD)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([cls]) => cls);
|
|
|
|
const out = [];
|
|
for (const cls of ranked) {
|
|
const v = VOLUME_QUESTIONS.find((q) => q.cls === cls);
|
|
if (v) out.push(v.q);
|
|
if (out.length >= MAX_QUESTIONS) break;
|
|
}
|
|
|
|
for (const meta of META_QUESTIONS) {
|
|
if (out.length >= MAX_QUESTIONS) break;
|
|
out.push(meta);
|
|
}
|
|
|
|
return out.slice(0, MAX_QUESTIONS);
|
|
}
|