Brain-retro #6 follow-up #2 (consolidated). Eight independent fixes:
A1 — task_cost wiring (cost tracking)
- router-prehook.mjs: capture classifier LLM usage via onUsage callback,
persist to state.task_cost.classifier_input_tokens / output_tokens.
- observer-transcript-parser.mjs: merge router-state.task_cost on top of
extractTokenUsage(turn). State-file values win for classifier/
self_assessment/reviewer fields.
- New buildCostFromClassifierUsage() exported from router-prehook.
- Verified live: state file now shows real input_tokens=190 /
output_tokens=598 / cache_read=10075 (was 0 before).
A2 — self-assessment coverage
- observer-self-assessment-api.mjs: DEFAULT_TIMEOUT_MS 10s -> 30s.
- .claude/settings.json: Stop-hook timeout 15s -> 60s.
- Same Windows TLS handshake issue. Was 85% no_self_assessment in retro #6.
B3 — brain-retro SKILL.md reconciliation
- Step 5b: batch=default for N>=20, subagent for N<20.
C1 — dead-code cleanup
- Removed recommendNode import + getClassificationMap + getDormancy from
observer-transcript-parser.mjs.
G — parseClassifierResponse Pass 3 (fixLLMJsonQuirks)
- Root cause: real Sonnet output sometimes contains raw newlines inside
string values (multi-line reason_for_choice) and trailing commas, which
strict JSON.parse rejects. Result was llm_error_type=parse_null on
every other call, falling back to regex with task_type=unknown.
- Fix: after Pass 1 (clean) and Pass 2 (brace-extract) fail, try Pass 3
that escapes raw newline/tab inside string values and strips trailing
commas before final JSON.parse attempt. Pure char-walk, no JSON5 dep.
H — 'unknown' added to NON_BLOCKING_TASK_TYPES in router-tool-gate.mjs
- Until G fully proves itself, blocking Bash/Edit on unknown is too strict.
With G in place, parse_null should be rare; H gives a safety net.
Tests added: +9 across 5 test files. Regression: 913 vitest tests in tools/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>