feat(voyage): S6 — v5.5 brief framing enforcement (brief_version 2.2)
Implements the CLAUDE.md cross-cutting invariant "brief framing must match operator intent" as a controlled brief_version 2.1->2.2 bump (operator option A1). Three defense layers, version-gated at >=2.2 so existing 2.0/2.1 briefs stay valid (forward + backward compatible), mirroring the phase_signals >=2.1 gate: - L1 framing: enum field (preserve|refine|replace|new-direction). Enum-checked on any version when present (BRIEF_INVALID_FRAMING); missing at >=2.2 -> BRIEF_MISSING_FRAMING. /trekbrief Phase 2.5 collects it BEFORE any brief prose (non-skippable, even in --quick). - L2 memory alignment: new brief-reviewer dimension 6 comparing brief Intent/Goal + framing against operator memory for explicit contradictions; degrades to score 5 (N/A) when no memory context is supplied. Wired into Phase 4e gate (memory_alignment.score >= 4). - L3 obligatory ## TL;DR (<=5 content lines) at >=2.2; soft cap -> BRIEF_TLDR_TOO_LONG warning. trekreview briefs are exempt from the framing/TL;DR gate. Handover 1 PUBLIC CONTRACT doc, README "What's new", and the CLAUDE.md invariant + agents table (brief-reviewer 5->6 dimensions) updated to 2.2 (schema axis only; plugin version badge + CHANGELOG remain S10). Iron Law followed: validator tests red->green first. Tests 586 -> 606 (+20, 604 pass / 2 skip). claude plugin validate passes (pre-existing CLAUDE.md root-context warning unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
This commit is contained in:
parent
fa23b16443
commit
736ae55d66
10 changed files with 409 additions and 31 deletions
|
|
@ -18,6 +18,23 @@ export const BRIEF_RESEARCH_STATUS_VALUES = ['pending', 'in_progress', 'complete
|
|||
export const BRIEF_BODY_SECTIONS = ['Intent', 'Goal', 'Success Criteria'];
|
||||
export const PHASE_SIGNAL_PHASES = Object.freeze(['research', 'plan', 'execute', 'review']);
|
||||
export const EFFORT_LEVELS = Object.freeze(['low', 'standard', 'high']);
|
||||
// v5.5 — framing: how this brief relates to prior operator intent (the first layer
|
||||
// of the framing-alignment defense). Required at brief_version ≥ 2.2.
|
||||
export const BRIEF_FRAMING_VALUES = Object.freeze(['preserve', 'refine', 'replace', 'new-direction']);
|
||||
// v5.5 — obligatory TL;DR section (≤ 5 content lines) at the top of brief.md,
|
||||
// gated at brief_version ≥ 2.2. Soft cap is a warning, not a blocker.
|
||||
export const BRIEF_TLDR_MAX_LINES = 5;
|
||||
|
||||
// Extract the raw text of a `## {heading}` section body (between its heading line
|
||||
// and the next `## ` heading, or end of document). Returns null if absent.
|
||||
function extractSection(body, heading) {
|
||||
const re = new RegExp(`^##\\s+${heading}\\b.*$`, 'm');
|
||||
const m = re.exec(body);
|
||||
if (!m) return null;
|
||||
const after = body.slice(m.index + m[0].length);
|
||||
const next = after.search(/^##\s/m);
|
||||
return next === -1 ? after : after.slice(0, next);
|
||||
}
|
||||
|
||||
function getRequiredFields(type) {
|
||||
return type === 'trekreview' ? REVIEW_AS_BRIEF_REQUIRED_FRONTMATTER : BRIEF_REQUIRED_FRONTMATTER;
|
||||
|
|
@ -88,12 +105,23 @@ export function validateBriefContent(text, opts = {}) {
|
|||
// a string ("2.1") or a number (2.1). v5.1.0 shipped with an unquoted-2.1 template
|
||||
// that silently bypassed this gate — fix locked in by quoting the template AND
|
||||
// accepting both shapes here as defense-in-depth (v5.1.1, finding 3c834097/df1435a2).
|
||||
// v5.5 — framing enum check fires on ANY version when the field is present but
|
||||
// malformed. The missing-framing BLOCKER below is version-gated (≥ 2.2).
|
||||
if ('framing' in fm && !BRIEF_FRAMING_VALUES.includes(fm.framing)) {
|
||||
errors.push(issue(
|
||||
'BRIEF_INVALID_FRAMING',
|
||||
`framing "${fm.framing}" not in [${BRIEF_FRAMING_VALUES.join(', ')}]`,
|
||||
'framing declares how this brief relates to prior operator intent.',
|
||||
));
|
||||
}
|
||||
|
||||
if (typeof fm.brief_version === 'string' || typeof fm.brief_version === 'number') {
|
||||
const vm = String(fm.brief_version).match(/^(\d+)\.(\d+)$/);
|
||||
if (vm) {
|
||||
const major = Number(vm[1]);
|
||||
const minor = Number(vm[2]);
|
||||
const atLeast21 = major > 2 || (major === 2 && minor >= 1);
|
||||
const atLeast22 = major > 2 || (major === 2 && minor >= 2);
|
||||
if (atLeast21 && !hasSignals && !hasPartial && fm.type !== 'trekreview') {
|
||||
errors.push(issue(
|
||||
'BRIEF_V51_MISSING_SIGNALS',
|
||||
|
|
@ -101,6 +129,29 @@ export function validateBriefContent(text, opts = {}) {
|
|||
'Re-run /trekbrief — Phase 3.5 collects per-phase effort + model signals.',
|
||||
));
|
||||
}
|
||||
// v5.5 framing enforcement — gated at ≥ 2.2 (trekreview briefs are exempt).
|
||||
if (atLeast22 && fm.type !== 'trekreview') {
|
||||
if (!('framing' in fm)) {
|
||||
errors.push(issue(
|
||||
'BRIEF_MISSING_FRAMING',
|
||||
'brief_version ≥ 2.2 requires a framing: field',
|
||||
`Set framing to one of [${BRIEF_FRAMING_VALUES.join(', ')}] — /trekbrief Phase 2.5 collects it before any brief prose is written.`,
|
||||
));
|
||||
}
|
||||
const tldr = extractSection(body, 'TL;DR');
|
||||
if (tldr === null) {
|
||||
const tldrIssue = issue('BRIEF_MISSING_SECTION', 'Required body section missing: ## TL;DR');
|
||||
if (strict) errors.push(tldrIssue); else warnings.push(tldrIssue);
|
||||
} else {
|
||||
const lines = tldr.split('\n').map(l => l.trim()).filter(Boolean);
|
||||
if (lines.length > BRIEF_TLDR_MAX_LINES) {
|
||||
warnings.push(issue(
|
||||
'BRIEF_TLDR_TOO_LONG',
|
||||
`## TL;DR has ${lines.length} content lines (max ${BRIEF_TLDR_MAX_LINES}) — keep it to a one-glance summary`,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue