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:
@@ -24,9 +24,10 @@ import { homedir } from 'node:os';
|
||||
import { detectChoiceProvenance, detectAskUserQuestionChoice } from './observer-choice-detector.mjs';
|
||||
import { loadChainMap, chainsFor } from './observer-chain-detector.mjs';
|
||||
import { buildHookMap, resolveScriptCounts } from './observer-hook-resolver.mjs';
|
||||
import { recommendNode } from './observer-recommended-node.mjs';
|
||||
// recommendNode / buildClassificationMap / buildDormancyMap были использованы
|
||||
// для слепого fallback на heuristic recommended_node — убрано 2026-05-26
|
||||
// (brain-retro #6 follow-up). Импорты сняты как dead code.
|
||||
import { loadRegistry } from './registry-load.mjs';
|
||||
import { buildClassificationMap, buildDormancyMap } from './registry-to-classification-map.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -48,23 +49,6 @@ function getHookMap() {
|
||||
return HOOK_MAP;
|
||||
}
|
||||
|
||||
let CLASSIFICATION_MAP = null;
|
||||
function getClassificationMap() {
|
||||
if (CLASSIFICATION_MAP) return CLASSIFICATION_MAP;
|
||||
try {
|
||||
CLASSIFICATION_MAP = buildClassificationMap(loadRegistry());
|
||||
} catch { CLASSIFICATION_MAP = {}; }
|
||||
return CLASSIFICATION_MAP;
|
||||
}
|
||||
|
||||
let DORMANCY = null;
|
||||
function getDormancy() {
|
||||
if (DORMANCY) return DORMANCY;
|
||||
try { DORMANCY = buildDormancyMap(loadRegistry()); }
|
||||
catch { DORMANCY = {}; }
|
||||
return DORMANCY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whitelist of router-node names. Used by extractCandidates to filter out
|
||||
* free-form prose bullets (analysis text, procedure steps, code snippets) that
|
||||
@@ -919,7 +903,14 @@ export function parseTranscript(transcriptText, fallbackSessionId = null, option
|
||||
decision_provenance,
|
||||
environment: { ..._envBase, classifier_model: _classifierModel },
|
||||
task_size: extractTaskSize(turn),
|
||||
task_cost: extractTokenUsage(turn),
|
||||
// A1 (2026-05-26): merge router-state.task_cost (classifier LLM tokens) on top of
|
||||
// extractTokenUsage (assistant per-turn tokens). State-file fields win for the
|
||||
// classifier_/self_assessment_/reviewer_ block; assistant input_tokens/output_tokens
|
||||
// come from extractTokenUsage and stay intact.
|
||||
// NB: routerState (line 855) honours routerStateBaseDir option; _state at line 898
|
||||
// does not (always default dir). Use routerState here so tests with custom temp dir
|
||||
// see the merged values.
|
||||
task_cost: { ...extractTokenUsage(turn), ...((routerState && routerState.task_cost) || {}) },
|
||||
// Pass 3 — dynamics meta-block (project-brain-factor-analysis-4passes).
|
||||
// prompt_length_chars: strlen of first user prompt (engagement / clarity proxy).
|
||||
// mcp_servers_used: unique mcp__<server>__* fingerprints in this turn.
|
||||
|
||||
Reference in New Issue
Block a user