feat(observer): opt-in reasoning-tag merges with heuristic primary_rationale

Closes brain-retro 2026-05-20 #11 — parseReasoningTag extracts opt-in
<!-- reasoning: triggers="..." candidates="..." boundaries="..." -->
HTML-comment from assistant text. Semicolon-separated values merged into
heuristic-derived primary_rationale arrays via Set-dedupe.

Conservative: tag is opt-in; heuristic still runs even when tag present
(heuristic provides baseline, tag enriches).

5 new vitest tests, 309/309 GREEN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-20 13:20:45 +03:00
parent 884169e847
commit f54c82d682
2 changed files with 88 additions and 11 deletions
+40 -11
View File
@@ -532,6 +532,31 @@ export function parseRoutingTag(turn) {
return null;
}
const REASONING_TAG_RE =
/<!--\s*reasoning:\s*triggers="([^"]*)"\s+candidates="([^"]*)"\s+boundaries="([^"]*)"\s*-->/;
/**
* Opt-in reasoning tag (Task 11). Claude may emit at most one such comment
* per turn to declare triggers / candidates / boundaries explicitly. Values
* are semicolon-separated. When present, parser merges them into the
* heuristic-derived arrays via Set-dedupe.
*/
export function parseReasoningTag(turn) {
for (const e of turn || []) {
const content = e && e.message && Array.isArray(e.message.content) ? e.message.content : [];
for (const b of content) {
if (b && b.type === 'text' && typeof b.text === 'string') {
const m = b.text.match(REASONING_TAG_RE);
if (m) {
const split = (s) => s.split(';').map((x) => x.trim()).filter(Boolean);
return { triggers: split(m[1]), candidates: split(m[2]), boundaries: split(m[3]) };
}
}
}
}
return null;
}
/** Text of the last real user prompt — used by the Stop-hook routing-gate (Task 5). */
export function extractLastUserPromptText(transcriptText) {
const { entries } = parseLines(transcriptText);
@@ -615,17 +640,21 @@ export function parseTranscript(transcriptText, fallbackSessionId = null) {
environment: extractEnvironment(entries, start),
task_size: extractTaskSize(turn),
task_cost: extractTokenUsage(turn),
primary_rationale: {
step: 1,
node_chosen: skills.length > 0 ? skills[0] : 'direct',
triggers_matched: extractTriggers(turn),
candidates_considered: extractCandidates(turn),
boundaries_applied: extractBoundaries(turn),
hard_floor: usedSuperpowers
? { invoked: true, rules: ['Pravila §12'] }
: { invoked: false, rules: [] },
task_classification: classifyTask(prompt),
},
primary_rationale: (() => {
const tag = parseReasoningTag(turn);
const merge = (heur, fromTag) => [...new Set([...heur, ...fromTag])];
return {
step: 1,
node_chosen: skills.length > 0 ? skills[0] : 'direct',
triggers_matched: merge(extractTriggers(turn), tag ? tag.triggers : []),
candidates_considered: merge(extractCandidates(turn), tag ? tag.candidates : []),
boundaries_applied: merge(extractBoundaries(turn), tag ? tag.boundaries : []),
hard_floor: usedSuperpowers
? { invoked: true, rules: ['Pravila §12'] }
: { invoked: false, rules: [] },
task_classification: classifyTask(prompt),
};
})(),
events,
};
}