fix(classifier,gate): G parser-quirks + H unknown-not-blocking + A1/A2/B3/C1

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>
This commit is contained in:
Дмитрий
2026-05-26 19:25:16 +03:00
parent 9ca75a788a
commit 7b4da1477e
12 changed files with 225 additions and 36 deletions
+20
View File
@@ -1740,6 +1740,26 @@ describe('parseTranscript — router-state enrichment (Task 3)', () => {
rmSync(dir, { recursive: true, force: true });
}
});
it('merges router-state.task_cost (classifier tokens) into episode task_cost — A1 cost tracking', () => {
const dir = mkdtempSync(join(tmpdir(), 'router-state-test-'));
const sessionId = 'test-session-cost-merge';
const state = {
classification: { recommendedNode: '#19' },
task_cost: {
classifier_input_tokens: 4500,
classifier_output_tokens: 120,
},
};
writeFileSync(join(dir, `router-state-${sessionId}.json`), JSON.stringify(state));
try {
const ep = parseTranscript(makeTranscript(sessionId), sessionId, { routerStateBaseDir: dir });
expect(ep.task_cost.classifier_input_tokens).toBe(4500);
expect(ep.task_cost.classifier_output_tokens).toBe(120);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
// ─── Phase 3 deferred #2: parser write-block v4.3 ────────────────────────────