Closes brain-retro 2026-05-20 #3 SIMPLIFIED — sanitizeWithCount in
pii-filter (counts matches per pattern) + persistent monthly counter
docs/observer/.pii-counters.json (bumped by Stop-hook on each episode
write) + status-md-generator reads real count (no more piiMatches: 0
hardcode).
PII patterns themselves NOT changed (F7 of parallel session already
extended to 13 patterns).
Counter is informational — write failure never blocks Stop-event.
5+1+1=7 new vitest tests, 256/256 GREEN.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V2_FIELDS list omitted prompt_signal and events — both are always produced
by parser and buildEpisodeFromContext, so the happy path is unaffected, but
a future ctx-fallback path that dropped them would silently write a
malformed episode. Add both to V2_FIELDS; appendEpisode now throws on either
being missing.
Tests: 2 new — appendEpisode throws when prompt_signal missing /
when events missing. 38/38 stop-hook tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When episode is user_chose_from_options, routing-gate does NOT block —
collaborative-choice from Claude-offered options doesn't require a
routing-tag (detector is deterministic). 18/18 stop-hook tests GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Stop-hook was writing empty-shell episodes (task_id "unknown-<ts>",
node_chosen "unknown", events []). Root cause: buildEpisodeFromContext
read fields from the Stop-event stdin that Claude Code never sends
(primary_rationale, node_chosen, ...) and the session field name was
wrong (ctx.sessionId camelCase vs Claude Code's session_id). The hook
never read transcript_path — the only real source of session data.
New tools/observer-transcript-parser.mjs — pure parseTranscript(text,
fallbackSessionId):
- Scopes to the last turn (from the last real user prompt to EOF) —
one episode == one prompt→response cycle. A tool_result-carrier user
message is not treated as a turn boundary.
- Extracts task_id (real sessionId), timestamps (real duration),
skill_invoked events, a tool_summary event with per-tool counts,
error events (tool_result is_error), node_chosen (first skill, else
"direct"), hard_floor (invoked when a superpowers:* skill is used),
path_type (regulated/improvised), task_classification (keyword
heuristic on the prompt).
- Reasoning fields triggers_matched/candidates_considered/
boundaries_applied stay [] — not recoverable from a transcript;
their capture is a separate ADR-011 follow-up.
observer-stop-hook.mjs: reads ctx.transcript_path + ctx.session_id
(camelCase fallback kept), readFileSync best-effort, delegates to
parseTranscript. No transcript → graceful fallback to ctx defaults.
Episode schema (5 mandatory + 7-field primary_rationale) unchanged —
no normative change. Stop-event is never blocked (exit 0 on any error).
TDD: 17 parseTranscript tests + 1 buildEpisodeFromContext transcript
test. Full tools Vitest 70/70 GREEN. CLI smoke against a real 575-entry
transcript: episode populated — real task_id, ~6.5 min duration,
tool_summary {Bash:5,Read:5,Grep:1,Edit:9,Write:1}, error event.
Refs: ADR-011 brain governance §6.2 (observer evidence loop).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure regex/JSON, 0 LLM calls. 4 Vitest tests GREEN. Per ADR-011 + spec §6.1.
Smoke run surfaces REAL drift (DONE_WITH_CONCERNS — plan B5 said «that's
a real signal, document, don't fix here»): 9 plugins in
~/.claude/settings.json enabledPlugins NOT formalized by exact
«name@source» string in Tooling Прил. Н:
- frontend-design@claude-plugins-official (informally as #30
«Frontend Design plugin»)
- 8× ToB plugins @trailofbits (differential-review, audit-context-
building, supply-chain-risk-auditor, insecure-defaults, sharp-
edges, static-analysis, variant-analysis, agentic-actions-auditor)
informally as #39 «Trail of Bits Skills»
This is naming-vocabulary mismatch (Tooling uses human-readable
names; settings.json uses machine names). Not architectural drift.
Resolution options for follow-up:
- Add machine names as «external_id» attribute to Tooling Прил. Н rows.
- Add tools/.l1-watcher-aliases.txt with accepted machine→human map.
Until resolved: C1 will FAIL on lefthook (C5 wiring) — addressed in
C5 by adding alias mechanism OR temporarily downgrade to WARN.
Also fixed CLI guard bug in observer-stop-hook.mjs (B3) and l1-watcher
— old guard `import.meta.url === \`file://\${argv[1]}\`` did not match
on Windows (file:/// triple-slash vs file:// double-slash + relative
argv[1]). New guard: argv[1].endsWith('/<filename>.mjs').
Weekly GH Actions cron (Mon 09:00 MSK) opens issue on drift.
Vitest config extended to ../tools/*.test.mjs with exclude for ruflo-*
and subagent-prompt-prefix tests (pre-existing, not part of brain
governance).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hook contract: reads JSON ctx from stdin (Claude Code Stop-event),
builds episode with 5 mandatory fields including primary_rationale
(7 sub-fields per spec v1.1 §5.2.1), sanitizes via observer-pii-filter,
appends to docs/observer/episodes-YYYY-MM.jsonl. Never blocks
Stop-event (exit 0 on error).
8 Vitest tests verified GREEN (6 in appendEpisode + 2 in
buildEpisodeFromContext): append/append-existing/PII-filter/
missing-required/missing-rationale-field/routing_decision-preserved
+ buildEpisode 5-field extraction + user-rationale-preserved.
Vitest config for tools/ already covers via glob ../tools/observer-*.test.mjs
(extended in B2 commit 4616308).
Per Pravila §16.2 + ADR-011 + spec v1.1 §5.2.1 (factor analysis).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>